WAF这东西,圈里人对它的态度一直很分裂。有人把它当万能保险箱,规则一开就高枕无忧;有人觉得它就是个摆设,随便一个变形的注入就能打穿。我做了几年安全和运维相关的工作,大大小小的WAF也见了不少,说实话,这两种看法都不准确。WAF绕过,尤其是我今天要聊的规则层面注入绕过,本质上是两套"语义理解系统"之间的博弈——规则引擎试图通过模式匹配来识别坏人,而攻击者要做的只是改变一下表达方式,让同样的恶意逻辑穿上不同的衣服。这篇文章我不想讲那种上来就甩几个payload让你"背下来"的教程,而是想把从入门到理解整个绕过逻辑的过程拆开来讲,把"为什么能绕过"这个底层原因讲透,同时给出对应的规则加固建议。适合刚入门的安全学习者、运维工程师,以及所有想搞明白"我的WAF到底拦得住什么、拦不住什么"的人。
1. WAF 到底在拦什么:先搞懂规则引擎的工作逻辑
1.1 一次请求进来之后,WAF做了哪些事
你在前面加了一层WAF之后,用户发来的每一个HTTP请求都得先过它这一关。整个过程大致可以拆成四步:
- 解析请求:把HTTP方法、URL路径、Header、Cookie、Body参数等结构化拆开。这一步各家做得深浅不一,但基本都要做。
- 归一化:把解码后的内容统一成一种标准形态。比如把URL编码还原、把重复的斜杠去掉、把大小写做归一化处理。
- 规则匹配:拿归一化后的数据去跟规则库里的特征正则进行比对。只要有一处命中,就可以判定为攻击。
- 执行动作:拦截、记录日志、返回403,或者只告警不拦截(很多绕过测试其实是在这一步钻了空子)。
你可以把WAF想象成一个负责查证件的门卫。这个门卫手里有一张写满了"可疑特征"的黑名单,任何人的证件上只要出现了名单里的字眼,就被拦在外面。问题来了——这个门卫只看字面,不读语义。它并不理解你证件上那行字实际在说什么,它只知道"这几个字连在一起等于危险"。
这就是规则层面检测的根本逻辑:**它假设恶意行为一定会以某种固定模式出现。**但现实世界里的SQL注入语句,并不是只有一种写法。你写成union select是注入,写成UnIoN/**/SeLeCt还是注入,写成union all select照样是注入。门卫手里那张黑名单再长,也不可能穷举完同一个语义的所有字面形态。
1.2 一条注入检测规则到底长什么样
下面这条正则,代表了绝大多数WAF检测SQL注入的典型思路:
union[\s\S]*?select '\s*(or|and)\s*[\w\s]+=\s*['\w]+ information_schema[\s\S]*?table倒也不难理解,就是找"union"后面跟着"select"这种组合,找引号后面跟着or/and再跟个条件判断,找查表名时必备的information_schema。这些特征确实覆盖了大量最常见的注入场景。
但反过来想,如果你的业务代码里压根没有能把用户输入拼进SQL语句的地方,这些规则再强也就是摆设。反过来说,一个精心构造的注入请求,只要用法和规则库里枚举的特征"长得不一样",引擎就认不出来。规则的覆盖范围是有限的,而攻击者的表达可能性是无限的,这个不等式是规则层面绕过永远存在的根本原因,也是我今天想重点展开的核心矛盾。
2. 规则为什么会失效:检测缺陷的底层原因
2.1 黑名单思维的天花板:同一种语义有无数种写法
先做个简单的思维实验。假设你的规则库里有一条"等号检测规则",看到参数里出现=就触发告警,目的是拦住' or 1=1--这种万能密码。
那我问你,下边这些表达式在SQL里是不是同样成立?
' or 1 like 1-- ' or 2-'1'=1-- ' or 'a'='a'-- ' or 1 in (1)-- ' or 1 between 0 and 2--全都是。1=1可以写作1 like 1,可以写作2-'1'=1,可以写作'a'='a',甚至可以写作1 between 0 and 2。你要是把每一种等价写法都加到规则库里,规则库会无限膨胀,而SQL语言本身是图灵完备的,你可以一直构造新的等价表达。试图用穷举法去堵一个无限空间的攻击面,从数学上就已经输了。
真实里遇到过一个有意思的案例。某个系统的WAF拦了or 1=1,测试的人随手把它改成or 'a'='a',规则就放行了。原因很简单,规则库里只枚举了数字型的1=1,没有枚举字符串型的'a'='a'。你说这条规则没用吗?它能拦90%的脚本小子的自动扫描。但你说它可靠吗?只要是个稍微懂点SQL变形的人就能绕过。规则的作用是"提高攻击成本",而不是"杜绝攻击",这个概念一定要摆在前面。
2.2 编码与归一化的不对称性:看到的不等于执行的
更高级一点的绕过思路,是利用解析顺序的差异。
举个很经典的例子。一个请求到达WAF之后,WAF先做了一次URL解码,然后拿解码后的结果去匹配规则——如果规则没有命中,WAF就把这个请求转发给后端Web服务器。Web服务器接收到请求之后,也要做URL解码,然后把解码后的参数送进业务逻辑。到这里,两边做的似乎是一样的,没毛病。
但假如这个中间层不止一个呢?假如反向代理先解了一次,WAF再解一次,到达后端时又解了一次呢?每一次解码都会多还原一层编码。假如攻击者把payload做双重URL编码——%2527先被WAF解成%27,此时WAF看到的还是百分号开头的形态,规则库里如果只写了匹配单引号的规则',那它匹配不到。请求继续往下走,后端拿到%27再解一次,变成真的单引号',注入就发生在这一层。
这种"我看到的和你看到的不一样"的问题,专业上叫归一化不一致。WAF以为自己在检查用户真实输入,实际上它检查的只是"被WAF解码过一次的输入",而后端最终执行参数时,用的是"被所有中间层解码后的总和"。这中间差出来的一层解码,就是绕过可以利用的空间。不只是URL编码,Unicode编码、HTML实体编码、十六进制编码,原理都一样。对付这种绕过的核心思路是把解码流程规范化:所有请求只解一次、按同一顺序解,并且确保WAF看到的字符串和后端执行时看到的完全一致。
2.3 你拦的是"结果",不是"过程"
还有一类绕过的思路不是改变字符串长相,而是改变攻击位置。
很多注入规则只盯着参数值做检查。比如?id=1 and 1=1这个注入点藏在参数值里。但有些系统会把参数名本身也拼进查询语句里,比如一些老旧的排序功能:?order=id被拼成ORDER BY {order},如果把order的值改成if(1=1,id,name),注入就发生在参数名这个位置。不少规则只扫值不扫名,于是放行。
还有,Header里的注入、Cookie里的注入、Content-Type里的注入,这些位置的安全检测强度往往远低于参数体。不是没有规则覆盖,而是规则库的权重设计偏向于"最常见的攻击位置",非主流位置就容易被忽略。**你拦的是已经有规则覆盖的那些位置和形态,而不是攻击者真正发起攻击的位置和形态。**这也是规则层面绕过的另一种共性:规则库覆盖不全不等于攻击者找不到可以利用的位置。
3. 规则层面注入绕过的常见手法拆解
下面这部分,我会按"手法类别"来拆。每一项都不仅仅给例子,而是把背后的原理和对应的加固思路都讲清楚。再次强调一下:所有测试都必须在授权的靶场环境或自己有权限的系统上进行。
3.1 大小写、空白与注释符的排列组合
这是最简单也最容易被忽略的一类。很多规则库里的正则默认区分大小写,对空白符的定义又不够宽泛,于是:
union select --> UnIoN SeLeCt union select --> union/**/select union select --> union%0bselect union select --> union select第一眼看上去像是同一个东西,但在正则引擎看来是几种不同的字符串。如果规则写的是union\s+select,那union/**/select就完全不在匹配视野里;如果规则写的是union select(中间一个空格),那union select(两个空格)也能溜过去。
这块儿的加固思路非常直接:所有检测特征统一走"忽略大小写 + 剥离注释 + 压缩空白"的预处理,把union/**/select先还原成union select再去匹配。我们实测下来,这一步能拦掉大量低水平变形。很多厂商的基础规则其实已经做了,但自建规则的人最容易踩这个坑——自己加规则的时候贪图省事,直接抄了一个区分大小写的正则就往上挂。
3.2 等价函数与运算符替代:绕的不是规则,是"关键词"
注入特征并不只有union select这一种。很多WAF的注入规则会重点盯几个"高危关键词":
information_schema(查表结构必备)concat/group_concat(拼接数据)substr/mid(截取字符)=/>/<(条件判断)sleep/benchmark(时间盲注)
攻击者的思路就四个字:换一个能实现同样效果的词。
| 规则盯防的关键词 | 绕过替代方案 | 效果说明 |
|---|---|---|
information_schema.tables | mysql.innodb_table_stats | 新版MySQL中可以直接从存储引擎统计表里拿表名,不需要碰information_schema |
substr() | mid()/left()/right() | 取子串的函数有多个等价选择,规则只拦了一个 |
= | like/regexp/in/between | 条件判断的等价写法非常多 |
concat() | 直接并列字符串'a' 'b' | 部分数据库支持隐式拼接 |
sleep(5) | benchmark(10000000,sha(1)) | 时间盲注的函数不止sleep一个 |
这类绕过的段位已经比大小写变形高一些了,因为攻击者对SQL语法体系的熟悉程度决定了它能变出多少种花样。规则加固的思路却不能是"继续加关键词"——你加了mid,人家还能用right;你把常见时间盲注函数都加了,人家还能写自定义函数。**更可靠的做法是:不让用户输入进入SQL语法语义层面(参数化查询/预编译),而不是在关键词层面做军备竞赛。**WAF规则层只是兜底,不是主线。
3.3 编码变形与双重解码,为什么总能骗过规则
前面2.2节讲过编码不对称的原理,这里给几个具体的例子:
# 单引号的URL编码 ' --> %27 # 双重URL编码 ' --> %2527 # Unicode编码(部分中间件支持) ' --> %u0027 # 十六进制编码(用于后端拼接) id=-1 union select 1,2,3 id=-1 union select 0x31,0x32,0x33先说一个实际场景。某次测试里,一个请求的User-Agent头里有' or 1=1--,加了单次URL编码%27%20or%201=1--之后,美图秀秀级别的基础WAF直接拦下来了——说明规则里有URL解码环节,解码后的内容被识别了。但改成双重编码%2527%2520or%25201=1--,就放行了。后端如果正好对这个值做两次解码,这个引号就能被还原出来。虽然这个场景最后没形成实际注入(因为参数没进SQL语句),但检测层面确实"漏"了。
加固这块的思路分两步。第一步,在WAF的预处理环节统一做"标准化解码"操作:连续解码直到没有可解码的字符,再统一进行大小写归一化。第二步,在Web服务器层面控制解码次数,不要让一个参数经过两个中间件各解一次。两层一起做,才能从根上消除"看到的和执行的不是一个值"的问题。
3.4 参数解析差异与协议层绕过
这一类的底层原因不是规则库写得不好,而是解析器的解析结果和后端实际使用的参数不一致。
举个例子,参数污染。请求长这样:
GET /search.php?id=1&id=1 union select 1,2,3WAF按标准解析方式取第二个id值,也就是1 union select 1,2,3,命中规则,拦截。但某些后端框架在取参数时,只取第一个value——第一个id是干净的1,于是请求被放行,后端拿到的却是攻击者拼好的恶意SQL。反过来也一样,有些框架取的是最后一个。WAF按自己的规则取,后端按框架的规则取,两者取的不是同一个值,校验就形同虚设。
再比如Content-Type伪装。有些WAF只对application/x-www-form-urlencoded这种格式的POST消息体做body检测,但攻击者把Content-Type改成text/plain或者multipart/form-data,一部分WAF直接放弃了body解析,参数内容完全不检查就放行了。而后端框架对这些Content-Type是照单全收的,照样把里面的数据拼进SQL语句。
这块儿的加固方案需要有一定的"对齐"意识。一是确定后端框架取参的规则,在WAF侧做参数取值顺序对齐;二是对Content-Type缺失或不常见的请求体,一律按最高安全级别处理,而不是跳过主体检测;三是在有条件的情况下用云WAF或Nginx层面的统一解析替代各业务系统自带的碎片化中间件解析。
4. 从绕过视角看防御:规则加固与验证的实操建议
4.1 搭一个能复现的测试环境
没有靶场,谈绕过就是空谈。搭建方式有很多种,我建议新手走一条成本最低的路线:
- 本机装一个开源WAF作为前置节点,配置好基础规则。
- 拿一个SQL注入靶场(比如常见的注入练习环境)作为后端。
- 用Burp Suite或者任何能改包的工具,手动构造不同类型的请求逐一测试。
测试时不要上来就找复杂手法,按顺序来:先测大小写变形,再测注释符混搭,再测编码替换,最后测参数污染。每测一条,就在日志里看一眼"是否命中规则、命中的是哪条规则",这样日积月累,你会逐渐摸清规则库的覆盖边界,也会更清楚"规则层面绕过"到底绕的是什么。
注意:所有测试都必须在本地靶场环境中进行。千万不要对着别人的线上系统做这类验证,那既没有任何技术意义,也是明确违规的行为。
4.2 规则加固的核心顺序
踩过不少坑之后,我总结了五条规则加固经验,按优先级排列:
- **第一优先级:参数化查询/预编译。**这不是WAF规则,但比任何规则都有效。SQL注入的本质是"数据和代码混在了一起",参数化查询让数据走专门通道进入SQL结构,注入直接失去存在的前提条件。所有新增的业务代码强制走ORM或预处理语句,这步做到位,80%的注入风险在一开始就不存在了。
- **第二优先级:统一解码与归一化。**确保WAF最终去匹配的字符串 = 后端真正执行时看到的字符串。把解码次数、解码顺序、大小写归一化流程固定下来,不给双重编码留空间。
- **第三优先级:分层配置规则。**对所有请求统一用一套宽松的基线规则,对高危接口(登录、查询、API接口)再加严一层。别把同一个强度的规则平铺到所有接口上,那样要么误杀太多,要么力度不足。
- **第四优先级:做语义分析型补充。**如果预算允许,在正则规则之上增加一个语义分析引擎——它不去匹配"长什么样",而是判断"这句话想干什么",对同义变形的识别能力比纯正则有数量级的提升。
- 第五优先级:日志与告警的"最后防线"。不管你规则配得多好,总会有一天被别人绕过去。关键不是你拦住了所有攻击,而是被绕过的那一刻你能不能发现。数据库错误日志中的异常报错、返回包中的SQL错误特征、同一IP短时间内大量请求5xx状态码,这些都是"可能已经有人摸进来了"的信号。有一次我们在日志里看到某接口密集返回500,排查后发现是有人在用时间盲注测试。要不是日志开着,这个行为可能被当成普通的服务抖动就忽略了。
4.3 别把WAF规则层当"万能保险箱"
我见过不少团队,买了一套WAF,做了个基础初始化配置,就把安全工作全押在上面了。这种心态非常危险,因为规则层面绕过的门槛其实不高——不需要0day,不需要什么高深的底层漏洞,只需要对SQL语法本身有一点灵活理解就够了。
**WAF规则层的定位应该是"护栏",不是"墙"。**它能把自动化扫描器挡在外面,把大部分随手试一下的投机者拦下来,也能在系统本身的防护做得不够好时,多争取一层缓冲。但它替代不了正当的编码规范、参数化查询、输入校验和权限控制。真正的防御纵深在于:即使某一条线被绕过,还有下一条线能兜住。
5. 常见问题速查表与排坑实录
最后整理一个实操中经常遇到的问题清单,都是真实踩过坑的地方:
| 问题现象 | 排查思路 | 解决建议 |
|---|---|---|
| 同样的payload,有时候被拦有时候直接放行 | 检查WAF是否有多个规则集并行匹配,看命中的具体是哪个规则;检查是否有请求频率限制和URL白名单导致部分路径"免检" | 把所有规则集改成"交集都查",不允许URL白名单覆盖安全规则 |
| 改了大小写还是被拦 | 目标WAF可能在匹配前已经做了大小写归一化,大小写绕过的"红利期"正在消失 | 改用注释符、编码变形、等价函数替代等混合手法;防御方注意将规则匹配统一为忽略大小写 |
| 为什么本地测试能过,线上却被拦了 | 线上WAF可能串联了多个安全产品,不是某个单一WAF在做检测;或者是不同设备的解码顺序不一样 | 测试时尽量复现线上完整的链路拓扑,至少先在Nginx层、WAF层、应用层三层分别做一次测试 |
| 规则加了十几条还是被绕 | 只用黑名单思路在堵漏洞,很难覆盖完整攻击面 | 优先做参数化查询和统一解码,规则只在正常代码规范之上做兜底 |
| 想验证编码绕过是否真的成功 | 不要只看WAF日志,还要看后端是否真正执行了payload | 用数据库日志或响应内容做"执行确认";请求被放行不等于注入成功 |
再补一条容易被忽略的经验:URL白名单和IP白名单请慎用。之前碰到过一个项目,为了方便业务联调,在WAF上加了某个路径的白名单,结果这个路径正好是一个接口,最后攻击者顺着白名单路径把整个库翻了。白名单不是不能加,但一定要加"白名单只豁免某项检测、不豁免全部检测"的限制,而且白名单路径要有明确的时效回收机制。
还有一个容易被忽略的小细节:检查你自己的安全产品日志是否被日志清洗或截断掩盖了。如果日志体量非常大,很多WAF的日志系统会在写入时截断payload内容,导致你在事后审计时根本看不到原始攻击载荷长什么样,也就无从分析绕过路径。有条件的话,把攻击payload的完整请求保存到一个独立存储里,别和大日志混在一起。
做了这么多年的安全对抗,我自己最大的体会是:规则层面的注入绕过,说到底是"模式匹配"和"语言表达"之间的不对称。只要检测方还在依赖有限的正则去匹配无穷的语义表达,绕过就永远存在。但这不意味着规则毫无意义——它能挡住没有思考能力的自动化扫描,能挡住绝大多数投机尝试,能为你发现异常争取时间。真正站在安全这一边的东西,还是那几句话:参数化查询、统一解码、最小权限、日志审计。把这四件事做好,别人绕过了WAF也翻不出什么浪花来。