本文仅用于 CTF、靶场、本机实验和明确授权环境。

SQL 注入不只出现在 ?id=1。排序字段、搜索通配符、JSON 列清单、Cookie、请求头,以及从数据库再次取出的旧数据,都可能进入动态 SQL。

1. ORDER BY 注入

危险代码:

1
2
$sql = "SELECT id,name,price FROM products
ORDER BY " . $_GET['sort'];

正常输入:

1
2
price
created_at

这里的输入是列名或表达式,不是普通字符串值。

观察方法:

1
2
3
4
切换列名后结果顺序是否变化
不存在列名是否产生 Unknown column
数字是否按结果列位置排序
条件表达式是否改变第一条记录

排序注入通常不改变响应长度,只改变顺序。

防御:

1
2
3
4
5
6
7
$allowed = [
'price' => 'price',
'newest' => 'created_at',
'name' => 'name'
];

$sort = $allowed[$_GET['sort']] ?? 'created_at';

排序方向 ASC/DESC 也要白名单。

2. LIKE 搜索

危险代码:

1
2
3
$q = $_GET['q'];
$sql = "SELECT * FROM articles
WHERE title LIKE '%$q%'";

需要区分:

1
2
SQL 注入          输入改变 SQL 结构
LIKE 通配符问题 %、_ 扩大业务匹配范围,但没有改变 SQL 结构

参数化查询可以阻止结构注入,却不会自动把 %_ 当普通字符。如果业务要求字面匹配,还需要单独处理通配符转义。

3. 登录框

1
2
3
$sql = "SELECT id,role FROM users
WHERE username='$u'
AND password='$p'";

检查:

1
2
3
4
5
用户名和密码哪个字段进入查询
程序判断行数、字段还是哈希
返回多行时选择哪一行
密码在应用层还是数据库层验证
错误是否区分用户不存在与密码错误

不要把弱口令、默认账户、认证逻辑错误、NoSQL 注入和越权都叫作 SQL 注入。

4. LIMIT 与 OFFSET

1
2
3
$sql = "SELECT * FROM articles
LIMIT " . $_GET['count'] .
" OFFSET " . $_GET['offset'];

分页参数容易被误认为“天然只能是数字”。安全做法:

1
2
3
4
转换并验证为非负整数
设置合理上限
驱动支持时按整数类型绑定
限制超大分页造成的性能影响

5. 动态表名与分表

1
2
$table = 'logs_' . $_GET['month'];
$sql = "SELECT * FROM $table";

表名属于 SQL 结构,普通参数占位符通常不能代替它。应将业务值映射到固定表名,并限制数据库账户只访问必要对象。

6. JSON API

1
2
3
4
5
6
{
"id": 1,
"filter": "active",
"sort": "created_at",
"fields": ["id", "name"]
}

分析:

1
2
3
id、filter   常作为数据值,可以绑定
sort 常作为标识符,需要白名单
fields 常被拼成列清单,需要逐项白名单

使用 JSON 只改变输入格式,不会自动消除 SQL 注入。

7. IN 数组

危险写法:

1
2
$ids = implode(',', $_GET['ids']);
$sql = "SELECT * FROM users WHERE id IN ($ids)";

安全思路:

1
2
3
4
验证每个元素类型
为每个元素创建独立占位符
限制数组长度
空数组使用明确业务分支

把整个逗号字符串绑定到一个占位符,通常也不能自动形成多个 IN 元素。

1
2
$token = $_COOKIE['token'];
$sql = "SELECT * FROM sessions WHERE token='$token'";

Cookie 由客户端携带,不能因为它不在 URL 中就默认可信。

9. 请求头

1
2
$ua = $_SERVER['HTTP_USER_AGENT'];
$sql = "INSERT INTO visits(user_agent) VALUES ('$ua')";
1
2
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
$sql = "SELECT * FROM bans WHERE ip='$ip'";

User-AgentRefererX-Forwarded-For 都可能由客户端控制。X-Forwarded-For 只有在可信反向代理正确覆盖时才具有可信意义。

10. ORM

ORM 通常能安全处理普通值,但危险点包括:

1
2
3
4
5
6
raw query
whereRaw / orderByRaw
模板字符串
动态字段名和表名
把整个条件字符串传给 ORM
原生数据库表达式接口

“使用 ORM”不是“没有 SQL 注入”的充分证据。

11. 存储过程

调用存储过程时使用参数,并不代表过程内部安全:

1
2
3
SET @sql = CONCAT('SELECT * FROM ', input_table);
PREPARE stmt FROM @sql;
EXECUTE stmt;

如果过程内部继续拼动态 SQL,注入仍然存在。

12. INSERT 注入

1
2
INSERT INTO messages(username,content)
VALUES ('alice','hello');

页面可能只返回“提交成功”。观察点可能在:

1
2
3
4
5
数据库错误
写入行数
后续列表页面
后台审核页面
二次查询

13. UPDATE 注入

1
2
3
UPDATE users
SET nickname='new'
WHERE id=1;

输入可能影响赋值表达式、其他字段或 WHERE 条件。必须使用可恢复测试数据。

14. 二次注入

第一阶段安全保存:

1
INSERT INTO users(username) VALUES (?);

第二阶段错误拼接:

1
2
3
$oldName = load_name_from_database();
$sql = "UPDATE users SET password='$new'
WHERE username='$oldName'";

即使第一步参数化,数据在第二步被重新当成 SQL 片段,仍会触发漏洞。

排查过程:

1
2
3
4
5
找到可写字段
→ 确认数据如何保存
→ 追踪后续读取位置
→ 检查新的 SQL 执行点
→ 观察第二阶段反馈

修复原则:每个 SQL 执行点都必须安全构造,数据库中的旧数据并不天然可信。

15. 堆叠查询

1
SELECT 1; SELECT 2;

是否能够一次执行多条语句,取决于:

1
2
3
4
5
数据库驱动是否允许 multi statements
应用使用的具体 API
数据库账户权限
WAF 是否拦截分号
应用是否处理后续结果集

常见误区:

1
2
3
分号没有报错 ≠ 第二条语句已执行
MySQL 支持多语句 ≠ Web 驱动默认允许
能执行查询 ≠ 拥有写入或管理权限

涉及修改、删除数据或改变数据库对象时,应使用可重置的专门靶场。

16. 审计清单

1
2
3
4
5
6
7
8
[ ] URL、POST、JSON、Cookie 和请求头是否全部追踪
[ ] 动态排序、分页、列名和表名是否白名单
[ ] 数组参数是否逐项绑定
[ ] ORM 是否存在 raw SQL
[ ] 存储过程内部是否拼接
[ ] 数据库旧数据是否进入新 SQL
[ ] 多语句功能是否无必要地开启
[ ] 数据库账户是否权限过大

下一篇收束 MySQL 文件能力、SQL mode、版本与驱动差异,并给出整套做题和修复决策树。

参考资料