Race condition operation in CTF in practice

Depov

Moderator
Staff member
MODERATOR
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
506
Reaction score
845
Deposit
0$
Limit Overrun – TOCTOU vulnerability

The most common type of race clause in CTF. The server checks the condition (Time of Check), then performs the action (Time of Use). Between verification and action – race window, window in units of milliseconds, where you can push parallel requests.

Pattern in code:

Time of Check
coupon = db.query("SELECT used FROM coupons WHERE code = ?", code)
if coupon.used:
return
Race window
Time of Use
db.execute("UPDATE coupons SET used = true WHERE code = ?", code)
apply_discount(order)

All requests that have entered the race window are checked if coupon.used with result False - no one's gotten to the UPDATE. A one-time coupon is used as many times as requests have managed to slip.

TOCTOU vulnerability in CTF occurs in tasks on:

Repayment of gift cards and bonus points many times
Rounding up the rate-limit with brute-force OTP codes
Double write-off or balance accrual
Reuse of CAPTCHA Solution
Multiple voting or evaluations

In bounty programs, this class brings stable payments. On HackerOne (report #759247) describes exactly this scenario: one gift card was repaid in parallel requests and used many times. According to PortSwigger, limit overrun is a subtype of TOCTOU vulnerabilities, but there are also more sly varieties.
Single-endpoint Collision

A thinner type of streaming race in a web application. Two queries to the same endpoint simultaneously change the same field. The second overwrites the data of the first before the first uses it.

A classic example is CVE-2022-4037 in GitLab CE/EE. According to NVD: CVSS 6.4 (MEDIUM), vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N, CWE-362. All versions of GitLab were affected by 15.5.7, 15.6 to 15.6.4 and 15.7. to 15.7.2. Mechanics:

Stream A records pending_email = "[email protected]"
Flow B overwrites pending_email = "[email protected]"
Stream A reads pending_email for generating a token - and there already [email protected]
Stream A sends a letter to [email protected], but the token is tied to [email protected]

The attacker receives a confirmation token for someone else's email. This leads to the forgery of verified emails and the capture of accounts when using GitLab as an OAuth provider. According to CISA (SSVC Decision: Track), operation is not automated and the technical impact is partial – but in CTF tasks single-endpoint compilers adore. PortSwigger has a separate lab on this vector.
Multi-endpoint Race

The most difficult type. The race takes place between queries to different endpoints that work with shared data. Typical CTF scenario: one endpoint adds the item to the cart, the other applies a discount, the third initiates payment. If you send a request for payment simultaneously with the change of the cart, the server can process the payment at the old price with the new contents.

Multi-threaded attack on a web application with multi-endpoint race is complicated by the fact that different endpoints are processed at different speeds. Here you need additional synchronization techniques:

Quick endpoint padding – add “junk” parameters or headers to align processing time with slow endpoint
Connection warning – Pre-queries warm up the server cache and connection, reducing the spread of delays
HTTP/2 multiplexing – sending through a single connection minimizes the spread over delivery time

Single-packet attack: how to neutralize the network jetter

The main enemy of the operation of the race state on a remote server is the network jitter. The variation of delay in the network is tens of milliseconds, and race window is often measured by units. Without minimizing the jitter, the HTTP query race turns into a lottery with lousy odds.
Last-byte synchronization for HTTP/1

For servers on HTTP/1.1, the last-byte synchronization technique is used:

Open N parallel TCP connections to the server
For each we send all HTTP request except the last byte of the body
The server holds partially received requests in the buffer - the processing does not start
Simultaneously send the last byte to all N connections
The server receives N “completed” requests at almost one moment

Jitter only affects the delivery of one byte through an already installed TCP connection - the order is less than the delivery of a whole request. Burp Suite automatically applies this technique when sent to HTTP/1 servers in parallel.

Limitation: Each request requires a separate TCP connection. With 20-30 requests, this is 20-30 parallel connections, and you can rest on server limits MaxClients or firewall rules. There are usually no problems in the CTF, in production - how lucky.
Single-packet technique for HTTP/2

HTTP/2 supports multiplexing – multiple requests through a single TCP connection. Single-packet attack race clause uses just that:

We generate N full HTTP/2 requests
We hold the final fragments of each - the server is waiting for completion
Packing all final fragments into one TCP pack
We send - the server receives all requests within a single network operation

This technique was first shown by PortSwigger researchers at Black Hat USA 2023. Single-packet attack completely removes the network jitter: all requests come literally in one package, the difference in delivery time is zero. Remote race clause becomes "local" in reliability.

In practice, 20-30 HTTP/2 requests are placed in one TCP package – for most CTF tasks, there are enough eyes. Why 20-30 instead of 2? Server jitter (internal delays in routing the request to the handler) does not go anywhere. The more requests in the pack, the higher the chance that several of them will get into the race window at the same time. For intelligence - 20-30. When the behavior is confirmed and pure exploit is needed, we reduce to 2-5.
Burp Suite and Turbo Intruder for race condition
Burp Repeater – Send group in parallel

With the Burp Suite 2023.9 version, Repeater has built-in parallel shipment support. For basic operation of race clause in CTF this is enough:

We intercept the target request through Proxy (e.g., POST /apply-coupon with body code=PROMO20)
We send to Repeater, duplicate the tab 20-25 times through Ctrl+R
Select all tabs, right-click - "Send group in parallel"
See the answers: if several requests returned HTTP 200 with confirmation of the discount - race condition confirmed

Burp himself chooses the synchronization technique on the protocol. HTTP/2 is a single-packet attack. HTTP/1 – last-byte synchronization.

The moment that is often missed: if the first attempt did not work, it does not mean that there is no race condition. Server jetter (GC-pause, flow pool locks, cash failures) can shift processing so that requests will not fall into one window race. Repeat 5-10 times before dropping the hypothesis. The CTF usually has 3-5 attempts.
Turbo Intruder: Single-packet attack script

For scenarios, it is more difficult - multi-endpoint race, racing with different timings, thousands of queries - need Turbo Intruder. The script on Jython gives full control over timing and parameters.

def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2)
for i in range(20):
engine.queue(target.req, gate='race1')
engine.openGate('race1')

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

