☰
WAF编码绕过攻防实战:从解析差异到规则设计
2026/10/15 11:41:31 网站建设 项目流程

1. 从一个真实的绕过案例说起

去年帮一个朋友看他们刚上线的业务,日志里出现了一批很奇怪的请求。URL 里全是%25%32%37这种双重编码的字符,乍一看像是乱码,但把两层解码剥掉之后,赫然是一个完整的注入语句。他们的 WAF 规则明明配了拦截,为什么没触发?原因很简单:WAF 在检测时只做了一次 URL 解码,而后端应用做了两次。这个时间差,就是编码绕过最经典的利用点。

Payload 编码绕过,说白了就是攻击者把恶意内容用各种编码方式“化妆”,让 WAF 的规则匹配不到,但后端在真正处理请求时又会把它还原成可执行的内容。这中间涉及一个核心矛盾:WAF 看到的字符串和后端应用最终解析出来的字符串,往往不是同一个东西。谁对“最终形态”的理解更准确,谁就赢。

这篇文章我想把这件事讲透。不管你是刚接触 WAF 运维的新手,还是已经配了几百条规则的老手,我都会从编码绕过的底层逻辑讲起,把常见手法、检测思路、规则写法、排查技巧一层层拆开。看完你至少能做到两件事:第一,知道攻击者会用哪些编码花招;第二,知道怎么让自己的 WAF 规则堵住这些花招,而不是被一个%25就骗过去。

2. 编码绕过的本质:解析差异

2.1 为什么同一个请求会有两种“真相”

要理解编码绕过,先得接受一个事实:一个 HTTP 请求从客户端发出到后端业务逻辑处理,中间要经过很多层解析。每一层都可能对字符串做一次解码或转换。WAF 站在某一层看,后端站在另一层看,两边看到的“真相”就可能不一样。

举个最朴素的例子。攻击者想传union select,但 WAF 规则里写了union\s+select就拦。于是他传union%20select。如果 WAF 在检测前会做一次 URL 解码,那它看到的是union select,能拦;如果 WAF 不做解码,它看到的是union%20select,正则匹配不上,就放过去了。而后端框架通常会自动做 URL 解码,最终拿到的还是union select,照样执行。

这就是解析差异。WAF 的解码层数和后端的解码层数不一致,或者解码的时机、范围不一致,绕过就产生了。我常跟人说,配 WAF 规则本质上是在做一件事:让 WAF 的解析结果尽可能逼近后端的解析结果。逼近得越准,绕过空间越小。

2.2 编码绕过的三大类手法

实际遇到的编码绕过,基本可以归到三类里。

第一类是多层编码。就是刚才说的双重、三重 URL 编码。%27是单引号,%2527是%27再编码一次,后端解两次就还原成单引号。层数越多,WAF 越容易漏。

第二类是异构编码。同一个字符有多种表示方式。比如单引号可以是%27(URL 编码)、%u0027(某些场景下的 Unicode 编码)、'(HTML 实体)、\x27(十六进制转义)。WAF 如果只认%27,其他形式就漏了。

第三类是编码与语法结合。这类最阴险,因为它不只是编码问题,还利用了后端语言的语法特性。比如在某些 SQL 方言里,/**/可以当空格用,union/**/select和union select等价,但 WAF 如果只匹配空格就拦不住。再比如用+代替空格、用大小写混写UnIoN、用注释截断union#select等等。

注意:这三类手法经常组合使用。一个成熟的攻击 payload 可能是“双重 URL 编码 + 注释代替空格 + 大小写混淆”的叠加体。所以 WAF 规则不能只防单一手法,要考虑组合场景。

2.3 一个关键认知:解码不是越多越好

新手容易走极端,觉得“那我让 WAF 无限次解码不就行了”。这个思路有问题。无限解码会带来两个麻烦:一是性能开销,每个请求都反复解码,QPS 高了扛不住;二是误报,正常业务里也可能出现%25这种字符,你解到底可能把正常请求当成攻击。

正确的做法是对齐后端的解码行为。后端解几次,WAF 就解几次,不多不少。这就要求你去摸清楚自己业务框架的解码逻辑。比如某些框架对 query string 解一次,对 body 解一次,对 path 又用另一套规则。这些细节决定了你的 WAF 该怎么配。

3. 常见编码手法逐个拆解

3.1 URL 编码与它的多层变体

URL 编码是最基础的。规则很简单:一个字节用%加两位十六进制表示。'是%27,空格是%20,<是%3C。

