SQL注入学习笔记
我好菜啊,卧槽。
我们以 MySQL、PostgreSQL、Microsoft SQL Server、Oracle 为主要参考。它们的语法有共性,但也存在差异。
一、sql基本语法
SELECT
SELECT column1, column2FROM table_nameWHERE condition;含义:
SELECT:查询哪些字段。FROM:从哪张表查询。WHERE:查询条件。;:语句结束符。
假设有一张 users 表:
| id | username | password |
|---|---|---|
| 1 | admin | 123456 |
| 2 | alice | abc123 |
| 3 | bob | qwerty |
执行:
SELECT * FROM users WHERE id = 1;只会返回 admin。
如果条件变成:
SELECT * FROM users WHERE id = 1 OR 1=1;where后为真,会返回所有。
WHERE
需要熟悉以下运算符:
| 运算符 | 含义 | 示例 |
|---|---|---|
= | 等于 | id=1 |
!= | 不等于 | id!=1 |
> | 大于 | id>1 |
< | 小于 | id<10 |
AND | 逻辑与 | id=1 AND active=1 |
OR | 逻辑或 | id=1 OR active=1 |
NOT | 逻辑非 | NOT active=1 |
LIKE | 模糊匹配 | name LIKE 'a%' |
IN | 集合匹配 | id IN (1,2,3) |
IS NULL | 判断空值 | name IS NULL |
特别注意 AND 和 OR 的优先级。
例如:
SELECT * FROM usersWHERE username = 'admin'AND password = '123'OR 1=1;逻辑上相当于:
(username = 'admin' AND password = '123')OR 1=1因为 AND 的优先级高于 OR。
这意味着 OR 1=1 可能让整个条件恒真。
字符串与数字
构造payload时要先清楚类型。
数字条件:
SELECT * FROM users WHERE id = 1;字符串条件:
SELECT * FROM users WHERE username = 'admin';两者区别在于:
- 数字通常不需要引号。
- 字符串通常需要引号。
- 字符串中的引号需要正确闭合。
假设服务端采用字符串拼接:
String sql = "SELECT * FROM users WHERE username = '" + input + "'";用户输入:
admin最终 SQL:
SELECT * FROM users WHERE username = 'admin';如果输入中包含用于结束字符串的字符,就可能改变后续 SQL 的结构。
这就是所谓的字符串上下文注入。
注释符
注释符的作用通常很大,因为注入sql后可能后面还有别的条件,引号啥的。
| 数据库 | 注释方式 |
|---|---|
| MySQL | # |
| MySQL | -- |
| PostgreSQL | -- |
| SQL Server | -- |
| Oracle | -- |
order by排序
SELECT * FROM usersORDER BY id ASC;ASC:升序。DESC:降序。
SQLi 中它有一个很重要的用途:判断查询结果有多少列。
例如:
SELECT username, email, ageFROM usersORDER BY 1;这里 ORDER BY 1 表示按照查询结果中的第一列排序。
如果改成:
ORDER BY 3;则按照第三列排序。
在适当的查询上下文中,可以逐渐增加序号:
ORDER BY 1ORDER BY 2ORDER BY 3ORDER BY 4当序号超过实际查询列数时,数据库可能报错。
UNION联合查询
这是 SQL 注入必须掌握的核心语法之一。
正常 SQL:
SELECT username FROM users;联合查询:
SELECT username FROM usersUNIONSELECT name FROM administrators;UNION 可以把两条查询的结果合并。
但是有两个重要限制:
- 两条 SELECT 查询必须具有相同数量的列。
- 对应列的数据类型必须兼容。
例如:
SELECT username, age FROM usersUNIONSELECT name, id FROM administrators;这里两边都是两列。
如果第一条查询是三列,第二条查询只有两列,就无法直接 UNION。
当应用程序把查询结果展示在页面上时,我们有机会让数据库返回其他表的数据。
这是 PortSwigger 中 UNION-based SQLi 的核心原理。
GROUP BY 和 HAVING
SELECT department, COUNT(*)FROM employeesGROUP BY departmentHAVING COUNT(*) > 5;GROUP BY:分组。COUNT(*):统计记录数量。HAVING:筛选分组后的结果。
注意:
WHERE 在分组前筛选记录,HAVING 在分组后筛选结果。
子查询
子查询就是 SQL 中嵌套 SQL。
例如:
SELECT usernameFROM usersWHERE id = ( SELECT user_id FROM orders WHERE order_id = 100);内部的 SELECT 先提供一个结果,外部 SELECT 再使用这个结果。
SQLi 中子查询非常重要,因为有些盲注场景无法直接回显数据,只能通过条件判断提取数据。
例如:
SELECT *FROM usersWHERE username = 'admin'AND ( SELECT SUBSTRING(password, 1, 1) FROM users WHERE username = 'administrator') = 'a';这个例子是在判断 administrator 密码的第一个字符是不是 a。
如果页面出现某种可观察的差异,就可以判断条件是否成立。
LIMIT 和 OFFSET
MySQL、PostgreSQL 等数据库常见:
SELECT * FROM usersLIMIT 10 OFFSET 20;意思是跳过前 20 条,返回后面的 10 条。
SQL Server 和 Oracle 在分页语法上有所不同。
它们在 SQLi 中可能涉及结果集控制、数据提取等场景。
二、数据库信息
SQL 语法是一方面,数据库自身的信息结构也很重要。
例如,你可能已经成功实现 UNION 注入,但不知道:
- 数据库叫什么?
- 当前使用哪张表?
- 有哪些字段?
- administrator 的密码存在哪里?
这就需要掌握数据库元数据。
information_schema
MySQL 和 PostgreSQL 等数据库提供了数据库元数据查询机制。
以 MySQL 为例:
查询数据库:
SELECT schema_nameFROM information_schema.schemata;查询表:
SELECT table_nameFROM information_schema.tablesWHERE table_schema = '目标数据库';查询字段:
SELECT column_nameFROM information_schema.columnsWHERE table_name = 'users';三、常用数据库函数
这些函数在盲注、信息收集和数据提取中非常重要。
| 函数 | 作用 |
|---|---|
VERSION() | 获取数据库版本(MySQL 等) |
DATABASE() | 获取当前数据库(MySQL) |
USER() | 获取当前数据库用户(MySQL) |
LENGTH() | 获取字符串长度 |
SUBSTRING() | 截取字符串 |
ASCII() | 获取字符编码值 |
CONCAT() | 拼接字符串 |
COUNT() | 统计记录 |
SLEEP() | MySQL 时间延迟函数 |
四、注入技巧
1.经典注入(In-band SQLi)
特点:数据库查询结果能够通过正常 HTTP 响应观察到。
主要分成两类。
A. 条件注入(Boolean Logic)
核心思想:改变 WHERE 条件,让查询结果发生变化。
假设原始 SQL:
SELECT * FROM productsWHERE category = 'Gifts'AND released = 1;可以通过比较不同逻辑条件的响应,判断输入是否影响 SQL 执行。
例如,测试:
' AND 1=1--和:
' AND 1=2--两者的差异:
| 条件 | 结果 |
|---|---|
1=1 | 恒真 |
1=2 | 恒假 |
如果两个请求的响应明显不同,就说明你可能成功影响了 SQL 查询逻辑。
B. UNION-based SQLi
核心思想:通过 UNION 将其他 SELECT 查询的结果合并到原查询结果中。
典型流程:
确认存在 SQL 注入 ↓判断原始查询列数 ↓判断哪些列可以显示文本 ↓确定数据库类型 ↓查询数据库元数据 ↓获取目标表数据重点掌握:
UNION SELECTORDER BYNULL- 数据类型兼容
information_schema
例如,假设原始查询有三列:
SELECT id, name, price FROM products;那么联合查询也需要三列:
SELECT id, name, price FROM productsUNIONSELECT 1, 'test', 2;这里第二条查询的三列分别是数字、字符串、数字。
为什么经常使用 NULL?
UNION SELECT NULL, NULL, NULL因为 NULL 在很多数据库中可以与不同类型兼容,有助于测试列数。
2. Error-based SQLi(报错注入)
核心思想:
让数据库产生可观察的错误,并利用错误信息推断数据。
例如:
- 数据类型转换错误。
- 数学运算错误。
- XML 函数错误。
- 数据库函数错误。
它的价值在于:即使页面不正常展示查询结果,数据库错误也可能泄露信息。
你需要理解:
- SQL 语法错误与运行时错误的区别。
- 错误信息是否被应用程序返回。
- 数据库错误是否包含查询内容或数据。
- 不同数据库错误行为的区别。
注意,现代应用程序经常会隐藏数据库错误,所以报错注入不一定可用。
3. Blind SQLi(盲注)
数据库可能已经执行了注入,但页面不会直接显示查询结果。
我们只能通过其他信号判断。
主要有三种。
A. Boolean-based Blind
通过页面响应差异判断条件是否成立。
例如:
SELECT * FROM usersWHERE username = 'admin';假设我们想判断密码第一个字符是否为 a:
SUBSTRING(password, 1, 1) = 'a'如果页面出现正常响应,可能说明条件成立。
如果页面出现另一种响应,可能说明条件不成立。
实际利用流程:
判断字符串长度 ↓判断第一个字符 ↓判断第二个字符 ↓逐字符提取 ↓还原目标字符串B. Time-based Blind
如果页面响应没有明显区别,可以利用数据库执行时间作为侧信道。
以 MySQL 为例:
SELECT IF(1=1, SLEEP(5), 0);如果条件成立,就会触发延迟。
反过来:
SELECT IF(1=2, SLEEP(5), 0);不会触发这次延迟。
注意:网络波动、服务器负载和应用程序超时都会影响判断,所以不能只凭一次请求认定成功。
C. OAST-based Blind
OAST 是 Out-of-band Application Security Testing。
它的思路是:
当页面没有结果、没有错误、也没有明显时间变化时,尝试观察数据库是否能通过外部 DNS 或 HTTP 请求产生可控的带外信号。
需要了解:
- 带外通道的概念。
- DNS 查询。
- HTTP 外部请求。
- 数据库是否具备相关网络能力。
- 实验环境的网络限制。
4. Stacked Queries(堆叠查询)
正常情况下:
SELECT * FROM users;堆叠查询允许在某些数据库和驱动配置下执行多个 SQL 语句:
SELECT * FROM users;SELECT * FROM products;这里需要特别注意:
不是所有数据库、驱动和查询接口都支持堆叠查询。
例如,不同语言的数据库驱动对多语句执行的支持就可能不同。
5. Second-order SQLi(二次注入)
这个概念非常值得理解。
假设一个网站:
- 用户注册时保存了用户名。
- 之后管理员进入后台查看用户信息。
- 后台把保存的用户名拼接到 SQL 中执行。
此时,用户输入并没有在注册时立即造成注入,而是在后续使用时才触发。
这就是二次注入。
它说明:
输入数据被保存下来,并不意味着它已经安全。
五、payload构造技巧
| 技巧 | 解决的问题 |
|---|---|
| 字符串闭合 | 结束原有字符串 |
| 括号闭合 | 结束原有表达式 |
| 逻辑运算 | 改变查询条件 |
| 注释截断 | 处理后续 SQL |
| UNION 查询 | 合并其他查询结果 |
| 编码与语法适配 | 处理传输和数据库差异 |
1. 字符串闭合
原始 SQL:
WHERE username = 'INPUT'思考:
- 输入前面是否有单引号?
- 后面是否还有单引号?
- 输入能否结束当前字符串?
- 是否还有其他语法需要闭合?
2. 括号闭合
原始 SQL:
WHERE (username = 'INPUT')这里除了字符串之外,还有一个括号。
因此,你需要考虑 SQL 最终是否仍然满足括号匹配。
3. 注释截断
原始 SQL:
SELECT * FROM usersWHERE username = 'INPUT'AND active = 1;即使你成功闭合字符串,后面仍然存在:
AND active = 1因此需要考虑后续语句如何处理。通常直接注释掉即可。
部分信息可能已经过时