What's up for what:

engine=Engine.BURP2 – includes single-packet attack for HTTP/2. For HTTP/1 take Engine.THREADED or Engine.BURP
concurrentConnections=1 – one TCP connection, all requests through multiplexing. For HTTP/1, bet 10-30
gate='race1' – requests are hoarded in the “gate” and do not go to the server before the call openGate('race1')

This Template (race-single-packet-attack.py) lies in the standard Turbo Intruder example directory. For the CTF task - intercepted the request, sent to Turbo Intruder, launched the script. Everything.

For custom scenarios — for example, alternating queries to two different endpoints — modify the query body before engine.queue(), substituting the desired path or parameters. Turbo Intruder allows you to set the delay between groups through time.sleep() in Jython — useful for multi-endpoint race when you need to adjust the timing.

Outside of Burp Suite, there are other approaches: custom Python scripts with asyncio/aiohttp, the Race the Web utility. But for CTF, the Burp Repeater + Turbo Intruder bundle covers 95% of tasks.
Search classification methodology in CTF tasks

The PortSwigger methodology is built in three stages: prediction, sensing, confirmation.

Stage 1 – predicting conflicts. We are looking for endpoints with competitive access to resources. In CTF tasks, these are:

Forms with one-time actions: the use of a coupon, activation of the invite, sending a solution
Endpoints with pattern "read → check → write" (balance, limits, meters)
Change email/password with confirmation tokens
Any operations marked “once” or rate-limit

For each candidate, three questions. Where is the condition stored? Server database is ideal for operation. Client JWT is useless. Editing or adding? Operations that change existing data (change of email) give more potential for conflicts. The common key? For a successful race, you need two operations that turn to one key - one user_id, one coupon_code.

Stage 2 – looking for clues. We send a pack of 20-30 parallel queries and compare the answers with normal (sequential) behavior:

Different HTTP codes in the responses to identical requests (part 200, part 409 or 403)
Differences in content: different balance values, different redirect-URL
Side effects: two letters instead of one, double-edition in profile
 
Top Bottom