第 20 章 绕过大全(WAF / 过滤 / 黑名单)

3 阅读15分钟

前面十几章,我们一直在讲"某种漏洞怎么打"。 但真实世界里,漏洞前面往往站着"防护"——WAF、黑名单、过滤器。 这一章不教"新漏洞",而是给你一套**"绕过"的通用方法论**。 因为你一旦理解了"绕过"的本质,前面每一章的技巧,你都能自己"长"出来。


20.0 开篇:绕过的本质,只有一句话

先看一个最经典的例子。

一个网站在前端做了 XSS 防护:

// 前端把 <script> 标签"删掉"
input = input.replace(/<script>/g, "");

攻击者输入:

<scr<script>ipt>alert(1)</script>

发生了什么? 前端的正则把中间的 <script> 删掉了一次,剩下的字符拼起来变成了:

<script>alert(1)</script>

防护"看到了"这段输入,也"处理"了它——但处理完之后,它偏偏变成了攻击者想要的样子。

🔥 绕过(Bypass)的本质:

防护系统"看到/理解"的东西,和"实际执行"的东西,不一致。

  • 防护在第 1 层(字符串过滤)看
  • 执行在第 2 层(浏览器/数据库/Shell)做
  • 只要这两层的"理解"不同,中间就有绕过空间。

所有的绕过,都是这一个原理的变体。


20.1 绕过的五种"通用武器"

不管面对什么防护,你手里其实只有五种基本武器。所有绕过,都是它们的组合。

20.1.1 武器一:编码 / 解码的不一致

同一个"字符",有许多种"写法"。

字符 <
  URL 编码:      %3c
  双重 URL 编码:  %253c
  HTML 实体:     &lt;   &#60;   &#x3c;
  Unicode:       \u003c
  十六进制:       0x3c
  八进制:         \074
  Base64:        PA==
  UTF-16/UTF-7:   不同字节序列

绕过原理过滤器只认某一种写法,而解析器认得更多种。

💡 这就是"编码"武器的全部:找"过滤器不认识、但解析器认识"的那种写法。

20.1.2 武器二:大小写 / 别名 / 等价替换

大小写:  <SCRIPT>  <ScRiPt>  SeLeCt
等价词:  AND == && , OR == || , = == LIKE
别名:    php == php5 == phtml == pht
函数别名:system == shell_exec == passthru == popen

绕过原理过滤器用"精确匹配",而执行器"大小写不敏感 / 有多个等价形式"。

20.1.3 武器三:多余字符 / 注释 / 空白

SQL 注释:   SELECT/**/FROM   UN/**/ION
空格替代:   /**/  %09  %0a  %0d  ()  +(URL 中)
XSS:        属性之间可以用 / 或空格
路径:       /./  //  /%2e/
HTML 注释:  <scr<!-- -->ipt>

绕过原理过滤器"逐字符匹配关键词",而解析器"遇到注释/空白会跳过"。

20.1.4 武器四:分裂 / 拼接

关键词拆开:  <scr<script>ipt>       (删一次后拼回)
             SEL/**/ECT
             \x3cscript\x3e
跨参数拼接:  参数 a + 参数 b 拼成 payload
分段传输:    HTTP chunked 把 payload 拆到多个包
环境变量:    用变量拼出命令

绕过原理过滤器"看单个片段"是安全的,但"拼起来"就成了 payload。

20.1.5 武器五:利用"解析顺序"的差异

□ 先检查后用(TOCTOU):检查时是一个值,用时是另一个值
□ 检查的是编码前,执行的是解码后(或反过来)
□ 检查的是"字符串",执行的是"解析后的语义"
□ 两个组件解析不一致(第 21 章请求走私的根)
□ 白名单检查 host,实际连接另一个(SSRF 的 @ 绕过)

这是"最高级"的一种绕过,因为它不依赖具体的编码技巧,而是利用了系统架构本身的缺陷

🔥 五种武器对应五句话:

编码     → "换个写法,你就不认识了"
大小写   → "你以为只有一种写法?"
注释空白 → "你逐字看,我跳跃看"
分裂拼接 → "你看片段,我看整体"
解析差异 → "你看到 A,我执行 B"

