关于WAF的那些事

0x01什么是waf

概念简述

首先WAF(Web Application Firewall),俗称Web应用防火墙,主要的目的实际上是用来过滤不正常或者恶意请求包,以及为服务器打上临时补丁的作用。

1、云waf:

在配置云waf时(通常是CDN包含的waf),DNS需要解析到CDN的ip上去,在请求uri时,数据包就会先经过云waf进行检测,如果通过再将数据包流给主机。常见产品:阿里云防护,腾讯云防护,创宇云之类等。

2、主机防护软件:

在主机上预先安装了这种防护软件,和监听web端口的流量是否有恶意的,所以这种从功能上讲较为全面。这里再插一嘴,mod_security、ngx-lua-waf这类开源waf虽然看起来不错,但是有个弱点就是升级的成本会高一些。常见产品:云锁,安全狗之类产品。

3、硬件WAF:

硬件WAF可以理解为流量代理,一般部署方式都需要流量经过它,针对数据包进行拆包->清洗->规则命中->放行/丢弃,当然现在更有甚者,给WAF上了一个头脑,采用深度学习,语义分析等操作,来减少本身WAF因为单调的规则导致可能被绕过的问题。常见产品:各产品铁盒子waf

为什么WAF可被绕过

  • 业务与安全存在一定的冲突。
  • WAF无法100%覆盖语言,中间件,数据库的特性。
  • WAF本身漏洞。

0x02 waf绕过方式

1、Web容器的特性

特殊的百分号

IIS+ASP 的环境中,对于URL请求的参数值中的%,如果和后面的字符构成的字符串在 URL编码表 之外,ASP脚本处理时会将其忽略。

假设现在有个url是:

1
http://www.test.com/test.asp?id=1 union se%lect 1,2,3,4 fro%m adm%in

再经过 IIS+ASP 中处理之后。

1

到达服务器上的实际是

1
http://www.test.com/test.asp?id=1 union all select 1,2,3,4 from admin

原理是因为在WAF层,获取到的id参数值为1 union all se%lect 1,2,3,4 fro%m adm%in,此时waf因为 % 的分隔,无法检测出关键字 select from 等。

但是因为IIS的特性,最后在服务器上解析的时候, id 获取的实际参数就变为1 union all select 1,2,3,4 from admin,从而绕过了waf。

PS:这个特性仅在iis+asp上 asp.net并不存在

unicode编码

IIS支持Unicode编码字符的解析,但是某些WAF却不一定具备这种能力。(已知 s 的unicode编码为: %u0053 , f 的unicode编码为 %u0066 )。

假设现在有个链接

1
http://www.test.com/test.asp?id=1 union %u0053elect 1,2,3,4 %u0066rom admin

当该请求到达WAF之后,WAF获取的数值是

1
1 union %u0053elect 1,2,3,4 %u0066rom admin

由于数据清洗等其他原因,WAF取出脏数据,进行规则匹配的时候,可能理解只是一个 union ,不会将其阻断,那么经过到达 IIS中间件 的时候, IIS 会做如下处理:

2

最后服务器和数据库最终获取到的参数会是:

1
1 union select 1,2,3,4 from admin

此方法还存在另外一种情况,多个不同的widechar可能会被转换为同一个字符。例如:WideChar和MultiByte字符转换问题

1
2
s%u0065lect->select
s%u00f0lect->select

PS:

其实不止这个,还有很多类似的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
字母a:
%u0000
%u0041
%u0061
%u00aa
%u00e2
单引号:
%u0027
%u02b9
%u02bc
%u02c8
%u2032
%uff07
%c0%27
%c0%a7
%e0%80%a7
空白:
%u0020
%uff00
%c0%20
%c0%a0
%e0%80%a0
左括号(:
%u0028
%uff08
%c0%28
%c0%a8
%e0%80%a8
右括号):
%u0029
%uff09
%c0%29
%c0%a9
%e0%80%a9

HPP(HTTP Parameter Pollution): HTTP参数污染

在HTTP协议中是允许同样名称的参数出现多次的。

例如:

1
http://www.test.com/test.asp?id=123&id=456

3

假设提交的参数即为:

id=1&id=2&id=3

1
2
3
Asp.net + iis:id=1,2,3 
Asp + iis:id=1,2,3
Php + apache/nginx:id=3

所以对于这类过滤规则,假设利用以下payload:

1
id=union+select+password/*&id=*/from+admin

来逃避对 select * from 的检测。因为HPP特性,id的参数值最终会变为:

1
union select password/*,*/from admin

4

上面的例子是一个ASP的,再举例一个PHP的例子,代码如下:

5

1、对于传入的非法的 $_GET 数组参数名,会将他们转换成 下划线 。经过fuzz,有以下这些字符。

