本文仅用于 CTF、靶场、本机实验和明确授权环境。文件写入、数据修改和高负载测试应限定在可恢复的专门环境中。

SQL 注入最终影响并不只由 Payload 决定。MySQL 版本、SQL mode、数据库驱动、账户权限、文件系统权限和应用反馈共同决定一条路线能否成立。

1. MySQL 文件读取

1
LOAD_FILE('/path/to/file')

能否读取通常取决于:

1
2
3
4
5
6
当前账户是否具备 FILE 权限
mysqld 操作系统账户是否能读取目标文件
secure_file_priv 是否允许该路径
文件是否位于数据库服务器本机
文件大小是否超过相关限制
查询结果是否存在回显通道

发现 SQL 注入并不等于能够读取任意文件。

2. 文件写出

1
2
SELECT expression
INTO OUTFILE '/allowed/path/result.txt';
1
2
SELECT expression
INTO DUMPFILE '/allowed/path/result.bin';

区别:

1
2
INTO OUTFILE   按字段和行格式输出
INTO DUMPFILE 写一行原始内容,不添加字段或行终止符

限制:

1
2
3
4
5
FILE 权限
secure_file_priv
目标目录写权限
目标文件通常不能已存在
数据库服务所在主机和 Web 主机可能不是同一台

普通漏洞验证不应通过写入可执行文件证明影响。

3. secure_file_priv

1
@@global.secure_file_priv

常见状态:

1
2
3
指定目录    导入导出限制在该目录
空字符串 不限制到特定目录,但其他权限仍有效
NULL 禁用相关文件导入导出操作

如果没有直接回显,可依次判断:

1
2
3
@@global.secure_file_priv IS NULL
LENGTH(@@global.secure_file_priv) > 10
ASCII(SUBSTR(@@global.secure_file_priv,1,1)) > 64

知道目录只满足一个条件,并不证明可读写。

4. LOAD DATA LOCAL 的边界

LOAD DATA LOCAL 中的文件位于客户端主机,而普通 LOAD DATA 的文件位于服务器主机。

在 Web 架构中:

1
浏览器 → Web 应用(MySQL 客户端)→ MySQL 服务器

此时 MySQL 所说的“客户端”是 Web 应用进程所在主机,不是浏览器用户的电脑。

风险是否成立取决于:

1
2
3
4
5
6
客户端库是否启用 LOCAL 能力
服务端 local_infile 是否允许
客户端是否连接了不可信 MySQL 服务端
客户端进程能够读取哪些文件
是否限制 LOCAL 文件目录
是否验证数据库服务器身份

防御重点是默认关闭不需要的 LOCAL 能力、限制目录,并验证 MySQL 服务端身份。

5. SQL mode

5.1 ANSI_QUOTES

启用后双引号用于标识符,不能再简单当作字符串引号。

5.2 NO_BACKSLASH_ESCAPES

启用后,反斜杠不再作为字符串中的转义字符。

5.3 PIPES_AS_CONCAT

启用后,|| 可能从逻辑 OR 变为字符串拼接。

5.4 ONLY_FULL_GROUP_BY

影响聚合查询中非聚合列是否合法。旧教程中的某些 GROUP BY 写法在严格模式下会失败。

查看:

1
SELECT @@sql_mode;

6. MySQL、MariaDB 与版本差异

需要记录完整版本:

1
SELECT VERSION();

版本差异可能影响:

1
2
3
4
5
6
7
8
函数是否存在
报错文本和报错行为
默认字符集与 collation
默认 SQL mode
正则实现
认证插件
保留关键字
information_schema 与数据字典实现

旧文章中的报错函数失效,不代表注入不存在,也可能只是版本不同。

7. 驱动差异

同一个 MySQL 后端,经由不同驱动可能表现不同:

1
2
3
4
5
PHP mysqli
PHP PDO
Python Connector
Java JDBC
Node.js mysql2

需要检查:

1
2
3
4
5
6
是否使用服务端预处理
是否模拟预处理
是否允许多语句
参数类型如何发送
连接字符集如何设置
错误如何抛出或隐藏

分析源码时,不能只看 SQL 字符串,也要看驱动调用方式。

