Sli.do and the rigged company vote
2018-11-14 | Security
Using the popular Sli.do application, employees are asked to submit and vote on questions that should be discussed on an upcoming meeting. One of the employees decides to rig the vote in favor of their questions. In this post, I will try to reconstruct a way how the employee could have done it.
A little bit of background first though: A Sli.do is an application with both web and mobile interface that is typically used in the meetings with higher volume of attendees. Its purpose is to simplify asking the questions and selecting what questions should be answered. The attendees submit their question on Sli.do and other attendees can see it and vote it up or down. The meeting organizer can then pick the most popular questions and answer them first.
In this not so imaginary scenario, the meeting is announced a couple of days in advance and some highly-emotional, I dare to say controversial even, questions appear. Now employees, being emotional human beings and all, start the voting and fierce discussion. Next thing we know, the meeting time comes and the Sli.do board looks something like this:
As you can see, a lot of people voted. The problem is, though, that the company only employs about 90 people, of which only the half is interested in actually coming to the meeting. The numbers do not add up. Someone cheated.
Naturally, once I have heard about this, I decided to explore what happened, and how this highly motivated and obviously bored employee managed to get so many upvotes on the questions.
The first logical step I took was trying to vote from incognito browser window. Logical, because Sli.do voting is designed to work for pretty much anyone with the event code or the link, while - I suspect - preventing them from voting multiple times, once they cast their votes.
So I created myself a testing sli.do event (you could have seen it already in the previous screen), posted sample question and voted once. Then I opened the incognito window, entered the event code and of course, was able to vote again.
While voting, I did not observe any page reload or anything similar, which led me to conclusion that XmlHttpRequest (XHR) is used to count in my vote. I opened the developer's tools, chose Network tab and tapped on XHR filter, and sure enough, it was there, the call to like endpoint:
POST /api/v0.5/events/732003/questions/6733294/like HTTP/1.1
Host: app2.sli.do
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0
Accept: application/json, text/plain, */*
Authorization: Bearer 4479c04de132299e18ab831208a697f2bfbdf950
{"score":1}
Note that I omited most of the headers displayed in the developer's console and I will continue doing so for all the following requests, because they do not contain important information for the following text.
What immediately picked my interest here was the authorization header using the bearer. My gut feeling was telling me that if I only replay the XHR, the vote will not be counted in again, but maybe if I slightly altered the token and tried again, it could work. The former assumption has proved itself right, but the latter has not. I got the 401 Unauthorized with "Bad token" message.
That's good, all I needed to do now was to get the correct token. And getting the correct token should be a trivial task, considering every new incognito session gets one.
rom the endpoint URL, I also deduced that 732003 is the ID of my event and 6733294 is the question ID of the question I voted on. It was time to look for the way to get fresh bearer token.
I inspected the Network tab once again and noticed another request to very interesting endpoint - auth:
POST /api/v0.5/events/427957c2-868c-480a-b1f7-e46f82c28c61/auth HTTP/1.1
Host: app2.sli.do
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0
Accept: application/json, text/plain, */*
{}
Pretty standard POST request against interestingly named endpoint. The most interesting part here is maybe the guid 427957c2-868c-480a-b1f7-e46f82c28c61. It tells the endpoint for what event the token should be generated, but it does not matter to us much now. Once issued, valid bearer token was provided back in the response:
HTTP/2.0 200 OK
content-type: application/json; charset=utf-8
{"access_token":"4479c04de132299e18ab831208a697f2bfbdf950","event_id":732003,"event_user_id":29430145}
Going through the requests above told me the following:
- To cast a vote, fresh bearer token must be obtained.
- Bearer token can be obtained by sending a POST request with empty JSON object in the body against auth endpoint.
- To cast a vote, I need to send a POST request against like endpoint with bearer token in authorization header and the JSON body {"score":1}.
Using the cURL to quickly test my ability to cast a vote, I satisfy each point of the above as follows.
Obtaining a bearer token
I call the auth endpoint, specifying the event guid which I extracted from the original browser's XHR call. It does not change over time, it remains the same for the event's duration.
curl "https://app2.sli.do/api/v0.5/events/427957c2-868c-480a-b1f7-e46f82c28c61/auth" --data "{}"
Endpoint replies with a JSON message containing the token:
{"access_token":"0d55acc730d032b771b0c63c81d7f535aace694b","event_id":732003,"event_user_id":666NotReally666}
For now, let's just remember that the token starts at index 18 and ends at index 57 of the response body.
Casting a vote using the bearer token
Again, I use cURL to call the like endpoint, specifying the bearer token in authorization header and providing correct event ID and question ID in the endpoint URL.
curl "https://app2.sli.do/api/v0.5/events/732003/questions/6733294/like" -H "Content-Type: application/json;charset=utf-8" -H "Authorization: Bearer $THE_TOKEN" --data "{\"score\":1}"
Instead of $THE_TOKEN I specified the value returned by auth response. And yeah, the vote was recorded.
Chaining it together
I am not exactly a bash-master, but I was able to chain these together, extracting the token from the auth response in between. My gut feeling tells me that this is not how you should be chaining cURL calls, but hey, it's a fast PoC:
curl "https://app2.sli.do/api/v0.5/events/732003/questions/6733294/like" -H "Content-Type: application/json;charset=utf-8" -H "Authorization: Bearer `(curl "https://app2.sli.do/api/v0.5/events/427957c2-868c-480a-b1f7-e46f82c28c61/auth" --data "{}" | cut -c 18-57)`" --data "{\"score\":1}"
Essentially, I sent a request to the like endpoint where the value for the authorization header is supplied by cut-ting the response of the nested cURL call to auth endpoint. Ugly, but did the job. I could then wrap it all in an endless loop and enjoy all the upvotes. Mission accomplished.
Few questions remain though:
- Can I specify higher score than 1 when voting to increase votes faster? No. 400, Bad request.
- Can I decrease votes? That depends. If downvotes are allowed on the questions (that's something that meeting organizer has to set up), yes, -1 is a valid value for the score. Otherwise no, 400, Bad request. Same thing happens if you try to decrease by less then -1.
You have probably noticed, I am not that well-built in bash programming, so when I was first implementing this, I used python and did consider a lot of unnecessary corner cases and added some autoconfiguration capabilities. You can check that out on my Github. It works with multiple additional endpoints that sli.do provides.
Few words before I end: Nothing of what I have described above is a security issue or a functional bug. Sli.do application is written this way on purpose - the most important thing for applications of this nature, is to never prevent a legitimate user from casting their vote or asking the question. And that is exactly what Sli.do correctly does.
And that's about it, thanks for reading me!