5

2、php在遇到相同参数时接受的是第二个参数。

5

3、通过 $_SERVER[‘REQUEST_URI’] 方式获得的参数并不会对参数中的某些特殊字符进行转换。

5

这里的代码中有两个waf。

第一个WAF在代码 第29行-第30行 ,这里面采用了 dowith_sql() 函数,跟进一下 dowith_sql() 函数,该函数主要功能代码在 第19-第26行 ,如果 $_REQUEST 数组中的数据存在 select|insert|update|delete 等敏感关键字或者是字符,则直接 exit() 。如果不存在,则原字符串返回。

而第二个WAF在代码 第33行-第39行 ,这部分代码通过 $_SERVER[‘REQUEST_URI’] 的方式获取参数,然后使用 explode 函数针对 & 进行分割,获取到每个参数的参数名和参数值。然后针对每个参数值调用 dhtmlspecialchars() 函数进行过滤,跟进一下 dhtmlspecialchars() 函数,发现其相关功能代码在 第3行-第14行 ,这个函数主要功能是针对 ‘&’, ‘“‘, ‘<’, ‘>’, ‘(‘, ‘)’ 等特殊字符进行过滤替换,最后返回替换后的内容。

由于 第44行和第45行 代码我们可以看到,这题的参数获取都是通过 REQUEST 方式,因此我们来看个例子。

5

第一次 $_REQUEST 仅仅只会输出 i_d=2 的原因是因为php自动将 i.d 替换成了 i_d 。而根据我们前面说的第二个特性,出现了两个 i_d ,php会自动使用第二个变量覆盖第一个,因此第一次 $_REQUEST 输出的是2。

第二次 $_REQUEST 会输出 i_d=select&i.d=2 是因为 \$_SERVER[‘REQUEST_URI’] 并不会对特殊的符号进行替换,因此结果会原封不动的输出。

所以这题payload如何构造,我们可以先来看个思维导图。

5

  1. 我们通过页面请求 i_d=padyload&i.d=123
  2. 当数据流到达第一个WAF时,由于我们开始的第一个知识点已经介绍过了,php会将参数中的某些特殊符号替换为下划线。因此便得到了两个 i_d ,所以此时的payload变成了 i_d=payload&i.d=123
  3. 前面我们介绍了,如果参数相同的情况下,默认 第二个参数传入的值 会覆盖 第一个参数传入的值 。因此此时在第一个WAF中 i_d=123 ,不存在其他特殊的字符,因此绕过了第一个WAF。
  4. 当数据流到达进入到第二个WAF时,由于代码是通过 $_SERVER[‘REQUEST_URI’] 取参数,而我们前面开头的第三个知识点已经介绍过了 $_SERVER[‘REQUEST_URI’] 是不会将参数中的特殊符号进行转换,因此这里的 i.d 参数并不会被替换为 i_d ,所以此时正常来说 i.di_d 都能经过第二个WAF。
  5. 第二个WAF中有一个 dhtmlspecialchars() 函数,作用前面我们已经介绍过了,这里需要绕过它,其实很好绕过。绕过之后 i_d=payload&i.d=123 便会进入到业务层代码中,执行SQL语句,由于这里的SQL语句采用拼接的方式,因此存在SQL注入。

因此最后payload如下:

1
http://127.0.0.1/index.php?submit=&i_d=-1/**/union/**/select/**/1,flag,3,4/**/from/**/ctf.users&i.d=123

5

畸形HTTP请求

当前的HTTP服务依据的是RFC2616标准的HTTP请求,但是当我们向Web服务器发送畸形的,也就是并非这个标准的HTTP数据包的时候,由于Web服务器的一些兼容性的特性,会尽力解析这些畸形的数据包。但是如果Web服务器和WAF针对畸形数据包解析的差距,就可能会出现一些Bypass的情况。以下是正常的数据包:

1
2
3
4
5
6
7
8
9
10
11
POST /test.php?id=1%20union/**/select/**/1,2,3/**/from/**/admin  HTTP/1.1
Host: 127.0.0.1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: ECS[visit_times]=2
Connection: close

....

如果我们将数据包修改为这样:

1
2
3
4
5
6
7
8
9
10
L1nk3r /test.php?id=1%20union/**/select/**/1,2,3/**/from/**/admin 
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: ECS[visit_times]=2
Connection: close

....

由于 L1nk3r 并不是标准HTTP协议中的请求方法,也没有协议字段 HTTP/1.1 ,也没有 host 字段。如果是在 HTTP/1.1 协议中,缺少 host 字段一般会返回 400 bad request 。但是某些版本的Apache在处理这个请求时,默认会设置协议为 HTTP/0.9host 则默认使用 apache 默认的 servername ,这种畸形的请求仍然能够被处理。

