上一章我们提炼出"绕过 = 解析差异"。这一章,我们要把这条原理推到极致—— 推到 HTTP 协议本身。 请求走私(Request Smuggling)是全书技术难度最高的一类漏洞, 也是"解析器不一致"这个思想最纯粹、最惊人、破坏力最大的体现。
21.0 开篇:一个请求,污染了所有人
现代网站几乎都有一个"反向代理"架构:
用户 → [前端代理/CDN/WAF] → [后端应用服务器] → ...
这个架构里,前端和后端是两个独立的、都要"解析 HTTP"的组件。
有一天,攻击者发出了一个"奇怪"的请求:
POST / HTTP/1.1
Host: example.com
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED
注意这个请求里同时带了 Content-Length 和 Transfer-Encoding——这是"违规"的,但没被拒绝。
接下来,神奇的事情发生了:
【前端代理】认 Transfer-Encoding:
→ body 是 "0\r\n\r\n",到此**请求结束**
→ "SMUGGLED" 被当成**下一个请求的开头**,放进连接缓冲区
【后端应用】认 Content-Length: 13:
→ body 是前面 13 个字节
结果:后端把 "SMUGGLED" 当成了"下一个请求"的一部分,
而下一个请求,**可能是另一个无辜用户发来的!**
🔥 请求走私的恐怖之处:
攻击者的"半截请求",会被"拼"到下一个无辜用户的请求前面。 于是,攻击者可以在"别人的会话里"执行任意操作——而受害者完全不知情。
它不需要用户点任何东西,不需要任何交互。中间那条连接上的每一个用户,都可能成为受害者。
21.1 HTTP 请求的"边界"是怎么定出来的
要理解请求走私,先要理解一个最基础的问题:一个 HTTP 请求,到哪里结束?
服务端要靠"头部"来知道 body 有多长。有两个头部能表达这件事:
① Content-Length: 13
→ "body 正好 13 个字节,第 14 个字节开始就是'下一个请求'"
② Transfer-Encoding: chunked
→ "body 是'分块'传输的,每个块前面标长度,直到遇到 0 长度的块为止"
正常情况下,一个请求只会用其中一个。 但如果两个同时出现(或一个畸形),就出问题了:
问题在于:规范对"两个同时出现怎么办"的规定很模糊,
而不同的实现(Nginx / Apache / Tomcat / Node / ...)"选择"了不同的处理方式:
有的"优先认 Content-Length"
有的"优先认 Transfer-Encoding"
有的"遇到两个就拒绝"
有的"把其中某一个删掉/忽略"
🔥 核心:
当"前端"和"后端"对"这个请求到哪里结束"的判断不一致时, 多出来的那部分(攻击者精心构造的"半截请求")就会被"走私"到后端, 成为"下一个请求"的开头。
21.2 四种请求走私类型
按"前端认哪个 / 后端认哪个"分,有四种主要类型。记住这张分类表,就抓住了请求走私的骨架。
| 类型 | 前端认 | 后端认 | 走私方向 |
|---|---|---|---|
| CL.TE | Content-Length | Transfer-Encoding | 前端→后端 |
| TE.CL | Transfer-Encoding | Content-Length | 前端→后端 |
| TE.TE | 都认 TE,但其中一个被"混淆" | 同 | 视实现 |
| CL.CL | 都认 CL,但值不同 | 同 | 特殊场景 |
21.2.1 CL.TE
前端认 CL,后端认 TE。
POST / HTTP/1.1
Host: example.com
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED
【前端认 CL=13】:body 取 13 字节 = "0\r\n\r\nSMUGGLED"
→ 前端认为这个请求到此结束
→ 把后面的内容直接转发给后端
【后端认 TE】:body 按 chunked 解析
→ "0" 表示结束
→ 那么 "SMUGGLED" 就是"下一个请求"的开头!
→ 后端把这个"多出来的"当成了新请求
💡 记住这个记忆法:CL.TE = 前端按"长度"看,后端按"分块"看,前端"多算"了。
21.2.2 TE.CL
前端认 TE,后端认 CL。
POST / HTTP/1.1
Host: example.com
Content-Length: 3
Transfer-Encoding: chunked
8
SMUGGLED
0
【前端认 TE】:完整解析 chunked
→ "8\r\nSMUGGLED\r\n0\r\n\r\n" 是一个完整的 body
【后端认 CL=3】:只取 3 个字节(= "8\r\n")
→ 剩下的 "SMUGGLED..." 成为"下一个请求"的开头
⚠️ TE.CL 有个坑:CL 的值要精确算对(比如上面是 "8\r\n",3 个字节),用 Burp 时要关掉"自动更新 Content-Length"。
21.2.3 TE.TE(两个都认 TE,但被混淆)
前端和后端都优先认 TE,但其中一个"认不出来"被混淆的 TE 头。 例如:
Transfer-Encoding: chunked
Transfer-Encoding: x
有的实现看到"两个 TE 头" → 用第一个(chunked)
有的实现遇到"不认识的值 x" → 忽略 TE,退回用 CL
→ 于是又变成了"一个认 TE,一个认 CL"
常见的"混淆"手法:
Transfer-Encoding: xchunked
Transfer-Encoding : chunked ← 头名后加空格
Transfer-Encoding: chunked
Transfer-Encoding: x ← 两个头
Transfer-Encoding:\tchunked ← 用 tab
Transfer-Encoding: chunked, identity ← 多值
X-Transfer-Encoding: chunked
21.2.4 CL.CL(较少见)
两个 CL,值不同——前端和后端各自取了"不同的那个":
Content-Length: 10
Content-Length: 20
有些实现取第一个,有些取最后一个,有些取最大值。 差异同样可以造成走私。
21.2.5 走私类型速查表
CL.TE → 前端认长度,后端认分块 (最常见)
TE.CL → 前端认分块,后端认长度
TE.TE → 两个都认分块,但一个被混淆
CL.CL → 两个都认长度,但值不同
💡 怎么判断是哪种? 用时间延迟技巧(见 21.3)。
21.3 怎么"检测"请求走私
请求走私是"盲"的——你看不到"被走私的那半截请求"发生了什么。所以检测靠间接信号。
21.3.1 技巧一:时间延迟检测
思路:构造一个"会让后端卡住"的走私请求——如果走私成功,后端会"等"后面的数据,导致响应延迟。
CL.TE 的探测(让后端"等"一个永远不完整的分块):
POST / HTTP/1.1
Host: example.com
Content-Length: 4
Transfer-Encoding: chunked
1
A
X ← 这里故意制造不完整:后端按 TE 解析,看到一个"未结束的块",会等待更多数据 → 超时
→ 如果响应延迟 → CL.TE 存在
TE.CL 的探测(让后端按 CL 读到一个"不完整的分块"):
POST / HTTP/1.1
Host: example.com
Content-Length: 6
Transfer-Encoding: chunked
0
X ← 后端按 CL=6 只读 "0
X...",剩下的"未完成",会等待 → 超时
→ 如果响应延迟 → TE.CL 存在
💡 这个方法特别妙:它不"利用"漏洞,只是"探测"漏洞——对一个卡住的连接,不会造成任何伤害。这是负责任测试的标准做法。
21.3.2 技巧二:用 Burp 的扩展
Burp 有专门的"HTTP Request Smuggler"扩展(官方 BApp),能自动:
- 检测走私类型(CL.TE / TE.CL / TE.TE)
- 尝试"确认"利用
- 提供现成的攻击 payload
💡 实战中,这是最省力的方式。 但理解原理仍然必要——否则你不知道扩展在做什么,也不知道结果意味着什么。
21.3.3 技巧三:观察"请求被"错位"的迹象
如果你能观察到"响应错位"(响应和请求对不上),说明存在走私。
用一个"能区分身份"的请求(比如带不同 Cookie),观察:
□ 我发的请求 A,收到的却是"本该发给 B 的"响应
□ 我的请求让"下一个请求"出错了
□ 响应里出现了"我没发过的内容"
21.3.4 检测的注意事项
⚠️ 请求走私测试**可能影响其他真实用户**(因为污染的是共享连接)
⚠️ 只在授权测试、或隔离环境中进行
⚠️ 用"时间延迟"这种"无害探测"优先,避免真的去污染别人
⚠️ 测试前和客户充分沟通(这是"高危操作")
🔥 这是全书"最需要谨慎"的一类测试。 因为它影响的是"其他用户",不是你自己的会话。
21.4 请求走私的利用方式
走私成功了,能干什么? 这是最有意思的部分。
21.4.1 利用一:绕过前端访问控制
前端做鉴权(比如拦截 /admin),后端不做。 走私一个"指向 /admin 的请求",让后端以为它来自已授权的上下文。
前端检查:/admin → 拒绝
但如果把 "GET /admin" 走私进后端的连接缓冲区,
后端的下一个请求处理时,就可能"顺手"处理了它
→ 绕过前端的访问控制
21.4.2 利用二:捕获其他用户的请求
这是最"恐怖"的利用。 通过走私,可以让"下一个用户的请求"被拼接/变成别的请求,从而:
□ 窃取其他用户的请求(包括他们的 Cookie、表单数据、Token)
□ 让其他用户执行"攻击者构造的操作"(在受害者身份下)
□ 把受害者的请求"转发到攻击者控制的地址"
典型场景:把走私的"半个请求"构造成"一个会捕获下一个请求内容的请求"。
攻击者走私出一个不完整的 POST 请求(缺了 body 的结尾)
→ 前端转发后,后端的"下一个请求"(可能是受害者的)的**开头部分**,
会被当成"上一个请求的 body"
→ 攻击者就"捕获"了受害者请求的一部分
21.4.3 利用三:反射型 XSS / 重定向
□ 通过走私,让"下一个用户的请求"被处理成"我构造的恶意请求"
□ 如果该请求会"回显",就可能触发 XSS
□ 也可能触发开放重定向(把用户导向攻击者)
21.4.4 利用四:缓存投毒(配合)
请求走私 + 缓存会"放大"危害:如果被投毒的响应被缓存,所有访问该页面的用户都会中招。
攻击者走私一个请求,使得"后端返回的某个响应"被 CDN 缓存
→ 之后所有访问该 URL 的用户,都拿到"被投毒"的响应
21.4.5 利用方式汇总
| 利用 | 说明 | 危害 |
|---|---|---|
| 绕过前端访问控制 | 走私 /admin 请求 | 越权 |
| 捕获用户请求 | 让别人的请求被自己"截取" | 数据泄露、会话劫持 |
| 反射型 XSS | 让下一个请求变成恶意请求 | XSS |
| 缓存投毒 | 配合缓存放大影响 | 影响所有用户 |
| 拒绝服务 | 让连接错乱、应用卡死 | DoS |
21.5 其他协议层漏洞
请求走私是"协议层"的代表,但协议层还有几个"亲戚"。
21.5.1 Host 头攻击
回顾第 2、3 章。应用如果信任请求里的 Host 头,就有问题:
POST /forgot-password HTTP/1.1
Host: attacker.com ← 篡改
利用场景:
- 密码重置链接被劫持(邮件里的链接指向攻击者域名)
- 缓存投毒(Host 参与缓存键)
- 虚拟主机路由绕过(用别的 Host 访问内网)
修复:服务端维护可信 Host 白名单,不信任请求头。
21.5.2 缓存投毒(Web Cache Poisoning)
原理:如果"缓存键"(Cache Key)只包含 URL,但页面的内容依赖于"不在缓存键里"的输入(如某些请求头),攻击者就能"投毒"。
缓存键 = URL
但页面内容 = f(URL, X-Forwarded-Host)
攻击者发请求:X-Forwarded-Host: attacker.com
→ 后端生成了一个"包含 attacker.com 的响应"(比如脚本 src)
→ CDN 把这个响应"按 URL 缓存"
→ ★ 后续所有访问该 URL 的用户,都拿到这个被投毒的页面 ★
关键要素:
□ 有一个"影响响应内容"但"不在缓存键里"的输入(unkeyed input)
□ 响应会被缓存
□ 缓存对"所有用户"生效
修复:
✅ 缓存键包含"所有影响内容的输入"
✅ 或者:不缓存依赖未键化输入的响应
✅ 规范 Host / X-Forwarded-Host 的处理
✅ 在缓存层之前,统一规范化请求
21.5.3 HTTP/2 降级走私
当"前端"用 HTTP/2、"后端"用 HTTP/1.1 时(很常见,因为 HTTP/2 主要在边缘),从前端到后端,协议被"降级"了。
HTTP/2 的头部是"二进制 + HPACK 压缩"的
降级到 HTTP/1.1 时,需要"重新序列化"成文本头部
如果"降级"过程中处理不当(比如某些头部没有被正确规范化),
就可能引入"解析差异" → 触发新的走私
💡 这是"新时代"的请求走私,也是"为什么请求走私"至今还活着的原因——协议在演进,但"多层解析不一致"这个问题没有消失。
21.5.4 协议层漏洞总览
| 漏洞 | 根因 |
|---|---|
| 请求走私 | CL/TE 解析不一致 |
| Host 头攻击 | 信任了 Host 头 |
| 缓存投毒 | "未键化输入"影响响应 |
| HTTP/2 降级走私 | 降级过程中的解析差异 |
🔥 它们的共性:
都是"系统由多层组成,各层对同一个东西的理解不一致"造成的。
21.6 修复:协议层的根本解
21.6.1 请求走私的修复
核心原则:让"前端"和"后端"对"请求边界"的理解完全一致。
【根本解:统一解析,消除歧义】
① 优先用 HTTP/2(端到端),避免"降级"
HTTP/2 用二进制帧,天然没有 CL/TE 歧义
② 如果必须用 HTTP/1.1:
✅ 让前后端"用同一个 HTTP 解析库"或"配置一致"
✅ 严格拒绝"同时带 CL 和 TE"的请求
✅ 严格拒绝"多个 CL""多个 TE"的请求
✅ 统一"TE 的值"(只接受 chunked,拒绝其他)
③ 具体配置示例:
Nginx:使用较新版本(已修复大量走私相关解析问题)
✅ 拒绝带下划线的头部(如 Transfer_Encoding)——某些组件会"误认"
✅ 在代理层"规范化"请求后再转发
21.6.2 一个"防走私"的配置示例(Apache)
# 拒绝同时带 Content-Length 和 Transfer-Encoding 的请求
SetEnvIf Request_Method "." reject_smuggling=0
RewriteEngine On
RewriteCond %{HTTP:Transfer-Encoding} !^$
RewriteCond %{HTTP:Content-Length} !^$
RewriteRule .* - [F]
💡 不同组件有不同的"防走私"配置,关键是:在"最外层"就消除歧义,别让"模糊的请求"进入内部。
21.6.3 Host 头攻击的修复
✅ 服务端维护"可信 Host 白名单",校验请求的 Host
✅ 生成"绝对链接"时,用配置里的域名,不用请求头
✅ 重定向时,校验目标在允许范围内(避免开放重定向)
21.6.4 缓存投毒的修复
✅ 缓存键包含"所有影响响应内容的输入"
✅ 对不缓存的内容显式设置 Cache-Control: no-store
✅ 在应用层"统一规范化"请求(如统一 Host 处理)
✅ 定期审计"未键化输入"
21.6.5 修复对照表
| 漏洞 | 根本解 |
|---|---|
| 请求走私 | 统一解析 / 用 HTTP/2 / 拒绝歧义请求 |
| Host 头攻击 | 可信 Host 白名单 / 用配置里的域名生成链接 |
| 缓存投毒 | 缓存键包含所有输入 / 显式 no-store |
| HTTP/2 降级走私 | 使用已修复的组件 / 端到端 HTTP/2 |
21.7 动手任务
任务 1:Academy 的请求走私实验
PortSwigger Academy → HTTP request smuggling(这是本章最好的练习场):
□ HTTP/1.1 部分:
- Exploiting HTTP request smuggling to bypass front-end security controls, CL.TE
- Exploiting HTTP request smuggling to bypass front-end security controls, TE.CL
- Exploiting HTTP request smuggling to capture other users' requests
- Exploiting HTTP request smuggling to perform web cache poisoning
- HTTP/2 相关实验(进阶)
💡 Academy 的请求走私实验是"隔离环境"——你可以放心地"污染",不会影响任何人。这是理解请求走私的最佳方式。
任务 2:本地搭一个"容易走私"的环境
① 用 Docker 搭一个 Nginx + 后端应用的组合
② 故意用"版本较旧"的 Nginx(或被配置成"优先 TE")
③ 用 Burp 的 HTTP Request Smuggler 扩展探测
④ 观察不同配置下检测结果的差异
⑤ ★ 只在本地环境做
任务 3:理解 Host 头攻击
□ 在一个靶场上,找一个"用 Host 头生成链接"的功能
□ 篡改 Host,看生成的链接是否改变
□ 体会"密码重置链接被劫持"的危害
任务 4:观察一次缓存投毒
□ 找一个"有 CDN 缓存"的靶场(或自己搭)
□ 找一个"影响响应内容但不在缓存键里"的输入
□ 尝试"投毒",然后访问"干净的 URL",看是否拿到被污染的响应
□ ★ 只在你自己的环境
任务 5:画一张"多层解析"的图
拿你熟悉的一个系统,画出"请求会经过哪些层":
浏览器 → CDN → WAF → Nginx → 应用 → 框架
在每一层的"边界"上,标注:
□ 这一层"理解"了什么?
□ 它和下一层"理解得一样"吗?
□ 哪里可能"不一致"?
这张图,就是你找"解析差异漏洞"的地图。
21.8 本章小结
核心结论
- 请求走私的本质:前端和后端对"请求的边界"理解不一致,导致攻击者的"半截请求"被走私,污染后续请求。
- 根源:
Content-Length与Transfer-Encoding同时出现时的解析歧义。 - 四种类型:CL.TE(前端认长度、后端认分块,最常见)、TE.CL、TE.TE(混淆)、CL.CL。
- 检测方法:时间延迟(最安全,只是"探测"不"利用")、Burp 的 HTTP Request Smuggler 扩展、观察"响应错位"。
- 危害极大:绕过前端访问控制、捕获其他用户请求、反射 XSS、缓存投毒。
- 它是"盲"的,且影响其他用户——测试必须格外谨慎(优先用无害探测)。
- 其他协议层漏洞:Host 头攻击、缓存投毒、HTTP/2 降级走私。
- 它们的共性是"多层解析不一致"——和上一章的"绕过"是同一个根。
- 修复根本解:统一解析、消除歧义(优先 HTTP/2 / 严格拒绝"同时带 CL 和 TE" / 拒绝重复头)。
- 缓存投毒的修复:缓存键包含所有影响内容的输入;Host 攻击用可信白名单。
随身口诀
走私的根:前端和后端,对"请求在哪结束"看法不同;CL.TE 最常见,时间延迟来探测;危害大到能"偷别人的请求",测试时要格外小心;修复就一句:统一解析、消除歧义——别让请求"模棱两可"。
21.9 进阶篇总结:从"点"到"面"
进阶篇到此结束。 回顾这四章,它们把漏洞篇的"点",连成了"面":
第 18 章 代码审计 → 从"黑盒猜"到"白盒看",看穿漏洞的成因
第 19 章 自动化 → 从"手工找"到"批量筛",放大自己的能力
第 20 章 绕过 → 从"背 payload"到"懂原理",掌握绕过的方法论
第 21 章 请求走私 → 从"应用层"到"协议层",看到解析差异的极致
🔥 进阶篇的一条主线:
漏洞篇教你"有哪些漏洞",进阶篇教你"为什么会有漏洞、怎么系统性地发现它们"。 而所有这一切,都指向同一个思想——"解析差异"和"根本解 vs 创可贴"。
21.10 预告:第五篇(实战篇)
从下一章开始,我们进入最后一篇:实战。
前面四篇,你学了思维、工具、漏洞、方法。但**"会做实验"和"能完成一次真实的测试"之间,还有一段距离。**
实战篇要补上的,是"怎么把这一切组织成一次专业的交付":
第 22 章 完整渗透测试流程 → 从接到任务到交付报告
第 23 章 找到你的第一个漏洞 → 从学习到真实产出
第 24 章 安全开发实践 → 攻防一体的落地
第 25 章 职业路径与成长 → 如何在这条路上走远
下一章:第 22 章 一次完整的渗透测试流程。 我们将把前面所有知识,串成一次"从 0 到 1"的真实项目。
📌 本章金句 "请求走私把'解析差异'这个思想推到了极致:一个请求怎么'结束',居然成了整个系统安全的命门。 它提醒我们——当你以为'请求就是请求'的时候,其实每一层都在用自己的方式'理解'它。安全,就藏在这些'理解'的缝隙里。"