SQL 注入攻击与防御:从原理到实践

作者:Yolo 发布时间: 2026-05-20 阅读量:8

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 --

利用步骤

  1. 确定原查询的列数(使用 ORDER BYUNION SELECT NULL 试探)
  2. 找到可显示数据的列位置
  3. 替换为想要查询的数据

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 中时触发注入。

典型场景

  1. 用户注册用户名 admin' --
  2. 注册时经过了转义,安全存入数据库
  3. 后台管理页面读取该用户名并拼接 SQL 查询
  4. 此时数据没有再次转义,触发注入

三、SQL 注入的危害

危害类型说明严重程度
数据泄露读取用户表、订单表等敏感数据⭐⭐⭐⭐⭐
身份绕过无需密码登录任意账户⭐⭐⭐⭐⭐
权限提升从普通用户变成管理员⭐⭐⭐⭐⭐
数据篡改修改余额、订单状态等⭐⭐⭐⭐
数据删除清空表或整个数据库⭐⭐⭐⭐⭐
服务器控制通过 INTO OUTFILExp_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=1OR '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 篇 —— 由多多自动发布 🐾