如果某些WAF在处理数据的时候严格按照GET、POST等标准HTTP方法来获取数据,或者采用正则匹配的方式来处理数据,那么这个时候就有可能因为WAF和WEB服务解析的前后不对等,绕过了这个WAF。

2、 Web应用层的问题

编码绕过

通过一般WAF会针对传来的数据包中带有的编码进行一次解码工作,如果WAF不能进行有效解码还原攻击向量,可能导致绕过,常见编码如URL编码、unicode编码(IIS)、宽字节编码等。例如我们用 url 的二次编码,而经过一次 url 解码的之后,WAF可能无法识别出它是恶意的数据,当把该恶意数据放行到Web服务器上时,Web服务器会再一次解码,最后导致了WAF绕过的结果。

12

看个实际例子:

21

多数据来源的问题

一般来说Web服务器从三个位置来获取用户传入的数据:

  • 从GET中获取
  • 如果GET中没有,尝试从POST中查找需要的值
  • 若GET和POST中都没有,尝试从Cookie中获取想要的值。

典型的例子就如 ASPASP.NET ,这两种语言中的 Request 对象对于请求数据包的解析并没有按照RFC的标准来,一般开发者如果按照下面方式获取数据:

1
2
ID=Request('ID');
ID=Request.Params(ID);

会出现以下这种情况:

13

在PHP情况下,我们看到是通过 request 方式传入数据,而php中REQUEST变量默认情况下包含了 GETPOSTCOOKIE 的数组。在 php.ini 配置文件中,有一个参数 variables_order ,这参数有以下可选项目

1
2
3
4
; variables_order
; Default Value: "EGPCS"
; Development Value: "GPCS"
; Production Value: "GPCS"

这些字母分别对应的是 E: EnvironmentG:GetP:PostC:CookieS:Server。这些字母的出现顺序,表明了数据的加载顺序。而 php.ini 中这个参数默认的配置是 GPCS ,也就是说如果有 POST 方式传入相同的数组,就覆盖掉 GET 方式传入的。

14

我们看个简单的例子

15

那个利用这个特性呢,实际上也可以bypasswaf,我们看下图中的例子:

19

3、WAF自身的问题

白名单机制

WAF存在某些机制,不处理和拦截白名单中的请求数据:

1、指定IP或IP段的数据。

2、来自于搜索引擎爬虫的访问数据。

3、其他特征的数据。

以前某些WAF为了不影响站点的SEO优化,将User-Agent为某些搜索引擎(如谷歌)的请求当作白名单处理,不检测和拦截。伪造HTTP请求的User-Agent非常容易,只需要将HTTP请求包中的User-Agent修改为谷歌搜索引擎的User-Agent即可畅通无阻。

数据获取方式存在缺陷

1、某些WAF无法全面支持GET、POST、Cookie等各类请求包的检测,当GET请求的攻击数据包无法绕过时,转换成POST可能就绕过去了。或者,若POST以 Content-Type: application/x-www-form-urlencoded 无法绕过时,转换成上传包格式的 Content-Type: multipart/form-data 也许就可以绕过了。

18

当然关于文件上传的话,可以试试将关键字换行分离试试看。下面看个例子:

17

对于网站来说,这样写是可以解析的,但是站在WAF的设计者的立场,他们可能并不知道,这个是可以这样写的。当用正则表达式去获取上传的文件名时,正则表达式就匹配不到了,所以上传就被绕过了。

2、某些WAF从数据包中提取检测特征的方式存在缺陷,如正则表达式不完善,某些攻击数据因为某些干扰字符的存在而无法被提取,常见的如%0a、%0b、%0c、%0d、%09、%0a等。之前某论坛流传的一个fuzz过某狗的脚本,代码主要如下:

16

主要上面代码中有两个关键fuzz的内容, fuzz_zsfuzz_ch ,其实主要来说还是利用mysql的一些特性,bypass一些正则表达式针对关键字的检查,例如 unionselect 之类的。

数据处理不恰当

1、%00截断

对于 %00 进行URL解码,实际上解码出来的就是C语言中的NULL字符,如果WAF对获取到的数据存储和处理不当,那么 %00 解码后会将后面的数据截断,造成后面的数据没有经过检测。

20

WAF在获取到参数id的值并解码后,参数值将被截断成1/*,因此没有命中规则,从而放过了。

2、&字符处理

某些WAF在对HTTP请求数据包中的参数进行检测时,使用 & 字符对多个参数进行分割,然后分别进行检测,如:

1
http://www.test.com/1.php?p1=1&p2=2&p3=3

这些WAF会使用&符号分割p1、p2和p3,然后对其参数值进行检测。

1
2
3
p1=1
p2=2
p3=3

但是,如果遇到这种构造:

1
http://www.test.com/1.php?p1=1+union/*%26x1=1*/+select/*%26x2=1*/1,2,3+from+admin