多层编码就是在已经编码的字符串上再编码一次。%27里的%本身编码成%25,于是%27变成%2527。再编一次变成%252527。后端如果解两次,%2527就还原成%27,再解一次才是'。

这里有个容易踩的坑:不是所有后端都会解多层。有些框架只解一次,那你传%2527它拿到的就是%27这个字面字符串,不会变成单引号。所以攻击者要先探测目标解几次,再决定编几层。作为防守方,你要做的是确认自己后端解几次,然后让 WAF 解同样的次数。

我一般建议在 WAF 里对 URL 参数做最多两次解码,然后对解码后的结果做规则匹配。为什么是两次?因为绝大多数框架最多解两次,解三次的情况极少,而且解太多次误报率会上升。

3.2 Unicode 编码的坑

Unicode 编码有好几种写法,%u0027是其中一种(注意是%u不是%)。这种编码在 IIS 某些版本上会被自动解析,在别的服务器上可能不会。所以它是一把双刃剑:用对了能绕过,用错了后端不认,payload 就废了。

对 WAF 来说,要不要处理%uXXXX取决于你的后端环境。如果后端是 IIS 或者某些会解析%u的框架,那 WAF 必须把%u0027也解码成'再匹配。如果后端根本不认这种编码,那 WAF 处理它反而可能引入误报。

我的经验是:先确认后端认不认,再决定 WAF 处不处理。不要盲目把所有编码形式都加进解码流程,那样只会让规则越来越臃肿。

3.3 HTML 实体编码在 XSS 场景的威力

HTML 实体编码主要出现在 XSS 场景。<可以写成&lt;,>是&gt;,"是&quot;,'是&#39;或&#x27;。

关键在于:HTML 实体是在浏览器渲染时才解码的,服务器端往往不解码。所以如果 WAF 只检查服务器端看到的原始字符串,而攻击 payload 是&lt;script&gt;,WAF 看到的是&lt;script&gt;,匹配不到<script>,就放过了。但这个内容如果被原样输出到页面,浏览器会把它解码成<script>并执行。

这就是存储型 XSS 里编码绕过的典型路径。防守思路是:WAF 在检测 XSS 时,除了看原始字符串,还要对 HTML 实体做一次解码后再匹配。同时,输出编码(output encoding)才是防 XSS 的根本,WAF 只是补充。

3.4 十六进制与八进制转义

\x27是十六进制转义,\047是八进制转义。这些主要出现在后端语言层面,比如 PHP 的字符串里\x27就是单引号。攻击者如果在参数里传\x27,某些场景下后端会把它当转义字符处理。

这类编码的检测比较麻烦,因为\x开头的字符串在正常业务里也可能出现(比如某些 ID 生成规则)。我的做法是:对\x和\0开头的转义序列做解码后再匹配,但同时给这类规则加一个白名单,排除已知的正常业务模式。

3.5 编码与语法混淆的组合拳

单独一种编码往往不够,真正难防的是组合。举几个我见过的真实组合:

  • union/**/select:用注释代替空格
  • UnIoN SeLeCt:大小写混淆
  • union%0aselect:用换行符代替空格
  • union+select:用加号代替空格
  • /*!union*/ select:MySQL 特有的可执行注释

这些手法的共同点是:它们改变了 payload 的“字面形态”,但没有改变它的“语义”。WAF 如果只做字面匹配,就会被绕过。要防住,规则必须做“归一化”——把注释去掉、把大小写统一、把各种空白符统一成空格,然后再匹配。

4. WAF 侧的解码与归一化设计

4.1 解码流程该怎么设计

一个合理的 WAF 解码流程,我通常建议按这个顺序来:

  1. 先做 URL 解码,最多两次
  2. 再做 Unicode 解码(如果后端支持)
  3. 再做 HTML 实体解码(针对 XSS 检测)
  4. 最后做语法归一化(去注释、统一空白、统一大小写)

这个顺序不能乱。为什么先 URL 后 Unicode?因为 URL 编码是最外层的,攻击者通常先做 URL 编码,再做其他编码。如果你先解 Unicode,可能把%u0027里的%当成 URL 编码的一部分,解出错误结果。

每一步解码后都要做一次规则匹配,而不是全部解完再匹配。这样能覆盖“只编一层”和“编多层”两种情况。代价是性能开销增加,所以实际部署时要根据业务 QPS 做取舍。

4.2 归一化:把“化妆”的 payload 打回原形

归一化的核心思想是:不管攻击者怎么改写 payload 的字面形式,只要语义相同,归一化后就应该得到相同的结果。

