0x00 废话

虽然上一节说了我们要从Less-1开始逐个分析,但是很多相近的原理完全相同的我就跳过了。怎么可能真的一个一个的来,我没讲的大家就当做练习吧,不然我全讲了也没意思了。

0x01 先来体验一下使用工具的快感

鉴于这个SQLi-labs有日志的功能,我决定先给大家介绍一个杀器,再配合日志功能,这样就可以自己学习了。这款工具就是SQLMAP,官网地址:http://sqlmap.org。根据官方的介绍,这是Automatic SQL injection and database takeover tool,并不是扫描器,这一点很重要!

SQLMAP 不会扫描 注入点,它只是一个自动化的注入工具。也就是说,你需要给他一个确实存在注入点的URL才可以正常工作。当然某些时候也可以将你不确定是否有注入点的URL丢给他,让SQLMAP帮助你判断。这是一个相当牛逼也相当复杂的工具,各种参数选项十分的多,详细的用法 十分建议 阅读官方的文档,我曾经读过一遍,对整个工具的熟练度提升了不少,很多以前认为SQLMAP无法做到的事情其实都可以做到的。

这个工具是基于PYTHON的,所以运行之前请大家安装好python并设置好环境变量。在SQLMAP的根目录下你会看到一个sqlmap.py,我们执行python sqlmap.py就可以运行他了。

我们先通过SQLMAP来查看当前使用的是什么数据库,我们输入下面的命令:

python sqlmap.py -u "http://127.0.0.1/sqli-labs/Less-1/?id=1" -p id --current-db

解释一下参数:

  • -u 参数指定了注入的URL
  • -p 参数指定了注入的参数,这里指定了id,也就是通过URL中的id参数注入,遇到多参数的URL时候这个-p就十分有用了。
  • --current-db,让SQLMAP列举出当前的数据库名称
  • -v 这个参数这里并没有使用到,这个参数会改变输出log的详细程度,一般默认即可,如果是学习或是想查看sqlmap使用的payload,可以使用-v 3参数。
  • --level 这个很有用咯,我个人理解是提高检测的等级,也就是说level 3会比level 1使用更多的payload进行测试。

之后我们根据SQLMAP的提示一路操作即可,应该比较容易看懂。等待一会儿后我们可以看到如下的界面:

我们需要注意其中的三个部分,第一部分SQLMAP列出了可用的payload,中间第二部分白色灰色字的部分列出了SERVER的部分信息,最后的一部分是我们需要的内容,也就是--current-db得到的结果,可以看到当前使用的数据库是security

现在我们打开Less-1文件夹下的result.txt文件,看到了什么?

对,那就是刚刚sqlmap尝试过的各种PAYLOAD,我们可以仔细研究这些payload,然后总结出攻击方法。

关于SQLMAP更多用法我这里就不讲了,网上又很多关于SQLMAP的用法,大家可以自行百度。

0x02 显错注入是什么鬼?

我们回顾一下,如果在后面多加一个单引号或者其他什么的符号,让其括号不匹配,从而显示出数据库错误。我们通常将这种显示数据库错误的注入类型称为 显错注入。如果你想装逼,还有个逼格更高的名字更适合你: Error-Based SQL Injection

对于注入,我们最感兴趣的就是从数据库里提取各种各样的数据了,显错注入的核心原理就是在SQL语句中造成错误,然后将我们感兴趣的内容通过错误显示出来。举个不是很恰当的例子,用户询问数据库,“嘿!把你的当前用户名转成数字类型告诉我!”,数据库突然觉得很蛋疼,因为用户名是字符串啊,这根本就没办法转换。于是笨笨的数据库就进行回复:“嘿!我不能把用户名root转换成数字型!”

看看我们的到了啥?那就是数据库的用户名,显错注入也正是这个原理,通过构造错误的SQL语句,从而将我们想要的信息显示出来。

0x03 从Less-1开始吧

首先我们先来搞出来当前的数据库用户名吧。

来访问这个东西试试:http://127.0.0.1/sqli-labs/Less-1/?id=1' AND (SELECT 5648 FROM(SELECT COUNT(*),CONCAT(0x7171,(MID((IFNULL(CAST(DATABASE() AS CHAR),0x20)),1,50)),0x7171,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a) AND '1'='1

这显示的都是什么东西啊?我们来分析吧,不要被这长长的一串payload吓到了,把他分解开来分析还是很简单的。首先1'是为了闭合前面的一个单引号,最后的AND是为了闭合后面的单引号,如果不理解的话想想第三节讲的内容。

下面把两个AND之间的内容抽出来:

SELECT 5648 FROM(SELECT COUNT(*),CONCAT(0x7171,(MID((IFNULL(CAST(DATABASE() AS CHAR),0x20)),1,50)),0x7171,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a

头和尾的括号就不要了,分析的时候看起来反而不爽,你觉得爽的话就留下吧。

分析的方法有两种,一种是从外向里的分析,另一种是从里向外的分析,我们先从外向里的分析吧。纵观全局,这是个SELECT * FROM xxx的语句。就是SELECT 5648 FROM(XXXX)a,a就是个别名吧,可以理解为将XXXX的结果作为一个表a来使用。现在我们提取出里面的东西。

SELECT COUNT(*),CONCAT(0x7171,(MID((IFNULL(CAST(DATABASE() AS CHAR),0x20)),1,50)),0x7171,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x

很容易看出来还是个SELECT FROM语句,从INFORMATION_SCHEMA.CHARACTER_SETS这个表里去SELECT,最后还要GROUP BY x,这个x就是另一个子语句的别名,稍后会讲到。我们可以看到这条语句总共SELECT了两个个东西,分别是COUNT(*),CONCAT(XXX),其中CONCAT出来的东西有个别名叫做x,就是最后GROUP BY用到的。

下面就是要分析最核心的CONCAT语句了。

CONCAT(0x7171,(MID((IFNULL(CAST(DATABASE() AS CHAR),0x20)),1,50)),0x7171,FLOOR(RAND(0)*2))

CONCAT的作用是连接这几个参数,这几个参数分别是:0x7171MID(xxx)0x7171FLOOR(xxx),至于为啥要有0x7171呢,这个参数其实可有可无的,加上的话主要是为了容易分辨,关键的地方在FLOOR,我们稍后分析。核心的部分在MID语句。继续抽出来。

MID((IFNULL(CAST(DATABASE() AS CHAR),0x20)),1,50)

其中MID的函数作用是为了截取字符串,这里的是从第1位开始,并截取50位,注意这里的字符串是从1开始的。

IFNULL的作用其实是判断所给的东西是否为NULL,如果是NULL的话就返回第二个参数,否则返回第一个参数。这里就是判断CAST(DATABASE() AS CHAR)是否为NULL,如果为NULL就返回第二个参数0x20

至于CAST嘛,把他当做类型转换就好了,现在该来说刚才没讲的FLOOR部分了。FLOOR(RAND(0)*2),这个RAND就是随机函数,没什么好说的。但是为何要在这里莫名的加个RAND呢?还记得这CONCAT出来东西是x吗?是的,最后的GROUP BY还用到了。在MYSQL手册里有这样的一段描述:

RAND() in a WHERE clause is re-evaluated every time the WHERE is executed.
You cannot use a column with RAND() values in an ORDER BY clause, because ORDER BY would evaluate the column multiple times.

没错,我们利用的就是RANDGROUP BY的冲突会报错这个特点。根据文档中描述,RAND()是不可以用在ORDER BY中的,其实同理,也不可以用在GROUP BY中的。

现在我们从头来理解一下这个payload,整个payload的核心是利用RANDGROUP BY的冲突报错来实现的,而具体想要读取什么内容则在CONCATMID中,此部分语句的灵活性很强,对于读取一个数据有很多种不同的实现方法,大家可以自己研究,不一定是按照我这里的payload,这个payload是从SQLMAP的截取出来的,比较经典。

可能有人会问这个FLOOR函数的问题,FLOOR函数是为了取整,具体作用是为了取到不确定的可重复数。大家可以试一下,FLOOR(RAND(0)*2)的结果,我本机测试的时候结果永远是0,但是因为RAND()又增加了随即性与GROUP BY冲突,所以会出现Duplicate entry的错误,当然你*3,*4,*999都是可以的。

另外,count(*)其实也是必须的,没有了COUNT(*)也不会报错。

0x04 讲了好多我特么都记不住了

没错,讲了这么多确实记不住了,所以我建议平时可以收集各种payload记到笔记里,用的时候拿出来稍加修改即可。当你遇到越来越多的情况以后,就会理解的越深入,自然会理解这些复杂的payload。遇到WAF的时候也需要在这个payload的核心基础上进行各种修改达到绕过的目的,在这个过程中就会增加你对SQL注入的理解。