WAF会将以上参数分割成如下3部分:

1
2
3
p1=1+union/*
x1=1*/+select/*
x2=1*/1,2,3+from+admin

由于目标服务器就只有一个参数 p1 ,然后 x1x2 是不存在的, %26& 符号的 url编码 ,如果WAF针对上述的三个参数进行分别的检测,是不会报注入的。这里巧妙就巧妙在利用两个参数拼接注释符号将不存在的 x1x2 注释了。所以最后实际上进入数据库查询的语句也就只有。

1
p1=1+union+select1,2,3+from+admin

数据清洗不恰当

当攻击者提交的参数值中存在大量干扰数据时,如大量空格、TAB、换行、%0c、注释等,一般waf都是清洗之后再进行规则匹配,因为如果干扰字符串过多的话检测需要消耗大量的资源和性能,所以清洗后可以提升性能降低匹配规则的复杂度,筛选出真实的攻击数据进行检测,以提高检查性能,节省资源。如果WAF对数据的清洗不恰当,会导致真实的攻击数据被清洗,剩余的数据无法被检测出攻击行为。

例如:

1
http://localhost/test/Article.php?id=9999-"/*" union all select 1,2,3,4,5 as "*/" from mysql.user

由于waf会结合一些数据库的特性来清洗数据,对于/*来说,它只是一个字符串,对于*/来说,它也是一个字符串,更是一个别名,但是对于WAF来说,它会认为这是多行注释符,当waf把上面的payload清洗为

1
9999-"" from mysql.user

针对规则库进行匹配,如果没有命中规则,执行原始语句:

1
9999-"/*" union all select 1,2,3,4,5 as "*/" from mysql.user

数据库的注释一般是 # – /**/等。

规则通用性问题

通用型的WAF,一般无法获知后端使用的是哪些WEB容器、什么数据库、以及使用的什么脚本语言。每一种WEB容器、数据库以及编程语言,它们都有自己的特性,想使用通用的WAF规则去匹配和拦截,是非常难的。通用型WAF在考虑到它们一些共性的同时,也必须兼顾它们的特性,否则就很容易被一些特性给Bypass!

比如对SQL注入数据进行清洗时,WAF一般不能知道后端数据库是MySQL还是SQL Server。那么对于MySQL的/*!50001Select*/来说,这是一个Select的命令,但是对于SQL Server来说,这只不过是一个注释而已,注释的内容为!50001Select。看个例子:

1
9999' and 1=(select top 1 name as # from master.sysdatabases)--

经过waf之后会被当成,waf将后端数据库认为是Mysql,由于在Mysql中 # 是注释,经过数据清洗,无法命中规则

1
9999' and 1=(select top 1 name as

但是实际上,这里的 # 只是一个字符,充当一个别名的角色而已。如果后端数据库是SQL Server,这样的语句是没问题的。但是通用型WAF怎么能知道后端是SQL Server呢?

为性能和业务妥协

要全面兼容各类Web Server及各类数据库的WAF是非常难的,为了普适性,需要放宽一些检查条件,暴力的过滤方式会影响业务。对于通用性较强的软WAF来说,不得不考虑到各种机器和系统的性能,故对于一些超大数据包、超长数据可能会跳过不检测。

例如下图中的例子:

23

从容师傅分享的例子中有个非常暴力的方法,直接用大数据包打挂WAF。。。用这个注释中包含超长查询字符串,导致安全狗在识别的过程中挂掉了,连带着整个机器 Service Unavailable:

1
2
3
4
5
6
/*666666666666666666666666666666666666666666666666666666666666666 
66666666666666666666666666666666666666666666666666666666666666666
66666666666666666666666666666666666666666666666666666666666666666
66666666666666666666666666666666666666666666666666666666666666666
66666666666666666666666666666666666666666666666666666666666666666
66666666666666666666666666666666666666666666666666666666666666666 6666666666666666666666666666666666666666*/

24

本身缺陷

比如某WAF,默认情况下只能获取前100个参数进行检测,当提交第101个参数时,那么,将无法对攻击者提交的第100个以后的参数进行有效安全检测,从而绕过安全防御。( CVE-2018-9230

21

0x03 总结

感谢了二向箔的live,基于live基础上,针对自己已知和以前看到的总结了一下Bypass WAF。当然如果侵权的话,请联系我,我删除,这部分有自己的学习总结,也有别人的一些思路。当然自己也很感谢从容师傅的文章,让我之前能够bypass很多waf。

Refer

记一次文件上传 Bypass WAF实战

我的WafBypass之道(SQL注入)

Bypass 360主机卫士SQL注入防御(多姿势)