20.2 分场景绕过速查

下面是"按漏洞类型"的绕过速查表。 每一类,其实都是 20.1 那五种武器的应用。

20.2.1 SQL 注入绕过

【关键字被过滤】
SELECTSeLeCt、SEL/**/ECT、特殊注释形式包裹 SELECT
UNION   →  UN/**/ION、UnIoN
AND/OR&& / ||AND/**/、A/**/ND
空格被过滤 → /**/%09%0a、()、+
逗号被过滤 → JOIN 代替、SUBSTRING(x FROM 1 FOR 1)

【引号被过滤】
→ 用 0x 十六进制表示字符串:0x61646d696e
→ 用 CHAR() 函数拼字符
→ 数字型注入不需要引号

【等于号被过滤】
=LIKE、REGEXP、BETWEENIN>< 组合

【注释符被过滤】
--   →  #(MySQL)、/* */

组合示例

# 原始
' UNION SELECT username, password FROM users-- -

# 绕过(关键字拆分 + 注释)
' UN/**/ION SEL/**/ECT username,pass/**/word FROM users-- -

20.2.2 XSS 绕过

【script 标签被过滤】
→ 换标签:img、svg、bodyiframedetails、marquee
→ 大小写:ScRiPt
→ 双写:<scr<script>ipt>

【on 事件被过滤】
→ 换事件名:onerror、onload、onmouseover、onfocus、ontoggle
→ 换标签(不同标签有不同默认事件)

【alert 被过滤】
→ confirm / prompt / print
→ 拼写拆分、用其他方式弹窗

【空格被过滤】
→ <svg/onload=alert(1)>     用 / 代替空格
→ <img/src=x/onerror=alert(1)>

【括号被过滤】
→ 用模板字符串语法包裹 1
→ 用 throw 语句

20.2.3 命令注入绕过

【空格被过滤】
→ 变量 IFS、花括号展开 {cat,/etc/passwd}
→ Tab 字符(%09)

【关键字被过滤】
→ 变量拼接:a=cat;b=/etc/passwd;$a $b
→ 反斜杠:c\at
→ 单引号:c'a't
→ 编码:用 base64 解码后执行

【分隔符被过滤】
→ 分号、管道、&&、||、换行(%0a)换着试

【通配符 */
→ cat /etc/passw*
→ /???/???

示例

# 原始
; cat /etc/passwd

# 绕过空格和关键词
;c'a't /etc/passw*

20.2.4 文件上传绕过

【扩展名黑名单】
→ 冷门后缀:php5、phtml、pht、phar
→ 大小写:.PHP、.Php
→ 末尾点:.php.
→ 双写:.pphphp
→ ::$DATA(Windows)

【内容检测】
→ 图片马(真实图片头 + 藏代码)
→ 二次渲染缺陷
→ 竞争条件

【.htaccess】
→ 上传 .htaccess 改变解析规则
→ 上传 .user.ini(PHP)

20.2.5 路径穿越绕过

../          →  %2e%2e%2f、..%2f、%2e%2e/
过滤一次      →  ....//  、....反斜杠
绝对路径      →  /etc/passwd(不用 ..)
编码         →  双重编码 %252e%252e%252f

20.2.6 各场景速查汇总表

场景常见过滤绕过思路
SQL关键词大小写 / 注释 / 编码 / 等价
XSS标签、事件换标签 / 换事件 / 编码 / 双写
命令空格、分隔符IFS / 变量 / 引号 / 通配符
上传后缀、类型冷门后缀 / 图片马 / .htaccess
路径上跳符号编码 / 嵌套 / 绝对路径

20.3 WAF 绕过

WAF(Web 应用防火墙)是"最专业"的防护,但它的本质决定了它一定能被绕过。

20.3.1 WAF 的工作原理(理解它,才能绕过它)

正常的 WAF 流程:
  ① 请求到达 WAF
  ② WAF 按"规则"检查请求(正则匹配危险特征)
  ③ 没命中 → 转发给后端
  ④ 命中 → 拦截

WAF 的两个关键弱点:
  A. 它做的是"字符串/正则匹配" → 本质是黑名单 → 可编码绕过
  B. 它和"后端""两个不同的解析器" → 解析差异 = 绕过空间

🔥 WAF 的两大死穴:

① 它是黑名单(规则只能覆盖"已知的攻击模式") ② 它是一个独立的解析器(它的解析 ≠ 后端的解析)

20.3.2 WAF 的七种绕过手法

① 编码绕过 WAF 匹配明文,后端会解码。

%3cscript%3e            URL 编码
%253cscript%253e        双重编码
&#60;script&#62;         HTML 实体
\u003cscript\u003e       Unicode

② 大小写 / 注释绕过 WAF 的正则如果不带 /i,或者只匹配一种写法:

<ScRiPt>
UN/**/ION
SEL/**/ECT

③ 多余字符 / 空白

%09 %0a %0d          三种空白
<img/src=x/onerror=>  用斜杠

④ 参数污染(HPP) 同一个参数名传多次,WAF 取第一个,后端取最后一个:

?id=1&id=1 UNION SELECT ...
WAF 看到第一 个 id=1(安全),后端用了第二个

⑤ 分块传输(Chunked) 把 payload 拆成多个 chunk,WAF 可能"拼不起来":

Transfer-Encoding: chunked

4
UNIO
4
N SE
...

⑥ 利用"解析差异"(最有效) WAF 检查的是"某个字段",后端用的是"另一个字段":

□ WAF 只看 URL,后端读 body
□ WAF 只看 body,后端读 multipart 的 filename
□ WAF 看到的是 Host,后端读 X-Forwarded-Host
□ WAF 解析 Content-Length,后端解析 Transfer-Encoding(→ 请求走私)

⑦ 找 WAF 的"盲区"

□ WAF 可能不检查某些方法(PUT/DELETE)
□ 不检查某些 Content-Type(如 application/json)
□ 不检查某些路径(如静态资源)
□ 不检查 Cookie / 自定义头
□ 不检查超大请求(可能"放行"而非"解析"

20.3.3 先识别 WAF,再谈绕过

# wafw00f:识别 WAF
wafw00f https://target.com

# 也可以手工判断:
□ 响应头里有 WAF 的特征(Cloudflare、Akamai、ModSecurity 等)
□ 发个明显的攻击 payload,看拦截页面的样式
□ 对比不同 WAF 的"默认拦截页"

💡 知道"是哪种 WAF",才能查它"哪种绕过管用"。 不同 WAF 的规则和解析实现不一样。

20.3.4 一个绕过 WAF 的思路框架

① 先发"正常请求",确认它能否到达后端
② 发"明显的攻击 payload",确认它被拦了(触发拦截)
③ 逐步"变形":
   - 先试大小写
   - 再试插入注释/空白
   - 再试编码(一层层加)
   - 再试参数污染 / 分块
④ 每次变形,观察"是 '被拦' 还是 '到达了后端'"
⑤ 找到那个"WAF 放行、后端执行"的临界点

🔥 关键心态

不要"背 payload 列表",而要"理解 WAF 是怎么解析的,然后构造一个'它看不懂、后端看得懂'的请求"。


20.4 "绕过思维"的通用框架

前面讲了这么多技巧,这一节把它们抽象成一套"可以随身携带"的思维框架。

20.4.1 四步绕过法

面对任何防护,按这四步走:

【第 1 步:确认防护存在,并定位它】
  - 它拦了什么?在哪一层拦的?(WAF?中间件?业务代码?)
  - 它是"报错拦截"还是"静默过滤"?
  - 它是"精确匹配"还是"包含匹配"?

【第 2 步:判断"防护层"和"执行层"】
  - 防护"看到"的是什么?(字符串?某个字段?某个解析结果?)
  - 执行"用到"的是什么?(另一个字段?解码后的值?另一个解析器?)
  - 两者的差异在哪?

【第 3 步:构造"两层理解不一致"的输入】
  - 换编码(第 2 层认识,第 1 层不认识)
  - 换大小写 / 别名 / 等价值
  - 插入注释 / 空白(第 1 层逐字看,第 2 层跳过)
  - 分裂成片段(第 1 层看片段,第 2 层看整体)
  - 参数污染 / 分块(第 1 层取一个,第 2 层取另一个)
  - 利用字段差异(第 1 层看 A 字段,第 2 层用 B 字段)

【第 4 步:验证】
  - 每次只改"一个变量",观察行为变化
  - 找到"防护放行,但后端执行了"的那个点

20.4.2 一个具体的思维示例

目标:绕过 WAF 打 SQL 注入。

【定位】WAF 拦了 "UNION SELECT" 这个明文
【判断】WAF 匹配的是"URL 里的连续字符串"
        后端 MySQL 允许"注释拆分关键词"
【构造】UN/**/ION SEL/**/ECT
        WAF 看不到完整的 "UNION SELECT"(被注释拆开)
        但 MySQL 解析时,注释被忽略 → 等价于 "UNION SELECT"
【验证】发送 → 没被拦 → 返回了数据 → 成功

注意:整个过程,你没有"记住某个 payload",而是"推理出一个 payload"。

20.4.3 一张"绕过武器"对照表

防护的弱点对应的绕过武器
只认一种编码换编码(URL/HTML/Unicode/双编码)
精确匹配字符串大小写 / 别名 / 等价值
逐字符匹配插入注释 / 空白 / 分隔符
只看片段分裂 / 拼接 / 双写
只看某个字段参数污染 / 字段替换
用自己的解析器解析器不一致(最高级)
只看已知模式找"新花样"(自创等价形式)

20.5 从绕过看防御:为什么"黑名单"注定失败

理解绕过的最好方式,是从"防御者视角"看它。

20.5.1 黑名单 vs 白名单

【黑名单】
  规则:"禁止这些已知的坏东西"
  例:禁止 <script>、禁止 UNION、禁止 ../

  问题:
  □ 攻击者总能找到"不在名单里"的等价形式
  □ 名单需要"持续更新",永远滞后
  □ 攻击面是"无限的",名单是"有限的"
  → 必然存在绕过 ★

【白名单】
  规则:"只允许这些已知的好东西"
  例:只允许 [a-zA-Z0-9],只允许 jpg/png,只允许这几个 IP

  优势:
  □ 攻击面是"有限的",校验是"封闭的"
  □ 不在名单里的,一律拒绝
  □ 不需要"追着攻击者更新"
  → 从根本上更难绕过

🔥 一句话总结本书反复出现的主线:

黑名单是"堵已知的洞",白名单是"只开该开的门"。 前者永远追不上攻击者,后者天然安全。

20.5.2 为什么"过滤"总是能被绕过

过滤(黑名单)在同一条规则上,有无数种等价绕法。

你要过滤 <script>:
  它变成 <ScRiPt>      → 大小写
  它变成 <scr<script>ipt> → 双写
  它变成 <img onerror>   → 换标签
  它变成 &#60;script&#62; → 编码
  它变成 <svg/onload>    → 换语法
  …… 无穷无尽

而上文提到的根本解(输出编码、参数化、白名单),则"根本不给你变形的空间"。

20.5.3 一个"防御层次"的对照

防御方式类型能否被绕过
前端 JS 校验黑名单直接绕过
关键字过滤黑名单可绕过(编码/变形)
正则黑名单黑名单可绕过(总有没覆盖的写法)
WAF 规则黑名单可绕过(解析差异)
参数化查询结构隔离不可绕(数据无法变代码)
输出编码上下文隔离不可绕(数据无法变成标签)
白名单校验白名单不可绕(非法输入直接拒绝)
最小权限 / 网络隔离纵深防御绕过也拿不到更多

💡 评估一个防护"靠不靠谱",就一条标准:

它是在"猜攻击者会怎么打"(黑名单),还是在"定义什么是允许的"(白名单/结构隔离)?

20.5.4 给防御者的三条建议

① 【能用白名单,就别用黑名单】
   能限定"只允许数字",就别去"过滤非数字字符"

② 【在"结构层"防御,而不是"字符串层"】
   参数化查询 > 转义引号
   输出编码 > 过滤标签
   类型系统 > 字符串校验

③ 【纵深防御,不要指望单点】
   就算某层被绕过,下一层还能兜底
   例:XSS = 输出编码 + CSP + HttpOnly

20.6 动手任务

任务 1:在 DVWA 上"复现"各类绕过

□ XSS:从阻挡到绕过(大小写 → 换标签 → 双写)
□ SQL:从阻挡到绕过(注释 → 编码 → 等价)
□ 命令注入:从阻挡到绕过(空格 → 变量 → 引号)
□ 文件上传:从阻挡到绕过(后缀 → 类型 → htaccess)
每个都记录:防护是什么?我是怎么绕的?原理是哪种"武器"?

任务 2:Academy 的"绕过"实验

PortSwigger Academy 里有很多"绕过"主题的实验:
□ SQL injection 的 bypass filter 类实验
□ XSS 的各种 context 绕过
□ File upload 的 extension blacklist bypass
□ SSRF 的 blacklist / whitelist bypass
□ Broken authentication 的各种绕过
把它们当作"绕过思维"的训练场。

任务 3:观察一个真实 WAF(仅在你自己的靶场)

① 在本地搭一个 ModSecurity + OWASP CRS(或 ModSecurity Core Rule Set)
② 放在你的靶场前面
③ 发各种 payload,观察:
   - 哪些被拦了?
   - 拦截页面长什么样?
   - 换个编码能绕过吗?
④ 体会"WAF 的规则 vs 真实的请求"之间的差距

任务 4:把"绕过"整理成一张表

20.2 里的所有绕过手法,整理成你自己的"绕过速查表":
  场景 | 常见防护 | 绕过手法 | 原理(哪种武器)
作为你以后测试时的"弹药库"

任务 5:从"防御者"视角重写一个功能

选一个"用黑名单过滤"的功能,把它改写成"用白名单验证"的版本。
体会:为什么白名单"不需要不断更新"

20.7 本章小结

核心结论

  1. 绕过的本质只有一句话防护"看到/理解"的,与"实际执行"的,不一致。
  2. 五种通用武器:编码、大小写/别名、注释/空白、分裂/拼接、解析顺序差异
  3. 每一种漏洞的绕过,都是这五种武器的应用(SQL / XSS / 命令 / 上传 / 路径)。
  4. WAF 的本质是黑名单,有两个死穴:① 只覆盖已知模式 ② 它是独立的解析器(解析差异 = 绕过)。
  5. WAF 七种绕过:编码、大小写/注释、多余字符、参数污染、分块传输、解析差异(最有效)、找盲区。
  6. 先识别 WAF(wafw00f),再选绕过策略。
  7. 绕过思维四步法:定位防护 → 判断"防护层 vs 执行层"的差异 → 构造"不一致"的输入 → 单变量验证。
  8. 不要背 payload,要会"推理" payload——因为真实系统千变万化。
  9. 黑名单注定失败,白名单天然安全:过滤是在"猜攻击者怎么打",白名单是"定义什么被允许"。
  10. 防御的正确方向白名单 > 黑名单;结构层防御 > 字符串层过滤;纵深防御 > 单点防护。

随身口诀

绕过的本质:防护看到的 != 实际执行的; 五把武器:编码、大小写、注释、分裂、解析差异; WAF 是黑名单,也是另一个解析器——差异就是缺口; 别再背 payload,理解"两层不一致",你就能自己推出来。


20.8 预告:下一章我们去哪

本章讲的"解析差异",在 HTTP 协议层有一个"登峰造极"的应用——请求走私(Request Smuggling)

它的威力大到什么程度?一个攻击者的请求,可以"污染"另一个无辜用户的请求,让受害者不知不觉地执行攻击者的操作。

它的门槛也高:需要真正理解 Content-Length / Transfer-Encoding 的解析规则,以及"前端"和"后端"是怎么"各理解一半"的。

第 21 章:请求走私与协议层漏洞。 进阶篇的最后一章。


📌 本章金句 "所有绕过,都是同一个故事的不同版本:系统由很多'层'组成,每一层都在'理解'你发来的数据——而它们理解得不一样。 攻击者的艺术,就是在这些'理解差异'里找到一条路。防御者的艺术,则是让所有层'对同一个输入达成一致',或者干脆不给输入决定权。"