☰
WAF绕过实战:从拦截原理到SQL注入绕过的完整路径
2026/10/19 4:41:23 网站建设 项目流程

搞过SQL注入的人应该都体会过这种挫败感:本地明明一把梭的语句,放到线上只要带个and或union,页面直接给你一个“拦截”大礼包。WAF 这个东西,平时感觉不到,等你真正想测试漏洞时才想起来它的存在。前阵子我搭了一个带 WAF 的 SQL 注入靶场,给这个 WAF 起了个内部代号叫“安全狗”,专门拿它来模拟真实业务里最常见的规则拦截场景。这次实战从环境搭建、规则探测、绕过手法到踩坑记录都走了一遍,这篇文章就是完整复盘,适合每一个想系统理解 WAF 绕过逻辑、而不是只会背 payload 的人。

所有测试都在本地靶场环境完成,没碰任何未授权目标。下面直接进入正题。

1. 项目背景与靶场搭建

1.1 为什么非要搭一个 WAF 靶场

想绕 WAF,最直接的冲动可能是找真实网站练手。但现实很骨感:真实站点有法律风险,WAF 的拦截日志还可能直接暴露你的 IP,更别提规则库每天都在更新,你昨天绕过去的手法今天可能就被补掉了。于是靶场成了唯一靠谱的选择。

靶场最大的价值是可控。你可以反复提交恶意请求,看 WAF 到底拦了什么、放行了什么,可以随时清日志、回滚数据和换 Web 服务版本,不需要为误操作道歉。拿我这里这套“安全狗”靶场来说,它模拟的就是那种基于规则匹配的软件 WAF,部署在 Web 服务前面,对 URL、POST 参数、Cookie 做黑名单检测。市面上很多商业 WAF 的检测思路都类似,搞懂这一类,后面再看云 WAF 会有很清晰的迁移感。

1.2 靶场环境搭建与部署步骤

我实际用的是 Linux + Nginx + PHP + MySQL 的组合。之所以选这套,是因为 MySQL 支持很多绕过特性,比如内联注释、特殊空白字符、函数等价替换,这些在真实渗透中出场率最高。Windows + IIS 也不是不行,但环境更老,和现在主流后端技术栈偏差较大。

搭建步骤大致是这样:

  1. 准备好一台干净的 Linux 主机,装好 Nginx、PHP、MySQL,并确保 Web 目录可以正常解析。
  2. 自己写一个带 SQL 注入的模拟项目页面,只保留一处明显漏洞。
  3. 在 Nginx 层接入 WAF 过滤模块,开启 SQL 注入防护规则。
  4. 重启 Web 服务,确认所有请求都会先经过 WAF。
  5. 用一组明显恶意请求做自测,验证拦截是否生效。

模拟项目页面的代码很简单,故意写成字符串拼接:

// 模拟项目X - 仅限本地靶场环境使用 $id = $_GET['id']; $sql = "SELECT * FROM users WHERE id = '$id'"; $result = mysqli_query($conn, $sql);

第一次自测的时候,我会先访问/?id=1,确认页面正常返回数据。然后提交/?id=1 and 1=1,如果返回了 WAF 拦截页面,说明防护规则已经生效。这里有个小建议:测试之前先对整个虚拟环境做一次快照,后面如果手滑写错规则或者把表弄乱了,直接恢复快照就能重新开始,不用重新装环境。

2. 从拦截原理推导绕过思路

2.1 WAF 到底在检查什么

很多人一上来就堆 payload,却搞不懂 WAF 是怎么拦的,结果越试越乱。其实软件 WAF 的处理流程并不复杂:先接收 HTTP 请求,进行 URL 解码、参数拆分、Body 解析,然后把参数值丢给一套规则引擎,规则引擎再用正则、关键词黑名单、行为特征去匹配。一旦命中,就返回拦截页面。

所以它本质上是在做“字符串匹配”,而不是真正理解 SQL 语义。这很关键。数据库拿到union select就知道你要联合查询,但 WAF 看到union select只是觉得这串字符像风险特征。既然是字符串匹配,那就能用编码、大小写、注释、等价替换这些手段去干扰匹配结果。

常见的检测重点集中在and、or、union、select、sleep、updatexml、information_schema、单引号、注释符这些关键词上。你只要构造的语句里出现这些连续字符串,就可能被拦。但这不代表没法表达同样的逻辑,关键是让 WAF 看到的字符串和数据库最终解析出来的语法不一致。

2.2 四个最核心的绕过方向

我把常见绕过方法先归纳成四个方向,有了这个框架,后面那些花哨手法就都有了归属,也不容易乱试。

绕过方向核心思路典型手法
特征混淆让风险关键词不以原始连续形态出现关键字间插注释、内联注释、大小写变形
编码转换让 WAF 解码后的内容与后端不一致二次 URL 编码、十六进制、Unicode
语法重组用数据库支持的等价语法替代被检测语法and换&&,=换LIKE,函数等价替换
解析异常利用 WAF 与中间件解析差异参数污染、multipart 表单、畸形 Content-Type

这四个方向不是独立的,实战中经常需要叠加。比如先用注释把union select拆开,再用十六进制把字符串值隐藏起来,最后换一个等价函数绕过报错检测。单独用某一种,遇到规则稍微严格一点的 WAF 往往就会被秒拦。

3. 安全狗靶场实战:从探测到绕过的完整过程

3.1 第一步:确认注入点并识别拦截规则边界

拿到靶场后,我先不急着绕,而是把 WAF 的“脾气”摸清楚。用 Burp Suite 或者身边任意一个请求工具,按顺序提交下面几组请求:

/?id=1 /?id=1' /?id=1 and 1=1 /?id=1' and '1'='1 /?id=1 order by 1 /?id=1 union select 1,2,3

每一组都记录响应特征。通常id=1正常返回,id=1'可能被拦,and 1=1大概率也会被拦。拦截页面一般会带有固定标识,或者直接返回 403 状态码。只要特征稳定,后续自动化判断就简单了。

我测下来发现,纯大小写变形完全没用,因为安全狗的规则开了不区分大小写。SeLeCt和select在它眼里是一样的。但是union和select中间如果插了注释或特殊空白字符,检测逻辑就容易被绕过去。这个细节很值得记录,属于典型的“规则正则写得太死”。

测试结果建议整理成一张拦截基线表:payload 是什么、是否被拦、返回状态是什么。后面做 fuzz 时,这个基线表能帮你快速定位是第一层 WAF 拦了,还是后端语法本身错了。

3.2 注释符与 MySQL 内联注释绕过

先说最基础的注释符替代空格。在 MySQL 里,/**/可以出现在 SQL 关键字之间的位置,等价于一个或多个空格。所以union select可以写成:

union/**/select

在 URL 里拼上注入点就是:

/?id=1' union/**/select 1,2,3%23

这里的%23是 URL 编码后的#,用来注释掉后面多余的 SQL 片段。如果只出现union select会被拦,但union/**/select没有被拦,那说明 WAF 的正则只匹配了原始空格形态。

更进阶的是 MySQL 内联注释。MySQL 支持/*! ... */这种带版本号的可执行注释,意思是如果当前数据库版本满足条件,注释里的内容会被当作 SQL 执行。例如:

/*!50000union*/

当 MySQL 版本大于等于 5.0 时,这段注释会被解析成union。WAF 在规则匹配时往往只识别普通注释,看到/*!可能直接当注释忽略,或者没有对注释内容里的关键字做二次匹配,于是就能绕过。

实际表达联合注入时会这样写:

/?id=1' /*!50000union*/ /*!50000select*/ 1,2,3%23

我在这个靶场里实测,union和select单独出现都会被拦,但加上版本号内联注释后放行的概率明显提高。记住,内联注释只适用于 MySQL,MSSQL 和 Oracle 都不认这套,换数据库环境就得换思路。

3.3 运算符与函数等价替换

and和or是最容易被 WAF 盯上的词。一个简单思路是把它们换成符号:

  • and换成&&
  • or换成||
  • =换成LIKE、IN、BETWEEN、REGEXP

由于&在 URL 里是参数分隔符,直接写会截断参数,必须做 URL 编码。比如?id=1' and 1=1--+可以改造成:

/?id=1' %26%26 1=1--+

这里%26%26解码后就是&&。如果安全狗的正则里只写了and和or,没有提取符号型运算符,这招就有效。不过要注意不同数据库对&&和||的支持情况,MySQL 能用,Oracle 的||是字符串拼接,含义完全不同,容易翻车。

函数等价替换在盲注和报错注入里特别有用。比如从字符串里取字符时,substr()被拦就换mid(),再不行换substring()。判断条件时,if()被拦可以换case when结构。时间盲注里sleep()是重点检测对象,可以换benchmark(10000000, sha1('test')),它会让数据库执行大量计算,效果等同于延时。

还有个很实用的小技巧:把字符串值转成十六进制。比如 SQL 里需要比较table_name = 'users',可以直接写成:

table_name = 0x7573657273

这样请求里既没有单引号,也没有users这个明文关键词,WAF 的正则就不容易命中。

3.4 编码与特殊空白字符绕过

WAF 一般都会做 URL 解码,所以直接%27表示单引号是没用的,它会把%27还原成'然后照常检测。但双重 URL 编码有时能成功:如果后端框架对参数做了两次解码,你提交%2527,WAF 如果只解一层,看到的是%27,可能认为里面没有单引号而放行;后端再解一层,又变成了'。这种手法跟具体技术栈关系很大,并不是每个环境都有效,需要在靶场里实际验证。

还有一种值得反复试的是特殊空白字符。MySQL 在解析 SQL 时,不仅把 ASCII 空格当作分隔符,还认%09(Tab)、%0a(换行)、%0b(垂直制表)、%0c(换页)、%0d(回车)。WAF 的正则如果只匹配普通空格,遇到这些字符就容易漏。

比如:

/?id=1'%0aunion%0aselect%0a1,2,3%23

这里的%0a在 MySQL 眼里就是空白,能把union和select正常分开,但安全狗如果只按空格做特征匹配,就不会命中。这个手法我实测下来成功率不错,建议优先尝试。

3.5 参数污染与协议边界绕过

参数污染(HPP)是我个人很喜欢的一类手法,它赌的是 WAF 和 Web 中间件“对参数的理解不一样”。最常见的用法是提交两个同名参数:

/?id=1&id=1' union select 1,2,3%23

Nginx 和 PHP 处理同名参数时,默认取最后一个值,所以后端最终拼接进来的是第二个恶意参数;而一些 WAF 在解析参数时可能只取第一个,或者只检查第一个参数里的内容,于是第二个参数就漏过去了。

使用前务必先确认目标环境到底取哪个参数。可以在正常状态下提交/?id=1&id=2,看页面返回的是 1 还是 2。如果返回 2,说明取最后一个;如果返回 1,说明取第一个。搞清楚这一点,HPP 才有意义,不然纯属自嗨。

除了参数污染,还可以尝试用multipart/form-data表单提交数据。很多 WAF 对 urlencoded 格式解析得很仔细,但对 multipart 的解析不够完善,尤其是畸形 boundary 或大小写混用的Content-Type,可能直接跳过检测。这类手法依赖具体 WAF 实现,不保证通用,但靶场里遇到卡壳时值得一试。

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

4.1 先分清“拦截”和“语法错误”

绕过 WAF 最怕的不是被拦,而是把“被拦”误判成“语法错误”,或者反过来。一个经验丰富的测试者,第一个动作永远是看响应特征。WAF 拦截页通常有几类特征:固定状态码(比如 403)、固定页面标题、固定关键字、响应长度异常稳定。而 SQL 语法错误则常常是 200 状态码,页面里会出现数据库报错信息,或者页面内容明显变化。

我建议把 WAF 返回的拦截页面完整保存下来,做自动化脚本时只匹配几个特征关键词,避免误判。如果在同一套靶场环境里,把一个请求放到浏览器直接访问返回拦截页,放到脚本里却显示正常,那多半是脚本没有处理好 Cookie、User-Agent 或编码头,而不是绕过成功了。

4.2 内联注释不执行,看着没绕过

用/*!50000union*/绕过时,如果请求没有被拦,但页面也没有出现注入效果,先别急着说手法没用,大概率是注释里的版本号比当前 MySQL 版本高。MySQL 的规则是:只有当前版本大于等于注释内版本号时,才会把内容当作 SQL 执行,否则当作普通注释。

所以需要先确认数据库版本。可以直接在靶场里用一个正常注入点执行version(),也可以从页面报错信息里猜。版本确认后,把50000改成合适的数字。比如 MySQL 5.7 环境,用/*!50000union*/是可以的,但如果你在 MySQL 5.1 上用/*!50000union*/,它可能不会执行。这个细节经常让新手卡半天,其实调整一下版本号就好。

4.3 注释符和编码的细节问题

--+在 URL 参数里很常见,很多人当成万能注释符,但它的原理是+被 HTTP 解码成空格,最终 SQL 看到的是--,这才是合法注释。如果是在命令行直接跑 SQL 文本,或者在其他协议里提交,+不会被解码成空格,注释就可能失效。所以别把--+背死,稍微变一变环境就不灵了。

还有#注释符在 URL 中必须编码成%23。直接在浏览器地址栏输入#时,后面内容会被当作 fragment,根本不会发送到服务器。很多新手说“我明明提交了#怎么没反应”,其实请求到了服务器前就已经被浏览器截断了。

4.4 自动化 fuzz 要控制节奏

绕过 WAF 不能全靠手点,写个小脚本自动遍历 payload 很有效率。但脚本不能瞎写,至少要加特征判断、超时控制和随机延时。

一个简单的 Python 示例:

import requests url = "http://localhost:8080/?id=" payloads = [ "1' and 1=1--+", "1' %26%26 1=1--+", "1'%0aunion%0aselect%0a1,2,3%23", "1' /*!50000union*/ /*!50000select*/ 1,2,3%23", ] for p in payloads: try: r = requests.get(url + p, timeout=5) if "拦截" not in r.text and r.status_code == 200: print("先进去看一眼:", p) else: print("被拦:", p) except Exception as e: print("出错:", p, e)

注意这个脚本只适合本地靶场。真实的未授权站点绝对不能这么扫,既违反规则,也容易被反制。脚本里一定要有超时和延时,不然 WAF 或服务端连接池很容易被拖垮,结果页面响应全部异常,fuzz 数据就失真了。

5. 一些个人实操心得与后续扩展

5.1 先看日志,再盲试

很多人喜欢从网上找一堆现成 payload 往靶场里怼,中一个算一个。这样其实效率很低,因为安全狗这类 WAF 大多会在本地写拦截日志,日志里能看到命中的规则 ID 和匹配内容。通过日志反推规则,比自己瞎猜准确得多。

我在靶场里每次提交异常请求后,都会顺手看一眼拦截日志。比如日志显示命中了union select规则,那接下来我就重点绕过空格和union select的连续形态。日志显示命中了information_schema,那我查数据时就把表名换成十六进制。这种“先看规则再构造”的方式,远比盲目堆绕过手法更接近真实渗透思维。

5.2 建立自己的 payload 矩阵

打靶场最值得留下的不是某个绕过成功的 payload,而是那套“请求 + 结果 + 原因”的对应关系。我会定期维护一个表格,把每个 payload、是否被拦、拦截日志命中的规则、最终是否出数据、出数据用的是哪一步手法,全部记录下来。

这个记录习惯帮了我大忙。后面再遇到新的 WAF,我不用重新踩完所有坑,只需要沿着矩阵里的方向快速过一遍,就能判断新 WAF 的规则偏重哪类特征。绕过本质上是寻找规则盲区,而规则盲区需要靠大量对照实验才能定位,没有记录就等于白测。

5.3 从绕过到防御的视角切换

绕过 WAF 打到一半,我经常提醒自己:现在研究的是防御体系的弱点,而不是“怎么样搞垮网站”。同一个靶场,完全可以切换成防守视角,去思考如果自己是规则编写者,该怎么补上这些绕过点。比如看到/*!50000union*/能过,就要考虑在规则里把内联注释内容拆出来二次检测;看到%0a能过,就要把特殊空白字符统一归一化成空格,再做一次匹配。

这段视角切换对我帮助很大。靶场实战不是鼓励大家去找真实网站的麻烦,而是用可控环境把攻防逻辑跑通。如果能把“绕过”和“修补”两边都理解到位,以后再面对更复杂的 WAF 产品,基本都能快速找到切入点,也大概能预判哪些手法会被新规则补掉。

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

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

立即咨询