Less 4 GET - Error based - Double Quotes - String
从题目看是get型的基于报错的双引号字符型注入
先打开第四关
先添加单引号看看是什么类型的注入
可是这一次输入单引号居然也查询出来了,这是怎么回事呐?
又想到题目说的双引号注入,尝试添加双引号试试
返回的错误信息是:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '"1"") LIMIT 0,1' at line 1
主要错误就是"1"") LIMIT 0,1
此处多了一个双引号导致sql语句无法闭合,可以猜测该页面查询的sql语句应该是
Select * from users where id=(“1”) limit 0,1
所以添加双引号之后不成对,就报错了。
但是为什么之前添加单引号没有影响查询的结果呐?
因为在双引号中间的单引号是不会影响结构的闭合的,但是为什么查询id=1’返回的结果也是id=1的结果呐?总不可能表中有一行的id值就是1’吧。
这里我用本地环境演示一下到底是怎么回事吧
可以看到本地查询什么就返回的什么,但是当进行比较的时候就有点意思了
可以看到这里居然判断的是’1abc’等于1。然后再次测试一下
这次判断的是’2abc’是不等于1的。所以看来在字符串和数字进行比较的时候,只是拿了字符串最前面数字来进行比较。即使是’abc1’这样的字符也是不行的。
因此就可以理解为什么在后面添加单引号的时候跟正常返回效果是一样的,因为在将id=1’与数据表中的id字段进行匹配的时候也只是把前面的1拿来进行了比较的。
既然知道了这里查询语句的构造,我们就可以闭合前面的(“并且注释掉后面的语句实施注入攻击了。
首先还是判断当前表的字段数
http://www.bj.com/sqli-labs/Less-4/index.php?id=1") order by 3 --+
order by 3的时候正常返回,order by 4的时候就出错了
至此后面的步骤都是一样的了,判断显示位、查询数据库的信息、查询表名列名以及字段名、查询用户名和密码等。
http://www.bj.com/sqli-labs/Less-4/index.php?id=-1") union select 1,2,group_concat(username,':',password) from security.users --+
Less 5 GET - Double Injection - Single Quotes - String
从题目来看的话,是get型的单引号字符型双查询注入。
先打开第五关构造参数进行访问
页面返回似乎不一样了,换一个参数访问试试
可以看到页面依然是这样,似乎没有显示位啊。不管了,既然传递了参数,那么先看看是什么类型的注入吧
首先输入单引号测试
可以看到报错了,报错信息是
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''1'' LIMIT 0,1' at line 1
从报错信息来看的话,就是多了一个单引号。而且这里sql查询语句的格式好像跟第一关是一样的
select * from users where id=’1’ limit 0,1
可是问题就来了,这里虽然是将参数带入查询了,但是页面没有显示位,我们怎么来显示后续的表名、列名字段名这些内容呐?仅仅依靠报错内容吗?
没错,当页面没有明显的显示位的时候还真就可以靠报错信息来注入了。
根据这一关的题目,我们基本可以知道考察的是双查询注入。那么什么是双查询注入呐?
在此之前,我们理解一下子查询,查询的关键字是select,这个大家都知道。子查询可以简单的理解为在一个select语句里还有一个select。里面的这个select语句就是子查询。比如
此处的子查询就是内部的select user()
在数据库执行这条语句的时候,首先执行了子查询得到了结果root@localhost。
然后又将这个结果带入了concat()函数。这个时候外层的select查询语句开始执行,所以就将字符串’this is’和root@localhost拼接到了一起作为结果返回。
要理解什么是双查询注入的话,首先需要了解以下四个函数(语句):
1. rand() //随机函数
2. floor() //取整函数
3. count() //汇总函数
4.group by clause //分组语句
1、rand()函数
调用该函数可以在0和1之间产生一个随机数。比如
当给rand()函数赋一个整数值的话,每一次调用都会生成一个固定不变的值。
2、floor()函数
该函数是用来向下取整的,比如
3、count()函数
该函数就是用来计数的函数,通常同来计算数据表中的行数
可以看到两次查询返回的数据行数是相符合的。
4、group by 语句
group by的常规用法是配合聚合函数,利用分组信息进行统计。
假设有一个名为user_info的表,结构如下:
当执行select max(user_id),grade from user_info group by grade ;
返回的结果是
这条语句的意思也很明显了,将数据按照grade字段分组,查询每组最大的user_id以及当前的grade的内容。注意,这里分组条件是grade,查询的非聚合条件也是grade。这里是不产生冲突的。
讲到这里,我想读者应该对这四个函数或者语句都有了一定的了解了。
那么双查询注入跟这四个有什么关系呐?
简单来说就是当在一个聚合函数,比如count函数后面如果使用分组语句就会把查询的一部分以错误的形式显示出来。
不理解?我们先来看一个简单的sql语句
select floor(rand()*2);
从上面对这两个函数的介绍来看,rand()会生成一个0到1之间的随机数,那么乘以2之后肯定就是0到2之间的这么一个随机数啦。外面又用floor()函数向下取整之后返回的结果无非就只有0或者1两个值了
接下来再试试复杂一点的语句
select concat((select user()), floor(rand()*2));
结果我想大家有可以推断出来了,无非就是root@localhost0或者root@localhost1了。
那么如果我们把这条语句后面加上from 一个表名。那么一般会返回root@localhost0或root@localhost1的一个集合。结果集的行数则是由表本身有几条结果决定的。比如这里users表里有13条数据,所以返回了13条内容
现在我们准备再加上group by 语句了。我们使用information_schema.tables 或 information_schema.columns这两个表来查询。因为表里面一般数据很多。容易生成很多的随机值,不至于全部是 root@localhost0,这样就无法体现出效果了。
select concat((select user()), floor(rand()*2))as a from information_schema.tables group by a;
这条语句的意思就是先把
concat((select user()), floor(rand()*2))这个结果取了一个别名为a,然后使用a进行分组。这样相同的root@localhost0分到一组,root@localhost1分到一组。就剩下两个结果了。
最后的亮点来了。。
我们现在再添加一个聚合函数count(*)
select count(*), concat((select user()), floor(rand(0)*2))as a from information_schema.tables group by a;
该语句的意思就是对分组的结果还要进行计数。
但是可以看到执行却报错了,而且在报错信息中还夹杂着我们查询的结果。
我们稍后利用这个特性来盲注的时候也就是依靠这个报错信息来获取我们需要的内容。
现在先解释一下为什么会报错,因为正常来看的话这里执行应该是会返回root@localhost0和root@localhost1以及两个分别出现多少次的计数。
下面我就用一个简单的例子来解释一下group by 和conut函数的具体工作过程。
假设我们已经有一个名为user的表,结构如下:
执行sql语句
select count(*) ,name from users group by name;
针对于进行了分组运算的时候会根据name属性,创建一个虚拟表。执行过程是先从上至下扫描,当扫描到第一行NAME === AA 的时候,因为虚拟表刚刚建立,当前虚拟表肯定是没有AA这个记录的,那么就将AA插入此虚拟表,并且计数count = 1。
然后向下扫描又是AA,因为当前虚拟表已经存在AA了,那么直接count + 1
当扫描到第三行 NAME === BB 的时候,当前虚拟表不存在该记录,所以会插入这条记录,并且计数count = 1。此时的虚拟表就是
到这里我们就明白了group by和count函数的基本工作过程了。那么为什么会报错呐?
因为注意观察的话会发现,其实floor( rand( 0 ) * 2) 这个函数的返回值是有规律可循的,就是01101100…
再来看刚才的执行语句
select count(*), concat((select user()), floor(rand(0)*2))as a from information_schema.tables group by a;
第一次查询返回的值是root@localhost0,这里floor函数执行了第一次,a别名的键值也就是root@localhost0了。第一次发现虚拟表中是没有root@localhost0这条记录的,所以需要插入。此时在需要确定分组的插入值时group by a中a又执行了一遍,也就是这里floor函数第二次执行了。那么返回的值当然就是root@localhost1了,所以实际上在虚拟表中添加的第一条记录是root@localhost1,并且计数count=1
接着查询第二次返回的值是root@localhost1,这是floor函数执行的第三次,因为虚拟表中已经有了root@localhost1这条记录,所以就直接count+1。因为是已经存在的记录,所以不需要通过group by a来确定插入的分组的值,其中的floor函数也就不需要执行了。
最后第三次查询返回的值则是root@localhost0,这是floor函数第四次执行。因为虚拟表中没有这条记录,所以是需要插入这条记录的,这时group by a需要执行一次以确定插入的分组的值。这时floor第五次执行返回的值则是root@localhost1,虚拟表中需要插入这条记录,但是记录已经存在了。而且mysql中键值是具有唯一性的,再插入一个一模一样的进来不就破坏了这个特性了吗?因此就会产生报错了。
此处还要说明一点的就是为什么使用的是rang(0)*2而不是rand()*2呐?
之前也说了,rang()是产生一个0到1的随机数,当给定了一个值的时候产生的就是一个固定的值了,也可以叫做伪随机数(产生的数据是可以预知的)。先看看例子吧
可以看到两次查询的结果都是固定的随机数,所以也就叫做伪随机数了,再看看用rand()来查询
可以明显看到结果都是完全不一样的,这才是真正的随机数。
所以我们想要在可以预测的序列中使数据库因为上述原理报错,使用rand(0)更加稳妥,使用rand()具有一些随机性,虽然最终也是可以达到这一目的。
了解了floor()和rand()结合报错的原理再回到这一关上面来看,双查询就是指select count(*), concat((select user()), floor(rand(0)*2))as a from information_schema.tables group by a;
基于报错也就是基于这里将的这种报错方法了。那么接下来就该注入了。
首先我们要查询当前链接数据的用户名
http://www.bj.com/sqli-labs/Less-5/index.php?id=1' and (select count(*) ,concat( (select user()),floor(rand(0)*2))a from security.users group by a) --+
这里为啥出错了呐?
因为前面提到过了,select中通过group by 分组返回的是一个结果表,而and后面是需要进行逻辑判断的,主要的是true和false这样的布尔值。所以这里既然返回的是表,那么再对这个表进行一次查询并返回一个布尔值不就行了吗,再次构造查询语句。
http://www.bj.com/sqli-labs/Less-5/index.php?id=1' and (select 1 from (select count(*) ,concat(user(),floor(rand(0)*2))a from information_schema.tables group by a)b) --+
因为内部的查询出错了,再次查询select 1也必然返回的是false这样的布尔值。与程序正常的查询做逻辑运算当然就是false了。
之后的步骤就很清楚了
http://www.bj.com/sqli-labs/Less-5/index.php?id=1' and (select 1 from (select count(*),concat((select group_concat(table_name) from information_schema.tables where table_schema='security'),floor (rand()*2)) as a from information_schema.tables group by a) as b) --+
但是当我要查询用户名和密码时却又报错了
http://www.bj.com/sqli-labs/Less-5/index.php?id=1' and (select 1 from (select count(*),concat((select group_concat(username,password) from users),floor (rand()*2)) as a from information_schema.tables group by a) as b) --+
提示输出信息超过一行,但是group_concat()返回的就是一个字符串应该也不存在返回多行数据的情况,这里具体原因还没有搞懂。所以尝试使用limit 0,1来一个个输出。
limit 0,1 表示输出第一个数据。 0表示输出的起始位置,1表示跨度为1(即输出几个数据,1表示输出一个,2就表示输出两个)
http://www.bj.com/sqli-labs/Less-5/index.php?id=1' and (select 1 from (select count(*),concat((select concat(username,': ',password,';') from security.users limit 0,1),floor (rand()*2)) as a from information_schema.tables group by a) as b) --+
floor()和rand()组合报错只是报错注入方法中的一种,还可以进行报错注入的就是updatexml()和extractvalue()以及exp()等方法,这三种将在下一遍文章中进行讲解。