8. 字符集与转义

风险受以下因素共同影响:

1
2
3
4
5
6
客户端输入编码
连接字符集
服务端字符集
转义函数所用连接对象
NO_BACKSLASH_ESCAPES
多层 URL 解码

经典宽字节问题只在特定字符集与错误转义组合下成立,并非所有 MySQL 网站通用。

正确做法是使用真正的参数化查询,并明确设置连接字符集。

9. 参数化查询

数据值:

1
2
3
4
5
$stmt = $conn->prepare(
'SELECT id,username FROM users WHERE id=?'
);
$stmt->bind_param('i', $_GET['id']);
$stmt->execute();

参数化适合:

1
2
3
4
5
字符串值
数字值
日期
布尔值
二进制数据

通常不能直接绑定:

1
2
3
4
5
表名
列名
SQL 关键字
排序方向
一整段条件表达式

这些结构需要固定映射、安全查询构造器或白名单。

10. 最小权限

最小权限不能修复注入,但能降低后果:

1
2
3
4
5
6
Web 应用使用独立账户
只授权必要数据库和表
只读接口不具备写权限
不授予无必要 FILE 权限
不授予管理、创建函数等高权限
不同应用不共用高权限账户

11. 错误与日志

生产页面不应显示:

1
2
3
4
5
完整 SQL
表名和字段名
连接账号
数据库路径
驱动堆栈

详细错误应写入受保护的服务端日志,并进行访问控制和敏感信息脱敏。

12. 完整做题决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
找到可控输入

建立正常响应基线

一次只改变一个因素

特殊字符是否影响响应?

真假条件是否存在稳定差异?

识别数字、字符串、LIKE、括号、ORDER BY 或写入型上下文

判断反馈通道
├─ 数据回显 → 联合查询与直接查询
├─ 错误回显 → 版本匹配的错误反馈
├─ 真假差异 → 布尔盲注
├─ 时间差异 → 受控时间盲注
└─ 无反馈 → 二次触发、其他页面、源码或题目通道

13. 方法失败时

1
2
3
4
5
6
7
8
9
请求有没有到达应用?
输入经过了几次解码?
WAF 看到了什么?
应用是否删除、转义或转换输入?
最终 SQL 是否语法完整?
表达式是否实际执行?
MySQL/MariaDB 是否支持?
当前账户是否有权限?
结果是否存在观察通道?

14. “函数不能用”的回答框架

1
2
3
4
5
1. 说明函数承担的能力
2. 判断失效原因:过滤、版本、权限、上下文或无反馈
3. 选择同义函数或等价表达式
4. 同类函数都不可用时,切换查询结构或反馈通道
5. 说明替代方案的条件和验证方式

15. 源码审计清单

1
2
3
4
5
6
7
8
9
10
11
[ ] 是否通过拼接或模板字符串构造 SQL
[ ] 所有普通值是否正确绑定
[ ] 动态表名、列名和排序是否白名单
[ ] 数组元素是否逐项绑定并限制数量
[ ] ORM 是否调用 raw SQL
[ ] 存储过程内部是否再次拼接
[ ] 数据库旧数据是否进入动态 SQL
[ ] 连接字符集是否明确
[ ] 是否无必要开启多语句和 LOCAL INFILE
[ ] 数据库账户是否遵循最小权限
[ ] 生产错误是否泄露数据库细节

16. CTF 复盘模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
题目与授权环境:
正常请求基线:
可控输入:
原始 SQL 推测:
闭合类型:
反馈通道:
已确认事实:
当前推断:
待验证内容:
过滤规则:
函数替代:
成功路线:
失败路线及原因:
正确修复:
可迁移知识:

17. 系列总结

真正需要记住的不是一条 Payload,而是:

1
2
3
4
5
6
7
输入在哪里?
闭合什么?
当前有什么反馈?
需要实现什么能力?
失败发生在哪一层?
替代方案需要什么版本、权限和配置?
开发者应如何从根本上修复?

这七篇构成当前阶段的 MySQL SQL 注入知识骨架。后续遇到新的 CTF 题、函数限制或数据库差异,再把经过验证的内容补成单独文章。

参考资料