Discounts by Design: Rigging an online raffle
Premise
Recently, a popular Youtuber and a professional climber Magnus Midtbø started his brand of bouldering apparel and accessories called Rungne. Because of a collaboration with another climbing YouTuber, a raffle was organized on the site and the prize pool contained different merchandise discount codes, a free shipping code, and most valuable of all, a 500$ gift card. You’d only need to input your name and email, and spin the wheel of fortune. Obviously, I entered my details to have a crack at the lottery and got back a demotivating 20% discount code for an item.
Out of curiosity I then took a deeper look at how this lottery works. I assumed that the client would send the server your details and get back a discount code. To confirm my hypothesis I had a look at the network traffic that happened right after clicking the Spin button. Indeed there was a GET request sent to getdrip.com
, with my information after clicking the button.
And the browser displays the response nicely congratulating me for winning a coupon code.
Things get strange
What I could not see from the request’s response was the coupon code itself.
What was even more strange was that the code was already present in the first request that was sent out with my credentials.
This had me confused and wondering where the coupon code could be. I searched for the code’s string in the site’s HTML which yielded a couple of locations. However, after refreshing the site the coupon code string was gone from the HTML. This meant that the coupon code was added at some point of the page load or interaction with the pop-up.
Next, I put a breakpoint on that DOM element to catch when and how it was being edited and refreshed the site with a clean cache and no stored cookies. I then filled out the same information and hit spin the wheel. This took me to the function that changed the DOM. Whilst I didn’t find the code in the local variables, I did find it among the global variables.
Looking through the call stack didn’t take me any further, but since I found the global variable containing the coupon code I could search for it within all loaded Javascript files.
This led to finding a function called propagateRewards
in the file spintowin.js
. Just above this function, there was another one called parseRewards
. This looked like the jackpot. Putting a breakpoint on the JSON parse and decode line, resetting the site’s data, and refreshing the page, I was presented with a base64 encoded string in the local variables.
Decoding this gave out all the possible coupon code results.
$ echo "W3sid2VpZ2h0IjoiMTAiLCJ0ZXh0IjoiMjAlIG9mZiBIYXJuZXNzIFBhbnRzIiwiY29kZSI6Ii4uLiJ9LHsid2VpZ2h0IjoiMzAwIiwidGV4dCI6IjIwJSBvZmYgQW5jaG9yIFBhbnRzIiwiY29kZSI6Ii4uLiJ9LHsid2VpZ2h0IjoiMCIsInRleHQiOiJOb3QgdG9kYXkiLCJjb2RlIjoiMCJ9LHsid2VpZ2h0IjoiMzgiLCJ0ZXh0IjoiRnJlZSBTaGlwcGluZyIsImNvZGUiOiIuLi4ifSx7IndlaWdodCI6IjEiLCJ0ZXh0IjoiR2lmdCBjYXJkICQ1MDAiLCJjb2RlIjoiLi4uIn0seyJ3ZWlnaHQiOiIzMDAiLCJ0ZXh0IjoiMjAlIG9mZiBIaWdoYmFsbGVyIFBhbnRzIiwiY29kZSI6Ii4uLiJ9XQ==" | base64 -d | jq
[
{"weight":"10","text":"20% off Harness Pants","code":"..."},
{"weight":"300","text":"20% off Anchor Pants","code":"..."},
{"weight":"0","text":"Not today","code":"..."},
{"weight":"38","text":"Free Shipping","code":"..."},
{"weight":"1","text":"Gift card $500","code":"..."},
{"weight":"300","text":"20% off Highballer Pants","code":"..."}]
Seeing this I was hit by a couple of things. Firstly, instead of sending one coupon code the server sends all of them to the client and the client then chooses one themselves. And secondly, this all happens before the email and name are ever entered into the raffle form. This was confirmed by placing a breakpoint in the parseRewards
function and refreshing the site. The breakpoint would be hit BEFORE even getting the fortune wheel pop-up. We can see the same base64 string in a response to a request made during the page load. YIKES! 😬
I then had a look at where and how the prize was chosen. Because we see from the base64 data that each code has a weight attached to it, I assumed the choice was made using some randomness, so I searched for the string ‘random’ in the sources tab and found the exact place where it was chosen.
Knowing where and how the code was chosen, one could just change the randomly selected index to be for example the index corresponding to the 500$ gift card and then let the script run on, enter their details in the pop-up’s form, and hit submit. The result would then be that the browser requests the marketing service at getdrip.com
saying that we “randomly” got the best prize in the raffle.
Lessons to be learned
This fortune wheel lottery was undermined by a couple of things. Firstly, the client should obviously not be the one to choose the prize. A proper system would first let the user enter their details and then upon receiving them on the back end, would send back the prize. Secondly, having a free item or even worse, free money, as a possible prize is rather risky, as it is a big incentive for people to try and cheat the system.
Though I don’t advocate its use, having some kind of obfuscation could have helped prevent finding this out. It wouldn’t have solved the problem of client-side verification but at least made it less likelier for people to find and abuse the bug.
Reporting the issue
I notified the Rungne store about this issue and also tried messaging Sleeknote, the marketing service proders behind this pop-up, about the bad lottery design and how it is possibly hurting their customers. Since Sleeknote didn’t have a security.txt on their site I tried reaching out to them through the only email address they had available on their site. After multiple unanswered emails I noticed that they were bought by another company named Drip roughly a year ago. Fortunately they had a security.txt file and after a short back-and-forth with them they said that they will have someone look into it. Few weeks later they reported that they considered it fixed on their end.
Conclusion
This was a fun little challenge to solve, quite CTF-esque. Unfortunately, the “fix” that was put in place is a disclaimer which they added to the campaign creation page. It states that they “recommend refraining from using high-value codes in Spin to Win campaigns to minimize the risk of access by malicious actors” and link to a Learn More page. In my opinion, this isn’t a proper fix as it just shifts the responsibility to the customer. The codes can still be trivially collected and used, causing the customer to lose more money than they otherwise might have. However, I do understand that technically this is a solution and now the users can and have to decide what they will do.