具体做几件事:

  • 去掉 SQL 注释:/**/、--、#等
  • 统一空白符:把%0a、%0d、%09、+都统一成空格
  • 统一大小写:把 payload 转成小写再匹配
  • 去掉多余括号和引号:某些场景下((select))和select等价

归一化之后再做正则匹配,命中率会高很多。但归一化也有风险:过度归一化可能把正常请求也“归一”成攻击特征,导致误报。所以归一化规则要精细,不能一刀切。

4.3 规则写法的几个实战技巧

写 WAF 规则时,我有几个习惯。

第一,用“语义正则”而不是“字面正则”。比如匹配 SQL 注入,不要写union\s+select,而是写union[\s/**/+]+select,把常见的分隔符都覆盖进去。

第二,对关键字符做“存在性检测”而不是“精确匹配”。比如检测单引号,不要只匹配',而是匹配['"%27%2527\x27]这种集合。当然这要配合解码流程,否则会误报。

第三,给规则加“上下文约束”。单独一个select可能是正常业务词,但select后面跟着from再跟着where,就高度可疑。用上下文约束能大幅降低误报。

第四,定期用“绕过测试集”回归。我维护了一个包含几百个编码变体的测试 payload 集合,每次改规则都跑一遍,看有没有漏网的。这个习惯帮我堵住了不少盲区。

5. 实操:搭建一套能扛编码绕过的检测流程

5.1 环境与工具准备

要验证自己的 WAF 能不能扛编码绕过,你需要一个可控的测试环境。我一般这么搭:

  • 一台测试服务器,部署一个存在已知漏洞的靶场应用(比如某个老版本的 CMS)
  • 一个 WAF 实例,可以是开源的也可以是商业的,配置好规则
  • 一个 payload 生成脚本,能自动生成各种编码变体
  • 一个日志分析工具,用来对比 WAF 拦截记录和后端实际执行记录

payload 生成脚本是关键。我写过一个 Python 小工具,输入一个基础 payload,它能自动生成 URL 编码、双重编码、Unicode 编码、HTML 实体编码、大小写混淆、注释插入等几十种变体。用这个工具批量打靶场,就能快速发现 WAF 的盲区。

import urllib.parse def generate_variants(payload): variants = [] # 原始 variants.append(payload) # URL 编码 variants.append(urllib.parse.quote(payload)) # 双重 URL 编码 variants.append(urllib.parse.quote(urllib.parse.quote(payload))) # 大小写混淆 variants.append(''.join(c.upper() if i % 2 else c.lower() for i, c in enumerate(payload))) # 空格替换 variants.append(payload.replace(' ', '/**/')) variants.append(payload.replace(' ', '+')) variants.append(payload.replace(' ', '%0a')) return variants base = "union select 1,2,3" for v in generate_variants(base): print(v)

这个脚本很简单,但覆盖了最常见的几类变体。实际用的时候可以再扩展,加入 Unicode 编码、HTML 实体等。

5.2 关键参数的计算与选择

WAF 解码层数设几次,这个参数不是拍脑袋定的。我的计算方法是:

  1. 先看后端框架的解码逻辑。查文档或者直接抓包看后端收到的原始字符串。
  2. 统计正常业务请求里%字符的出现频率。如果频率很低,说明多层解码的误报风险小,可以多解一层。
  3. 压测 WAF 在不同解码层数下的性能。每多解一层,CPU 开销大概增加 5% 到 15%,具体看实现。

综合下来,我一般设两次 URL 解码。这个值在“覆盖大多数攻击”和“控制误报与性能”之间比较平衡。如果你的业务对安全要求极高,可以设三次,但要接受更高的误报率和性能开销。

5.3 完整检测流程的落地

把上面的东西串起来,一个完整的检测流程是这样的:

  1. 请求进入 WAF
  2. 提取 URL、Header、Body 中的参数
  3. 对每个参数做第一层 URL 解码,匹配规则
  4. 对第一层解码结果做第二层 URL 解码,匹配规则
  5. 对第二层解码结果做 Unicode 解码,匹配规则
  6. 对结果做 HTML 实体解码,匹配 XSS 规则
  7. 对结果做语法归一化,匹配 SQL 注入规则
  8. 任何一步命中,就拦截并记录

这个流程听起来繁琐,但实际实现时可以用“管道”模式,把各个解码器和匹配器串起来,代码结构很清晰。性能上,大部分请求在第一层或第二层就被放行了,只有可疑请求才会走完全流程。

