资讯动态

WAF编码绕过原理与防御:从双重编码到宽字节的实战拆解

发布时间:2026/9/25 4:41:31 来源:尧图企业网站定制
做Web安全这些年我经常遇到一个场面WAF规则写得看起来挺全SQL注入的常规Payload往里面一打直接被拦页面弹了个403现场的人都很满意。可过不了多久有人换了一种编码方式同样的注入逻辑又静悄悄穿过去了。不是WAF坏了也不是规则没写全而是WAF绕过里最典型的一类——编码绕过特性在起作用。这篇文章我从防御研究的角度把编码绕过这件事彻底拆开WAF为什么会被编码手法绕开、常见的编码绕过形式有哪些、怎么在本地环境里自己复现一次、最后应该怎么把这类问题堵上。不管你是网站运维、安全工程师还是正在做安全自查的后端开发这篇文章都能直接拿来用。1. 编码绕过的原理WAF的检测漏洞到底出在哪1.1 检测链路拆解WAF看到的内容与后端看到的不一样很多人以为WAF就是“拿请求里的字符串去和规则库比对”命中就拦没命中就放。真实情况比这个复杂得多。WAF要做的工作通常分三步解析HTTP请求拿到参数、对参数做一层或多层解码、把解码后的内容丢进规则引擎做正则匹配。规则库本质上是一堆字符串特征和正则表达式比如常见的单引号、or、union、select这些关键字。问题就在这里请求从进入WAF到最终被后端业务代码处理中间往往要经过多个解析层。网关解一次码应用框架解一次码业务代码可能又解一次。每一层的解码规则、解码次数、字符集都可能不一样。打个比方小区门口的保安负责核对快递单号他看到的单号是打印出来的一串字符。可包裹到了收件人手里收件人还要撕开外层包装看到的是里面的真实物品。保安对着外层单号核对收件人看的是内层实物这中间只要存在“处理方式差异”就会产生判断不一致。WAF也是同样的处境。它看到的请求和后端最终解析出来的请求如果内容不一致规则匹配就形同虚设。编码绕过利用的正是这个“解析差异”。1.2 规范化难题为什么编码绕过防不胜防同一个字节序列在不同解析规则下可能代表完全不同的含义。/可以被写成%2f空格可以是%20、或者%09单引号可以被写成%27、%c0%a7、%u0027、#x27;等等。安全里有个词叫规范化Canonicalization意思是把各种不同表示形式的输入统一成标准形态。理想情况下WAF应该把请求里的所有编码都还原成最底层的样子再去做特征匹配。但现实是解码不是解一层就结束了解多了可能误伤正常业务解少了就可能留下绕过窗口。所以很多WAF产品为了稳定和性能只会做有限次数的解码或者只对部分字段做解码。这种“不完整的规范化”就是编码绕过的温床。攻击者不需要对抗WAF的规则库只需要找到一套“WAF解不出来但后端能解出来”的编码组合就可以轻松穿过检测。搞清楚这一点再看具体手法就不难了。提示下面所有手法演示目的都是帮助你理解WAF检测的盲区从而做好自身防护。请不要对任何你没有授权的系统做类似测试这既是技术边界也是底线。2. 从URL双重编码到宽字节四类高频编码绕过手法拆解2.1 URL双重编码最容易被新手复现的绕过姿势URL编码大家都熟%27代表单引号%20代表空格。很多WAF在做规则匹配前会对URL参数做一次解码。注意很多情况下它只做“一次”。那如果攻击者把一个Payload编码两次呢比如1 OR 11--先做一次URL编码得到1%27%20OR%2011--再做一次编码得到1%2527%2520%254F%2552%25201%253D1--。WAF拿到后先解一层得到的是1%27%20%4F%52%201%3D1--里面既没有明文单引号也没有明文or关键字。规则匹配自然放行。但后端如果因为某些原因又解了一次比如Java应用里有人在Filter中调用了URLDecoder.decode()或者PHP业务代码拿到参数后又做了一次urldecode那么最终拼进SQL的字符串就是原原本本的1 OR 11--也就是所谓的万能密码载荷。这个过程中WAF一次解码、后端一次解码两者看到的请求内容完全不同绕过就这么成立了。这种双重解码的情况在实际系统里非常普遍。最常见的是“WAF/网关解一次应用框架自动解一次业务代码又显式解一次”的叠加。排查的时候你可以逐层打印解码结果很快能看到是哪一层造成了差异。2.2 宽字节与Unicode字符集不一致带来的突破口宽字节注入算是编码绕过里历史最悠久的一种核心原因是字符集不一致。经典场景是这样的PHP站点使用GBK编码连接MySQL后端对输入的单引号做了转义比如addslashes会把变成\对应的字节就是%5c%27。如果攻击者提交%bf%27转义后就成了%bf%5c%27。在GBK字符集下%bf%5c会被解析成一个合法的汉字字符也就是说那个转义用的反斜杠被“吸收”进了汉字里后面的单引号就不再有转义状态直接逃逸出来。WAF这边呢如果它按UTF-8来解析请求%bf后面跟%27并不是一个合法的UTF-8序列很多实现会直接跳过或者替换成占位符规则匹配不到单引号请求就放过去了。后端却因为使用GBK连接最终还原出了注入效果。类似的字符集乱象还体现在全角字符和Unicode兼容字符上。比如全角单引号UFF07在某些数据库或中间件的字符转换过程中会被归一化成ASCII单引号WAF如果不做Unicode规范化就拦不到这种形态。这类问题在存量老项目里特别多排查时如果发现普通规则明明有效、但宽字节变体能穿过先去看WAF、应用、数据库三方的字符集配置是否统一。2.3 十六进制与进制编码换一种写法绕过关键字匹配数据库的SQL语法天然支持多种字面量表示方式这也是编码绕过得以存在的另一块土壤。最典型的是MySQL里的十六进制字面量0x61646D696E其实就等价于字符串admin。举个例子如果WAF规则里写了“拦截参数中出现的admin关键字”攻击者把请求改成SELECT * FROM users WHERE username 0x61646D696E规则匹配的是admin这个明文而0x61646D696E显然不包含admin于是放行。后端MySQL执行时会把十六进制字面量解析成真正的字符串admin查询照样执行。除了十六进制还有CHAR(97,100,109,105,110)这种函数拼接方式也能还原出admin。这类方法单独用往往只能绕过“针对字符串字面量”的规则对select、union这类结构性关键字不见得有效。所以实际对抗中攻击者通常会把十六进制、大小写混写、注释符组合到一起使用增加WAF规则匹配的难度。这背后的教训是WAF规则如果只盯着某个“敏感词”做字面量匹配很容易被换一种进制就绕过去。防御方的正确思路应该是尽量匹配“语义结构”而不是“单词本身”。2.4 HTML实体编码前端反噬场景下的XSS绕过编码绕过不只是SQL注入的专利在XSS场景里同样常见。HTML实体编码就是典型#x27;表示单引号#x3C;表示小于号#x3E;表示大于号。假设攻击者想提交一段scriptalert(1)/scriptWAF如果只检查原始请求里的script字样直接把这段转成#x3C;script#x3E;alert(1)#x3C;/script#x3E;WAF查不到script就放行了。但浏览器在渲染HTML页面时会自动把实体解码回原样前端脚本也就被执行了。这就像寄快递时把一件物品拆成零件分开寄出仓库的自动分拣机只认识完整物品的外形认不出这些组合起来的零件。这类问题在富文本编辑器、评论区、用户昵称展示等功能点都比较容易出现。防御时后端输出必须做HTML编码WAF层也应当在匹配规则前先做一轮HTML实体解码候选否则规则库里写再多XSS特征也有遗漏。2.5 不止SQL注入XSS、RCE、鉴权场景里的编码绕过编码绕过的适用范围比很多人想象的要广。SQL注入可以用URL编码、宽字节、十六进制绕过XSS可以用HTML实体、URL编码、JavaScript的String.fromCharCode绕过命令执行场景里cat被拦了可以换成cat、c\at这类变形甚至把整段命令做Base64编码再交给后端执行。鉴权场景也有类似问题。一些接口的Authorization头里会携带Base64编码的账号密码信息如果WAF对这个字段不做解码检查攻击者把admin编码成YWRtaW4放进去基于账号关键词的黑名单规则就废了。这类情况本质上是“接口先编码后使用”和“WAF不跟着解码”之间的矛盾。另外像CSRF绕过、组件默认密钥导致的认证绕过之前Nacos曝出过的默认密钥问题就是典型成因和编码绕过并不相同。排查时一定要先把问题分类不要看到绕过就往编码上靠。3. 仿WAF后端本地完整复现一次编码绕过3.1 实验环境准备自己搭一个有“解析差异”的靶子为了把原理讲透我在本地用Python搭了一个最小化实验环境。它不依赖任何商业WAF也没有任何真实目标纯粹用于理解两层解码差异的机制。环境只需要一个Python脚本抽象掉HTTP细节模拟两层处理逻辑第一层是WAF它对参数做一次URL解码然后和规则库匹配第二层是后端业务它对同一个参数再做一次URL解码然后拼接SQL。规则我设置成检测单引号、双引号以及or、union、select这几个关键字。from urllib.parse import unquote import re RULE re.compile(r[\]|(?:or|union|select), re.IGNORECASE) def process(raw_param): # 第1层模拟WAF只做一次URL解码后匹配规则 decoded_once unquote(raw_param) if RULE.search(decoded_once): return blocked by sim-waf # 第2层模拟后端业务再做一次URL解码后拼接SQL decoded_twice unquote(decoded_once) sql fSELECT * FROM items WHERE id {decoded_twice} LIMIT 1 return fexec sql: {sql}这个逻辑很简单但已经能完整表达WAF绕过里最核心的“两层解码差异”问题。你在本地跑一下就能直观看到同一个参数在不同层的眼里长什么样。3.2 三步构造Payload并确认执行结果我准备了三个测试用例分别对应普通Payload、单层URL编码、双重URL编码cases [ 1 OR 11--, # 普通Payload 1%27%20OR%2011--, # 单层URL编码 1%2527%2520%254F%2552%25201%253D1--, # 双重URL编码 ] for c in cases: print(c, , process(c))跑完之后结果非常典型。第一个用例进入WAF层解码后就是1 OR 11--单引号和or关键字命中规则被拦掉。第二个用例是单层编码WAF解码一次后和第一个用例完全相同照样被拦。第三个用例是双重编码WAF解一次后得到的是1%27%20%4F%52%201%3D1--这里没有明文单引号也没有明文or字符串因为字母O和R都被编码成了%4F和%52规则匹配直接放行。请求到了后端这一层再做一次URL解码才还原出1 OR 11--SQL最终被拼接并执行。这个实验最有价值的地方在于它让你清楚看到“WAF层是否拦截”和“后端是否执行”其实是两件事。WAF放行不等于万事大吉只有后端真正执行了攻击者想要的逻辑才算绕过了完整的防护链。注意所有测试都在本地独立环境进行。对线上系统、第三方站点做任何绕过测试都可能面临法律风险这是绝对不能越过的红线。3.3 判定标准与日志验证怎样才算真正绕过判断一个绕过手法是否成立不能只看WAF有没有放行还要确认后端的执行链路是否真的被影响。我一般按三步走第一步看WAF日志确认请求被放行。第二步看业务日志确认后端代码确实拿到的参数和WAF看到的不一样最终拼接或处理的结果符合预期。第三步看数据层反馈在实验环境里可以打印最终SQL在生产排查时则通过数据库审计日志、慢查询日志来确认是否有异常操作发生。这个“三层确认法”在真实排查里也很实用。很多团队在WAF放行后就慌了其实应该冷静地把WAF日志、业务日志、数据库日志拉出来对比定位问题到底出在哪一层。如果三层日志都能对上号那这个绕过就是真实存在的如果只有WAF放行但后端并没有按预期解析那可能只是误报或者无害请求。4. 知道原理之后怎么防五个让编码绕过失效的动作4.1 在入口层做统一解码与字符集收敛编码绕过本质上是“各层解码策略不一致”那最直接的解决思路就是统一规则。入口网关或WAF层明确解码策略规定对哪些字段做解码、做几次解码、按什么字符集解码然后把业务侧、数据库连接侧全部收敛到同一套标准。实际配置时数据库连接指定utf8mb4字符集应用框架强制UTF-8WAF侧也按UTF-8解析。这样宽字节这类依赖GBK和UTF-8差异的绕过基本就失去了土壤。很多老项目改起来有成本但只要把“入口统一解码全链路UTF-8”作为一个明确的安全基线新上线系统从一开始就按这个标准做收效会非常明显。4.2 规则匹配前先完成规范化再谈特征检测WAF的规则引擎不能直接拿原始请求去匹配正则应该在匹配前先完成系统化的预处理。URL解码、HTML实体解码、Unicode规范化、Base64解码都应当作为可配置的预处理步骤按业务场景有选择的执行。当然解码不能无限递归解码深度必须设上限否则业务里的合法%字符也会被反复解码导致页面功能异常。这里我建议设成“有限深度解码固定字符集归一化”既能覆盖绝大多数编码绕过又不会误伤正常业务。对于有能力和资源的团队可以进一步考虑引入语义解析方案。比如用SQL解析器对待检测内容做词法分析而不是停留在正则匹配。语义解析可以直接识别出“这是一个拼接的SQL片段”十六进制也好、编码变形也好最终都会被还原成AST结构检测的准确性会高一个量级。4.3 业务代码坚持参数化查询不依赖WAF兜底这一点我必须放在很重要位置无论WAF规则写得多完善都不如业务代码自身健壮。参数化查询预编译语句是阻断SQL注入最根本的手段没有之一。PHP的PDO可以这样写$stmt $pdo-prepare(SELECT * FROM items WHERE id ? LIMIT 1); $stmt-execute([$id]);Java用PreparedStatementPython的SQLAlchemy和sqlite3也都支持占位符绑定。只要用了参数化查询前面提到的URL双重编码、宽字节、十六进制这些绕动手法全部失去意义。因为用户输入不再作为SQL语句的一部分参与解析只是作为参数值被传递。需要留意的是参数化查询只能解决“值”注入的问题。如果业务里涉及动态表名、动态列名、ORDER BY排序字段这些不能用占位符必须用白名单机制显式校验这部分的防护责任主要在应用代码里。4.4 把编码绕过纳入自动化自测回归安全防御是持续过程而定期自测是发现规则盲区最高效的方式。我建议维护一套“编码绕过用例集”至少包含下面这些类别万能密码基础形态 OR 11--URL单层与双重编码变体宽字节组合%bf%27十六进制关键字0x61646D696EHTML实体编码#x27;Base64编码后的敏感数据大小写混写和注释符组合UnIoN/**/SeLeCt把这套用例集成到CI/CD流水线里每次业务迭代或WAF规则变更时自动跑一遍。规则加完不代表防护到位真正起作用的是每次绕过测试都被挡在外面的那一刻。我在实际项目里见过太多因为“加了一条规则就觉得安全”最终被打穿的案例自动化回归是避免这个问题最务实的办法。4.5 做纵深防御不把宝押在单点设备上WAF只是第一道门不能指望它解决所有问题。成熟的防御体系应该是分层配合入口WAF负责拦截明显恶意流量应用层用参数化查询和白名单校验守住业务逻辑运行时安全监控RASP类产品关注异常调用行为数据库侧用最小权限账号、禁用高危函数、开启审计日志兜底。举个例子数据库账号如果只给了指定库表的增删改查权限即便SQL注入真的发生攻击者能做的事也极其有限。让每一层都承担一部分防御责任而不是把所有希望押在一台WAF设备上即便某一天某种编码绕过突破了第一道门后面仍然有第二道、第三道防线兜住。5. 高频问题与现场排查经验5.1 编码绕过自查速查表现场排查编码绕过问题时我发现很多情况是有规律可循的。下面这张表是我在实际工作中常用的定位思路可以直接照抄。现象可能原因排查切入点普通规则有效但编码后的Payload能通过存在二次解码WAF解码深度不足逐层打印WAF和后端的解码结果同一Payload在GET请求能拦POST请求漏过WAF只解析了URL参数没规范解析BODY检查Content-Type字段与BODY解析策略含0x/CHAR的Payload漏过规则只匹配字符串字面量没还原进制在规则预处理中增加十六进制解算或引入SQL语义解析实体编码Payload在XSS场景漏过规则层未做HTML实体解码检查WAF的预处理栈是否包含实体解码候选宽字节组合漏过WAF、应用、数据库三方字符集不一致统一UTF-8配置并增加GBK宽字节特征检测这张表不一定能覆盖所有场景但能帮你快速缩小排查范围。编码绕过看着千变万化绝大多数最终都能回溯到“某一层没解码”或者“两层解出来的内容不一致”这两个根因上。5.2 一个宽字节绕过疑云的完整复盘之前我处理过这样一个自查场景客户自研的WAF规则里有单引号检测但测试人员发现在本地模拟环境里id1%bf%27这个请求居然能穿过去执行。团队一开始怀疑规则没生效我按经验做了排查。先看WAF侧的匹配过程%bf%27经过URL解码后按UTF-8校验不合法规则引擎直接跳过了解码后的内容等于这段数据没进入有效匹配流程。再看后端业务PHP使用GBK连接MySQL转义函数在单引号前加了反斜杠形成%bf%5c%27这样的字节序列。GBK把%bf%5c解读成一个合法汉字反斜杠被吸收单引号逃逸。两边一对问题根源就清晰了不是某条规则写错了而是WAF与后端在字符集理解上天然不一致。修复动作总共三步第一后端连接和存储统一改成UTF-8从根上消除GBK歧义第二业务代码改用PDO参数化查询彻底放弃字符串拼接SQL第三WAF侧增加对宽字节组合形态的特征检测作为暂时兜底。这个案例再次印证编码绕过的修复几乎不会靠单点规则完成基本都是字符集、代码写法、WAF策略三管齐下。5.3 复盘后的三个提醒第一测试别只测标准Payload。一个合格的防御测试用例集至少应该包含编码绕过、大小写混写、注释符组合、参数污染这几类变形。只测 OR 11这种小学生Payload等于没测。第二别忽略中间件和网关的解码行为。很多系统上线时没有网关后来加了CDN或API网关链路上多了理解释层原有WAF规则就可能出现新的盲区。架构一变更防御测试就要跟着跑一遍。第三WAF日志和业务日志最好能打通。排查编码绕过问题时最怕的就是WAF说“我放行了”业务却说“我没执行”两边日志对不上问题就无法定位。在日志体系里把请求ID串联起来能让排查效率提高很多。我个人在这些年的实际工作中最深的体会是不要把WAF当成一道焊死的铁门它更像是一个需要持续调试的安检系统。编码绕过的招式再多本质都是“解析不一致”。与其穷举每一种编码组合不如把规范化、参数化查询、最小权限这些基本功做扎实。到那时候你会发现所谓编码绕过不过就是测试用例集里普普通通的一行数据而已。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