45

关于WAF的那些事

 5 years ago
source link: http://www.lmxspace.com/2018/11/10/关于WAF的那些事/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

首先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 中处理之后。

am2iUba.jpg!web

到达服务器上的实际是

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 会做如下处理:

mqUFR3u.jpg!web

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

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

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

s%u0065lect->select
s%u00f0lect->select

PS:

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

字母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协议中是允许同样名称的参数出现多次的。

例如:

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

MVBrIjF.jpg!web

假设提交的参数即为:

id=1&id=2&id=3

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

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

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

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

union select password/*,*/from admin

YFFFNjy.jpg!web

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

EviANzb.png!web

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

mQnmaye.png!web

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

NVFFzyU.png!web

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

JvMVVv2.png!web

这里的代码中有两个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 方式,因此我们来看个例子。

NvYjMzY.png!web

第一次 $_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如何构造,我们可以先来看个思维导图。

3amUVbi.png!web

  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如下:

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

iQ3ee2z.png!web

畸形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
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

....

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

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绕过的结果。

6nQZniR.jpg!web

看个实际例子:

aYZVjeM.png!web

多数据来源的问题

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

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

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

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

会出现以下这种情况:

2QBjUv7.jpg!web

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

; variables_order
;   Default Value: "EGPCS"
;   Development Value: "GPCS"
;   Production Value: "GPCS"

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

FjeayyQ.png!web

我们看个简单的例子

Njeqeqv.png!web

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

AbMbYzf.png!web

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 也许就可以绕过了。

JFZRjaq.png!web

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

qeQjuq7.png!web

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

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

f2IJfuU.png!web

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

数据处理不恰当

1、%00截断

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

em2eaqU.jpg!web

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

2、&字符处理

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

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

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

p1=1
p2=2
p3=3

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

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

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

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

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

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来说,不得不考虑到各种机器和系统的性能,故对于一些超大数据包、超长数据可能会跳过不检测。

例如下图中的例子:

3QVfyir.png!web

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

/*666666666666666666666666666666666666666666666666666666666666666 
66666666666666666666666666666666666666666666666666666666666666666 
66666666666666666666666666666666666666666666666666666666666666666 
66666666666666666666666666666666666666666666666666666666666666666 
66666666666666666666666666666666666666666666666666666666666666666 
66666666666666666666666666666666666666666666666666666666666666666 6666666666666666666666666666666666666666*/

6n6Bnu2.png!web

本身缺陷

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

fABvuaU.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK