本文的Bypass WAF 的核心思想在于,一些 WAF 产品处于降低误报考虑,对用户上传文件的内
容不做匹配,直接放行
0、环境
**环境:**两台服务器,一台配置宝塔面板,一台配置长亭雷池WAF
思路主要围绕:multipart/form-data
,也就是主要针对于 POST 参数,对于漏洞点在 GET 参数位置则用处不大。
我们知道,HTTP 协议 POST 请求,除了常规的application/x-www-form-urlencoded
以外,还有multipart/form-data
这种形式,主要是为了解决上传文件场景下文件内容较大且内置字符不可控的问题。multipart/form-data
格式也是可以传递 POST 参数的。对于 Nginx+PHP 的架构,Nginx 实际上是不负责解析multipart/form-data
的body部分的,而是交由 PHP 来解析,因此 WAF 所获取的内容就很有可能与后端的 PHP 发生不一致。
1、multipart/form-data特性
以 PHP 为例,我们写一个简单的测试脚本:
<?php
echo "POST Content:\n";
echo file_get_contents("php://input");
echo "\n\n";
echo "\$_POST Content: \n";
var_dump($_POST);
echo "\n\n";
echo "\$_FILES Content: \n";
var_dump($_FILES);
?>
先使用BP来进行一些测试,以便了解一些特性
这是在Content_Type:application/x-www-form-urlencoded
情况下的一个request和response
然后,在观察一下Content-Type:multipart/form-data
的情况:
参数并没有进入_FILES 数组,而是进入了_POST 数组。
可以发现将Content-Type改成传输文件,并且在没有filename参数的情况下,我们是可以将文件内容传输为POST参数的。而正常情况下,传输文件还需要有一个filename参数,有了个参数,才会将文件内容装载成文件
那么,何时是上传文件?何时是 POST 参数呢?这个关键点在于有没有一个完整的 filename=。这 9 个字符是经过反复测试的,缺一个字符不可,替换一个字符也不可,在其中添加一个字符更不可。
加上了 filename=以后的效果:
Bypass WAF 的核心思想在于,一些 WAF 产品处理降低误报考虑,对用户上传文件的内容不做匹配,直接放行。事实上,这些内容在绝大多数场景也无法引起攻击。但关键问题在于,WAF 能否准确有效识别出哪些内容是传给_POST 数组的,哪些传给_FILES 数组?如果不能,那我们是否就可以想办法让 WAF 以为我们是在上传文件,而实际上却是在 POST 一个参数,这个参数可以是命令注入、SQL 注入、SSRF 等任意的一种攻击,这样就实现了通用 WAF Bypass。
小结:通过上传文件时,不使用filename=,来使文件内容传递给$_POST数组,通过这样就可以达到POST传参并且不会绕过WAF的目的
2、基础绕过
可以看到当我们进行sql注入时,是会对我们进行阻拦的
可以考虑某些绕过去进行单一规则绕过,但是进行到下一条语句时,很有可能需要重新去尝试绕过的规则。所以我们可以想象通用性绕过。
那么利用multipart/form-data
的特性来绕过,但是发现WAF还是检测到了。
尝试一下其他方法
0x00截断filename
在 filename 前加入了 0x00,因为有些 WAF 在检测前会删除 HTTP 协议中的 0x00、空格,这样就导致了 WAF 认为是含有 filename 的普通上传,而后端 PHP 则认为是 POST 参数。空格也可以试试
但是在长亭的雷池WAF中并没有绕过
双写上传描述行
双写描述行后,一些 WAF 会取第二行,而实际 PHP 会获取第一行。
在没有\t,00,20的破坏下,WAF取第二行,而PHP取第一行
可以看到使用双写描述行的方法成功绕过了WAF
双写整个 part 开头部分
原理与双写上传描述行类似,但是可惜的是并没有绕过
但是当两个part头中的空格去除时,就能成功
个人理解为有空格的情况下,那么这两个part头是分开的个体,而当空格去除时,这两个part头就合成一个part头了,只是它们的赋值语句重复了,那么就取决于赋值语句的选取第一个还是第二个
**需要注意的是,**如果双写part头能成功,那么就需要考虑它的一些垃圾数据的影响,因为该参数会引入一些垃圾数据,在命令注入及 SQL 注入的攻击场景,需要尽可能将前面的内容闭合。
构造假的 part 部分 1
原理都是相似的
可以看到POST内容的开始是由--a决定的,而结束是由--a--决定的,那么是否可以构造两个开始来影响WAF的决定呢?
通过测试可以发现雷池WAF取得是第二个,还是覆盖那一套。而php则是取得第一个
当然,php取得第一个开始,那么第二个开始是会成为垃圾数据,所以需要注意引号的闭合
构造假的 part 部分 2
与构造part部分1相比缺少了一个空行,但是我觉得这还是将两个开始合成一个开始,考虑的是对于语句的复写选择,但相比于构造1的好处是数据会纯净许多
两个 boundary
写两个boundary,思路是考虑WAF与PHP后端的选择差异
测试发现,PHP后端选择的是第一个boundary,那么思考的点就是WAF的取值是否是第二个
很遗憾,WAF的取值貌似也是第一个,并不能绕过。
有一点疑惑是PHP取得是第一个boundary,即a,那么WAF不管取得是第一个还是第二个,它的参数应该是都含有-1' union select user(),database()#
这个危险语句的。只是取一个是该语句,而取第二个只是包含该语句。因此在我看来这个方法无论如何都是无法成功
解决了,第二个boundary是具有filename的,也就是如果WAF取第二个那就不会检测内容
两个 Content-Type
与两个boundary类似,使用了两个Conten-Type
可以看到PHP取得还是第一个并且WAF也是第一个,所以没有成功
空 boundary
也就是说boundary并不设值,而结束符是分号;
,那么是否可以引起歧义。因为作为最后一个
参数,它有没有分号都是不影响的。
这边的原理是让WAF认为boundary为分号,而PHP是将boundary认为是空
很遗憾,并没有成功
空格 boundary
原理与空boundary类似,想让WAF认为是空
但是也没有成功
boundary 中的逗号
boundary遇到逗号,
结束,同理是否可以让WAF认为是a,b
或者以逗号,
,来让WAF认为分界为逗号,
,而实际是空值
失败
3、进阶绕过
0x00截断进阶
从前面可知,0x00对于一些WAF来说可能会先删除在检测,而PHP又是自动选择第一行,所以在基础的0x00截断中并没有成功将filename=截断
进阶的内容就是,当我们在适当的地方加入0x00、空格和\t后,就会破坏第一行,让PHP反以第二行为主
这三个位置为首选:替换成0x00、0x20同理
此外还有:
最容易被忽视的是参数名中的 0x00
由此测试还有一个十分鸡肋的方式,用处不大,但有意思。只有当网站获取全部 POST 数组
后以参数前缀来取值的场景才可利用,因为参数名后缀部分不可控。
在三个首选位置中,最后一个位置成功了
boundary 进阶
boundary 的名称是可以前后加入任意内容的,WAF 如果严格按 boundary 去取,又要上当了。
通过空格去破坏Content-Type,使PHP取第二个,WAF取一个。但是比较吃Nginx的版本
很可惜的是WAF并没有取后面的b
单双引号混合进阶
通过不规范的单双引号破坏,使PHP取第二个,WAF取第一个
WAF也取了第二个,失败
urlencoded 伪装成为 multipart
这个 poc 很特殊。实际上是 urlencoded,但是伪装成了 multipart,通过&来截取前后装饰部分,保留 id 参数的完整性。理论上 multipart/form-data 下的内容不进行 urldecoded,一些 WAF 也正是这样设计的,这样做本没有问题,但是如果是 urlencoded 格式的内容,不进行 url 解码就会引入%0a 这样字符,而这样的字符不解码是可以直接绕过防护规则的,从而导致了绕过。
也就是说,它想要通过urlencoded去传递参数,而传递的这三个参数构成了form-data的一个样子
由于版本限制,并不能直接按照上图做
猜测它的想法应该是想要利用WAF自动去补齐,然后去第二个Content-Type,那我直接补齐试试
很遗憾没有成功。
4、高阶绕过
skip_upload 进阶 1
在 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。
但是长亭WAF还是没有绕过
skip_upload 进阶 2
在 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 个就会出错了。即skip_upload
那么:
看看是否能绕过,还是失败了