关于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是:
http://www.test.com/test.asp?id=1 union se%lect 1,2,3,4 fro%m adm%in |
再经过 IIS+ASP 中处理之后。
到达服务器上的实际是
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 )。
假设现在有个链接
http://www.test.com/test.asp?id=1 union %u0053elect 1,2,3,4 %u0066rom admin |
当该请求到达WAF之后,WAF获取的数值是
1 union %u0053elect 1,2,3,4 %u0066rom admin |
由于数据清洗等其他原因,WAF取出脏数据,进行规则匹配的时候,可能理解只是一个 union ,不会将其阻断,那么经过到达 IIS中间件 的时候, IIS 会做如下处理:
最后服务器和数据库最终获取到的参数会是:
1 union select 1,2,3,4 from admin |
此方法还存在另外一种情况,多个不同的widechar可能会被转换为同一个字符。例如:WideChar和MultiByte字符转换问题
s%u0065lect->select |
PS:
其实不止这个,还有很多类似的:
字母a: |
单引号: |
空白: |
左括号(: |
右括号): |
HPP(HTTP Parameter Pollution): HTTP参数污染
在HTTP协议中是允许同样名称的参数出现多次的。
例如:
http://www.test.com/test.asp?id=123&id=456 |
假设提交的参数即为:
id=1&id=2&id=3
Asp.net + iis:id=1,2,3 |
所以对于这类过滤规则,假设利用以下payload:
id=union+select+password/*&id=*/from+admin |
来逃避对 select * from 的检测。因为HPP特性,id的参数值最终会变为:
union select password/*,*/from admin |
上面的例子是一个ASP的,再举例一个PHP的例子,代码如下:
- 1、对于传入的非法的 $_GET 数组参数名,会将他们转换成 下划线 。经过fuzz,有以下这些字符。
- 2、php在遇到相同参数时接受的是第二个参数。
- 3、通过 $_SERVER[‘REQUEST_URI’] 方式获得的参数并不会对参数中的某些特殊字符进行转换。
这里的代码中有两个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 方式,因此我们来看个例子。
第一次 $_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如何构造,我们可以先来看个思维导图。
- 我们通过页面请求 i_d=padyload&i.d=123 。
- 当数据流到达第一个WAF时,由于我们开始的第一个知识点已经介绍过了,php会将参数中的某些特殊符号替换为下划线。因此便得到了两个 i_d ,所以此时的payload变成了 i_d=payload&i.d=123 。
- 前面我们介绍了,如果参数相同的情况下,默认 第二个参数传入的值 会覆盖 第一个参数传入的值 。因此此时在第一个WAF中 i_d=123 ,不存在其他特殊的字符,因此绕过了第一个WAF。
- 当数据流到达进入到第二个WAF时,由于代码是通过 $_SERVER[‘REQUEST_URI’] 取参数,而我们前面开头的第三个知识点已经介绍过了 $_SERVER[‘REQUEST_URI’] 是不会将参数中的特殊符号进行转换,因此这里的 i.d 参数并不会被替换为 i_d ,所以此时正常来说 i.d 和 i_d 都能经过第二个WAF。
- 第二个WAF中有一个 dhtmlspecialchars() 函数,作用前面我们已经介绍过了,这里需要绕过它,其实很好绕过。绕过之后 i_d=payload&i.d=123 便会进入到业务层代码中,执行SQL语句,由于这里的SQL语句采用拼接的方式,因此存在SQL注入。
因此最后payload如下:
http://127.0.0.1/index.php?submit=&i_d=-1/**/union/**/select/**/1,flag,3,4/**/from/**/ctf.users&i.d=123 |
畸形HTTP请求
当前的HTTP服务依据的是RFC2616标准的HTTP请求,但是当我们向Web服务器发送畸形的,也就是并非这个标准的HTTP数据包的时候,由于Web服务器的一些兼容性的特性,会尽力解析这些畸形的数据包。但是如果Web服务器和WAF针对畸形数据包解析的差距,就可能会出现一些Bypass的情况。以下是正常的数据包:
POST /test.php?id=1%20union/**/select/**/1,2,3/**/from/**/admin HTTP/1.1 |
如果我们将数据包修改为这样:
L1nk3r /test.php?id=1%20union/**/select/**/1,2,3/**/from/**/admin |
由于 L1nk3r 并不是标准HTTP协议中的请求方法,也没有协议字段 HTTP/1.1 ,也没有 host 字段。如果是在 HTTP/1.1 协议中,缺少 host 字段一般会返回 400 bad request 。但是某些版本的Apache在处理这个请求时,默认会设置协议为 HTTP/0.9 , host 则默认使用 apache 默认的 servername ,这种畸形的请求仍然能够被处理。
如果某些WAF在处理数据的时候严格按照GET、POST等标准HTTP方法来获取数据,或者采用正则匹配的方式来处理数据,那么这个时候就有可能因为WAF和WEB服务解析的前后不对等,绕过了这个WAF。
2、 Web应用层的问题
编码绕过
通过一般WAF会针对传来的数据包中带有的编码进行一次解码工作,如果WAF不能进行有效解码还原攻击向量,可能导致绕过,常见编码如URL编码、unicode编码(IIS)、宽字节编码等。例如我们用 url 的二次编码,而经过一次 url 解码的之后,WAF可能无法识别出它是恶意的数据,当把该恶意数据放行到Web服务器上时,Web服务器会再一次解码,最后导致了WAF绕过的结果。
看个实际例子:
多数据来源的问题
一般来说Web服务器从三个位置来获取用户传入的数据:
- 从GET中获取
- 如果GET中没有,尝试从POST中查找需要的值
- 若GET和POST中都没有,尝试从Cookie中获取想要的值。
典型的例子就如 ASP 和 ASP.NET ,这两种语言中的 Request 对象对于请求数据包的解析并没有按照RFC的标准来,一般开发者如果按照下面方式获取数据:
ID=Request('ID'); |
会出现以下这种情况:
在PHP情况下,我们看到是通过 request 方式传入数据,而php中REQUEST变量默认情况下包含了 GET ,POST 和 COOKIE 的数组。在 php.ini 配置文件中,有一个参数 variables_order ,这参数有以下可选项目
; variables_order |
这些字母分别对应的是 E: Environment ,G:Get,P:Post,C:Cookie,S:Server。这些字母的出现顺序,表明了数据的加载顺序。而 php.ini 中这个参数默认的配置是 GPCS ,也就是说如果有 POST 方式传入相同的数组,就覆盖掉 GET 方式传入的。
我们看个简单的例子
那个利用这个特性呢,实际上也可以bypasswaf,我们看下图中的例子:
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 也许就可以绕过了。
当然关于文件上传的话,可以试试将关键字换行分离试试看。下面看个例子:
对于网站来说,这样写是可以解析的,但是站在WAF的设计者的立场,他们可能并不知道,这个是可以这样写的。当用正则表达式去获取上传的文件名时,正则表达式就匹配不到了,所以上传就被绕过了。
- 2、某些WAF从数据包中提取检测特征的方式存在缺陷,如正则表达式不完善,某些攻击数据因为某些干扰字符的存在而无法被提取,常见的如%0a、%0b、%0c、%0d、%09、%0a等。之前某论坛流传的一个fuzz过某狗的脚本,代码主要如下:
主要上面代码中有两个关键fuzz的内容, fuzz_zs 和 fuzz_ch ,其实主要来说还是利用mysql的一些特性,bypass一些正则表达式针对关键字的检查,例如 union 、 select 之类的。
数据处理不恰当
1、%00截断
对于 %00 进行URL解码,实际上解码出来的就是C语言中的NULL字符,如果WAF对获取到的数据存储和处理不当,那么 %00 解码后会将后面的数据截断,造成后面的数据没有经过检测。
WAF在获取到参数id的值并解码后,参数值将被截断成1/*
,因此没有命中规则,从而放过了。
2、&字符处理
某些WAF在对HTTP请求数据包中的参数进行检测时,使用 & 字符对多个参数进行分割,然后分别进行检测,如:
http://www.test.com/1.php?p1=1&p2=2&p3=3 |
这些WAF会使用&符号分割p1、p2和p3,然后对其参数值进行检测。
p1=1 |
但是,如果遇到这种构造:
http://www.test.com/1.php?p1=1+union/*%26x1=1*/+select/*%26x2=1*/1,2,3+from+admin |
WAF会将以上参数分割成如下3部分:
p1=1+union/* |
由于目标服务器就只有一个参数 p1 ,然后 x1 和 x2 是不存在的, %26 是 & 符号的 url编码 ,如果WAF针对上述的三个参数进行分别的检测,是不会报注入的。这里巧妙就巧妙在利用两个参数拼接注释符号将不存在的 x1 和 x2 注释了。所以最后实际上进入数据库查询的语句也就只有。
p1=1+union+select1,2,3+from+admin |
数据清洗不恰当
当攻击者提交的参数值中存在大量干扰数据时,如大量空格、TAB、换行、%0c、注释等,一般waf都是清洗之后再进行规则匹配,因为如果干扰字符串过多的话检测需要消耗大量的资源和性能,所以清洗后可以提升性能降低匹配规则的复杂度,筛选出真实的攻击数据进行检测,以提高检查性能,节省资源。如果WAF对数据的清洗不恰当,会导致真实的攻击数据被清洗,剩余的数据无法被检测出攻击行为。
例如:
http://localhost/test/Article.php?id=9999-"/*" union all select 1,2,3,4,5 as "*/" from mysql.user |
由于waf会结合一些数据库的特性来清洗数据,对于/*
来说,它只是一个字符串,对于*/
来说,它也是一个字符串,更是一个别名,但是对于WAF来说,它会认为这是多行注释符,当waf把上面的payload清洗为
9999-"" from mysql.user |
针对规则库进行匹配,如果没有命中规则,执行原始语句:
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
。看个例子:
9999' and 1=(select top 1 name as # from master.sysdatabases)-- |
经过waf之后会被当成,waf将后端数据库认为是Mysql,由于在Mysql中 # 是注释,经过数据清洗,无法命中规则
9999' and 1=(select top 1 name as |
但是实际上,这里的 # 只是一个字符,充当一个别名的角色而已。如果后端数据库是SQL Server,这样的语句是没问题的。但是通用型WAF怎么能知道后端是SQL Server呢?
为性能和业务妥协
要全面兼容各类Web Server及各类数据库的WAF是非常难的,为了普适性,需要放宽一些检查条件,暴力的过滤方式会影响业务。对于通用性较强的软WAF来说,不得不考虑到各种机器和系统的性能,故对于一些超大数据包、超长数据可能会跳过不检测。
例如下图中的例子:
从容师傅分享的例子中有个非常暴力的方法,直接用大数据包打挂WAF。。。用这个注释中包含超长查询字符串,导致安全狗在识别的过程中挂掉了,连带着整个机器 Service Unavailable:
/*666666666666666666666666666666666666666666666666666666666666666 |
本身缺陷
比如某WAF,默认情况下只能获取前100个参数进行检测,当提交第101个参数时,那么,将无法对攻击者提交的第100个以后的参数进行有效安全检测,从而绕过安全防御。( CVE-2018-9230 )