提示:记录日志时一定要记录“原始 payload”和“解码后 payload”两个版本。排查问题时,这两个版本能帮你快速定位是解码环节出了问题,还是规则本身有问题。

6. 常见问题与排查技巧实录

6.1 为什么规则明明配了还是被绕过

这是被问得最多的问题。我总结了几种常见原因,做成速查表:

现象可能原因排查方法
规则匹配不到WAF 解码层数少于后端抓包对比 WAF 和后端看到的字符串
规则匹配不到攻击用了异构编码检查 payload 里有没有%u、&#、\x
规则匹配不到攻击用了语法混淆检查有没有注释、大小写、空白符替换
规则误报归一化过度检查归一化规则是否把正常字符也改了
规则误报上下文约束太松给规则加前后文限定
性能下降解码层数太多压测不同层数下的 QPS

排查时我有个习惯:先把 WAF 的检测日志和后端的访问日志对齐时间戳,找到同一个请求,对比两边看到的 payload。这个对比能直接暴露解析差异在哪里。

6.2 误报和漏报的平衡怎么找

误报和漏报是一对矛盾。解码层数多、归一化激进,漏报少但误报多;反过来则误报少但漏报多。

我的做法是分级处理。把规则分成三档:

  • 高危规则:命中直接拦截,比如明确的 SQL 注入特征
  • 中危规则:命中记录并告警,但不拦截,比如可疑的编码模式
  • 低危规则:只记录,用于后续分析

这样既能拦住大部分攻击,又不会因为误报影响正常业务。中危和低危规则的日志定期分析,如果发现某条规则长期只产生误报,就调整或下线;如果发现某条规则漏掉了真实攻击,就升级为高危。

6.3 几个我踩过的坑

第一个坑:以为所有后端都解两次 URL 编码。有次给一个业务配 WAF,按两次解码配的,结果那个业务的后端只解一次。攻击者传%2527,WAF 解两次得到'拦截了,但后端只解一次得到%27,根本不执行。这导致大量正常请求里带%25的被误拦。后来改成按后端实际行为配,问题才解决。

第二个坑:忽略了 Header 里的编码。很多人只检查 URL 和 Body,忘了 Cookie、Referer、User-Agent 这些 Header 也可能藏 payload。有次攻击就是把注入语句放在 Cookie 里,用双重编码绕过,因为 WAF 没检查 Cookie 的解码。

第三个坑:归一化把正常业务词改坏了。有次归一化规则里把--当注释去掉,结果业务里有个参数值正好是A--B这种格式,被改成了AB,导致规则误判。后来给归一化加了白名单,排除已知的正常模式。

第四个坑:性能没做压测。上线前没压测,结果高峰期 WAF 的 CPU 打满,请求延迟飙升。后来把解码层数从三次降到两次,又加了缓存,才稳住。这件事让我养成了“任何规则改动都要压测”的习惯。

6.4 持续对抗的思路

编码绕过不是一次配好就完事的。攻击手法在进化,新的编码方式、新的语法特性会不断出现。所以 WAF 规则需要持续维护。

我的做法是:

  • 每周看一次 WAF 拦截日志,找有没有“疑似绕过”的请求(比如解码后出现敏感词但没被拦的)
  • 每月更新一次 payload 测试集,加入新发现的变体
  • 每季度做一次完整的绕过测试,用自动化工具批量打
  • 关注安全社区的新披露,看到新的绕过手法就第一时间验证自己的 WAF 能不能防

这套流程坚持下来,WAF 的防护效果会越来越稳。反过来,如果配完就不管了,半年后基本就是个摆设。

7. 一些个人体会

做 WAF 这几年,我最大的感受是:编码绕过本质上是“解析权”的争夺。谁对请求的最终形态理解得更准确,谁就掌握主动。WAF 要做的不是“比攻击者聪明”,而是“比后端更懂后端”。你得清楚后端每一步怎么解析、怎么解码、怎么执行,然后让 WAF 的检测逻辑跟它对齐。

另一个体会是,没有万能的规则,只有持续的对抗。任何一条规则都有被绕过的可能,关键是建立一套发现绕过、分析绕过、修补规则的闭环。工具和规则是死的,人是活的。

最后分享一个小技巧:如果你不确定某个 payload 会不会绕过你的 WAF,最直接的办法是把它同时发给 WAF 和后端,对比两边的处理结果。WAF 放过了但后端执行了,就是绕过;WAF 拦了但后端本来也不会执行,就是误报。这个对比测试做多了,你对编码绕过的直觉会越来越准。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询