Sql注入-4:报错注入
引言
对于一些网页开发者没有关闭sql报错信息回显的情况可以使用报错注入,通过网页回显的sql报错信息确定一些需要的数据。这与普通的语法报错不一样,普通的语法报错只会告诉我们sql语句语法的问题信息,而我们的报错注入想通过报错信息回显我们需要的数据,例如:数据库版本、名称,表明、字段名和用户名等等。下面详细介绍两种报错方式的根本原理。
Floor报错
我以前在学习报错注入的时候,发现网上对于floor报错的原理分析地并不是很清楚,只是大致地解说了报错原因为分组后数据计数时重复而造成的错误。本次笔记我会根据我的理解将floor报错的根本原因分析清楚。首先让我们看一个函数rand(),在mysql执行后发现返回了几行随机的数字,每一次执行结果都不一样(注:行数与users表里的行数对应)。
紧接着我们给rand()函数添加一个种子参数0,这时候会发现此时返回的多行随机数据每一次都一样(指的是每一次运行得到的多行数据一样,并不是单次每一行数据一样),如下图所示:
添加种子参数后,rand()函数变为了“伪随机”的概念,因为每一次随机的多行数值均是一样的,这是mysql的特性,无需过多纠结,了解就好。然后再让我们看一下floor函数,floor函数返回小于等于该值的最大整数,例如:floor(0.9273781)的结果为0,floor(1.2817841)的结果为1。那么我们现在构造这样一个表达式:floor(rand(0)*2),其中“*2”的含义为rand函数的值乘以2,这样的表达式首先会计算rand函数,返回永远一样的随机序列,然后将结果乘以2放入floor函数向下取整数,结果会得到一个永远固定的0,1序列组合,从图中可以看到这个固定的0,1序列组合为[0、1、1、0、1、1、0…]。
当我们了解了以上的概念后,就可以来看group by和count(*)函数了,学过sql的都知道group by经常与count一起用,group by指定一个参数进行分组,count计算该分组下的数据个数,如:需要查询某个班男性和女性同学分别及格的人数,就可以使用这样的语句:
Select count(*) from [学生表] where [分数字段]>=60 group by [性别字段]
接下来我们来看一个floor报错的常用语句:
Select 1, count(*), floor(rand(0)*2) as a from users group by a
我们来分析一下这一语句,首先“as a”是将floor整个表达式取别名为a,然后从users表中每一行查询三个值,第一个值为固定值1,第二个值为以a分组后的每组个数,第三个为floor“伪随机”产生的固定值序列,放入sql执行后显示错误。
这就是我们想达到的原始报错效果(下文会讲如何把需要的数据涵盖进报错信息从而回显得到数据),那么为什么会出现这样的问题呢?因为group by和count(*)的合作机制很特殊,导致了内部产生了bug,让我们来一层层分析。
当group by和count一起使用执行时,mysql会建立一张虚拟表,这张虚拟表有两个字段,一个是key,一个是count(*),此时虚拟表无任何值,mysql先计算group by后面的值,也就是floor表达式,上文提到了,固定序列的第一个值为0,mysql查询虚拟表,发现没有key为0的记录,因此将此数据插入,这时特殊的地方就来了,mysql插入的过程中还会计算一次group by后面的值,也就是floor表达式,但是此时floor的结果为固定序列的第二个值,也就是1,因此插入的key值为1,count(*)置1,以上过程可以简单理解为,mysql判断是否有key时计算了一次floor的值,真正插入时又计算了一次floor值,若判断结果有key,直接count(*)加1,不会再重复计算一次group by后的值。
key |
count(*) |
1 |
1 |
|
|
|
|
紧接着mysql会继续查询下一条数据,若发现重复的key,就count(*)加1,若没有找到key,则添加新key值,mysql遍历users表中第二行,计算floor的值,此时为固定序列的第三个值,也就是1,查询虚拟表,发现key为1的记录,因此count(*)加1,到此结束。
key |
count(*) |
1 |
2 |
|
|
|
|
然后遍历users的第三行数据,计算floor值,此为固定序列的第四个值,也就是0,查询虚拟表,发现key为0的记录不存在,因此执行插入,此时又计算了一遍floor的值,为固定序列的第5个值,也就是1,于是执行插入key值1,但因为key中包含了1的记录,因此这个时候执行插入直接报错。
key |
count(*) |
1 |
2 |
1 |
|
|
|
以上就是利用floor报错的根本原理了,需要注意的是:由上面的原理可见利用floor(rand(0)*2)报错需要数据表里至少存在3条记录,固定序列的执行过程梳理见下表。
固定序列 |
遍历行数 |
执行情况 |
0 |
第一行 |
判断是否有key:0 |
1 |
第一行 |
计算并插入新key:1 |
1 |
第二行 |
判断是否有key:1 |
0 |
第三行 |
判断是否有key:0 |
1 |
第三行 |
计算并插入新key:1,报错 |
既然知道了原理,那我们就可以利用这个报错信息回显我们需要的数据了,比如将数据库名与floor函数值拼接就可以报错得到数据库名信息,其他需要的数据也是通过种思路,这里就不一一列举了,以后的笔记会记录各种信息数据获取的方式。下面的语句中concat函数的作用是将括号里的内容拼接。
Select 1, count(*), CONCAT(database(), floor(rand(0)*2)) as a from users group by a
Extractvalue报错
还有一种常用的报错方式是extractvalue函数,该函数的含义为从某一个xml文档中查询某一个特定的关键词,函数接受两个参数,第一个参数为待查询的关键词,第二参数为xml文档的路径,合法的xml路径类似/…/…/…/…这样的格式,若查询不到,mysql也不会报错,但若我们在第二个参数里填写错误格式的路径,则mysql会报错,同理,我们拼接需要回显的数据信息,报错信息就会包含我们需要的数据。一般常用“~”和我们需要的数据进行拼接,得到错误的xml文档路径格式,从而达到报错回显数据,需要注意的是:这里的select database()外面还需要一个括号,不然会有语法错误。
select extractvalue('anything', concat('~', (select database())))
小结
本节介绍了两种报错注入的方式,还有其他很多报错注入的方式,例如updatexml()、double数据类型超出范围、bigint 溢出和xpath等等,有兴趣的可以自行百度。下面以sqli-labs中less-4演示上文讲解的两种报错注入获取数据库名称的方式。
Floor报错注入的payload如下:
?id=1") union Select 1, count(*), CONCAT(database(), floor(rand(0)*2)) as a from information_schema.COLUMNS group by a --+
其中浏览器将空格转义为%20,--为注释,+会在浏览器中转化为空格,从而在sql中成功注释掉后面的语句;select的三个参数,其中一个查询1的作用是因为该网页sql正常的执行会查询3个字段(后续章节会通过盲注来判断网页本身sql查询的字段数量),而union联合查询需要与前面的查询字段个数匹配,故构造payload时增加一个查询参数1,使得payload正确执行,这也是为什么上文讲解floor报错时用到那样sql语句的原因。
Extractvalue报错注入的payload如下:
?id=1") union select extractvalue('anything', concat('~', (select database())))
下一节将会介绍对于可以回显数据信息的网站(例如:登录后标示用户名称等)如何利用联合查询爆数据库,表和字段等。
感谢观看,理解有误之处,欢迎交流和指导!
刘杰寅
知乎|烈焰宝宝