Somewhere in the vast expanses of the wild internet, there is a site I sometimes frequent that likes to put surveys in their blog posts. Quite by accident, I discovered that if I reload the page in an incognito window, I could vote in these surveys multiple times. Being hopelessly curious about what's going on behind the curtain, I ventured down the rabbit hole that led me to spammy scripts and purple underwear (No, seriously...).
When I first encountered the behavior with the incognito browser, I figured there had to be some sort of local state that was tracking whether or not I had voted, which incognito was ignoring. Now, it only worked the first time incognito loaded... simply reloading the incognito window wasn't enough to fool the survey (you already voted!). My initial idea was that there was a cookie being set, which wasn't there on the initial load but stuck around after reloading (which will happen with incognito). Delete cookies, reload.... aaaaaand no dice. Hmmm, isn't that interesting...
So I figured there must be somewhere else that this state is being persisted. "Well, what about local storage?" thought the intrepid inquisitor... and low and behold... (Note: I don't shop at "The Purple Store"... it's just some weird site I found that uses the same plugin. Based on the survey results I'd say their poll was already abused anyway, so I really didn't feel guilty about messing with it)
When I voted, num_votes got set equal to one... I wonder what happens if I edit the value and set it to ... oh I don't know, -1000 maybe? Reload, vote, reload, vote, reload... each vote increments by one.... interesting...
But as interesting as this was, it was kind of tedious. Now, in Chrome it turns out there is an easy way to replay AJAX requests. In the dev console, under Network, just right click the request and select "Replay XHR", and voila. Off it goes:
This will work, and doesn't even require the local storage hack to work (well if you are already "out of votes" you would have to modify local storage to get the first request, but after that you're golden).
This would work fine if you were bored and wanted to vote twenty times, but I knew that in order to really abuse the poll, I would have to find a way to automate this process. The first couple StackOverflow articles I found on the subject were doing all this funny business with creating forms in memory and "submitting" them, but luckily I found one that explained how to do it with just a plain xmlHttpRequest object.
I fumbled around with it a bit. At first it didn't seem to be working, but when I finally got to the root of the issues I was having, it basically amounted to not setting up the xhr object right. I was trying to reuse the same request object, reopening and resending, but then you have to reset the Content-Type header every time and it was just a pain, so I wrapped it all up in a neat little function:
function spam(times){ var http = new XMLHttpRequest(); var url = "{teh url}"; var params = "{teh form data}"; http.open("POST", url, true); //Send the proper header information along with the request http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); //http.setRequestHeader("X-Forwarded-For", randomIp()); http.onreadystatechange = function() {//Call a function when the state changes. if(http.readyState == 4 && http.status == 200) { var stat = http.responseText == "0" ? "Fail!" : "Success!"; console.log(stat); if(times > 1){ //setTimeout(function () { spam(times - 1); }, 1000); spam(times - 1); } } } http.send(params); }
The only bits that need to be filled in are the url and the params, which come from the original request in the Chrome dev console (make sure you select "view source" on the Form Data):
The funny business with the "X-Forwarded-For" header is due to an article I found from a WordPress publisher, who found he was the target for an exploit (from 2009, yeesh, why are people still using this???). I thought maybe my failures had something to do with not setting that header, but it turned out to be irrelevant. Basically the same story with "setTimeout", I thought maybe a flood of requests was causing it to crap out, but alas, also not the problem. ¯\_(ツ)_/¯
Once I squashed all the bugs, this worked as well as I'd hoped. I can fire it in rapid succession and it will spam the hell out of whichever survey option I've deemed to be the one true answer. Briefs (the target of my script) have a long way to go, though... and having proven this works I'm basically too bored with it to bother changing the outcome. Guess "I LIKE CLICKING PURPLE THINGS" is still the champ. Whatever... lol
So, what's the point of all this? Mostly I was just curious to see if it was possible. I would say if you want your polls to be meaningful, think seriously about how you would address these issues. I don't know if the plugin is borked or if this is a WordPress configuration issue. I don't know WP, like, at all, so I can't say. Having me discover this weakness on your site is relatively benign (I'm not looking to cheat anyone... or even troll anyone for that matter), but that may not be true of the next guy that figures it out.
And the original site I found this on? I brought it to their attention, but I have a feeling it's a low priority. I think if I broke it they'd just pull the surveys, which would make me super sad, so I'm not going to do that.... well... maybe just a little <evil laugh>... :P
No comments:
Post a Comment