XSS前端防火墙
前一段时间,在EtherDream大神的博客里看到关于XSS防火墙的一系列文章,觉得很有意思。刚好科创要做一个防火墙,就把XSS前端防火墙作为一个创新点,着手去实现了。
在实现过程中,由于各种原因,比如说JavaScript不熟练啦,SQL注入防火墙的干扰啦,出现了一系列问题,在文章中也会提到。
0x00 对XSS的分类
根据触发方式和可执行脚本的位置,把XSS分成如下几类,每一种都有不同的防御方式:
1) 内嵌型,直接内嵌在HTML标签中的一些可执行JS代码的属性中,如:
<a href="#" onclick="alert(document.cookie)">test</a> <img src="1" onerror="alert(document.cookie)"> <a href="javascript:alert(document.cookie)">test</a>
以上是比较基础也比较有代表性的几个例子,当然还有一些变形:
<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgzKTwvc2NyaXB0Pg==">test</a> <a onclick="alert((+[][+[]]+[])[++[[]][+[]]]+([![]]+[])[++[++[[]][+[]]][+[]]]+([!![]]+[])[++[++[++[[]][+[]]][+[]]][+[]]]+([!![]]+[])[++[[]][+[]]]+([!![]]+[])[+[]])" href="javascript:void">test</a> <svg><a xlink:href="javascript:alert(14)"><rect width="1000" height="1000" fill="white"/></a></svg>
2) 静态外联型,成块/文件直接嵌套在页面中,如:
<script src="evil.js"></script> <SCRIPT TYPE="text/javascript">alert('hacked by bb');</SCRIPT>
这一块儿变形比较少,因为再怎么变,也都要引入一个script标签。而针对script标签(不包括script脚本的内容)的防御,莫过于URL黑白名单、URL关键字。
3) 动态外联型,动态添加script标签(恶意代码在script标签中)
<script type="text/javascript"> var temp = document.createElement('script') temp.src = 'xss.js' document.body.appendChild(temp) </script><br/>
最常见的类型如上,在能执行脚本的地方,就能添加新的script标签。
4) 其他,iframe是比较危险的标签,为了防止通过iframe绕过防火墙,我们也做了一些处理。
<iframe srcdoc="<script>alert('bb')<\/script>"></iframe><br/> <iframe src="eval.htm"></iframe><br/>
0x10 内嵌型XSS的防御
针对内嵌型XSS的防御思想是: 在触发事件执行代码前,检测代码是否有害。
为了实现这个,需要用到addEventListener对全局的事件进行监听。为了获取所有的可触发的事件,我们遍历了document所有的事件(以on开头,如onclick、onmouseout):
for (var k in document) { if (/^on./.test(k)) { //绑定事件 } }
绑定事件,同时做到在触发事件之前先触发我们的检测脚本,需要用到addEventListener中的第三条属性,也就是:
document.addEventListener('click', function (e) { //检测代码 }, true);
在这一块儿,出现的比较突出的问题是:
1. 性能问题。 鼠标的移动,会触发很多次检测,而大多数检测都是没有意义的。
2. 判断是否为恶意代码问题。 要有一个比较完善的判断机制。
3. JS事件冒泡机制。JS事件会一层一层向外冒泡,可以参考js冒泡机制。
解决方案:
1. 添加了hash机制,对已经扫描过的事件,直接跳过。
2. 完善恶意代码检测函数,我们初步的检测代码如下:
function xss_test(code){ var keyword=['xss','eHNz','xss','xss','\\u0078\\u0073\\u0073', '\\x78\\x73\\x73','\\170\\163\\163',/*特殊格式XSS*/ 'alert\\\(\\s*\\d+\\\)','alert\\\(test\\\)','hacked',/*匹配alert(1),alert(test)*/ 'String.fromCharCode','document.cookie', '(\\\[\\\].*){3,}',/*匹配[]![]类的变形*/'data:text/html',/*匹配(URL编码|base64)的变形*/ '(&#x[0-9a-f]{2,}.*){3,}','(&#\\d{2,}.*){3,}',/*匹配HTML HEX变形*/ '(\\\x?[0-9a-f]{2,}.*){3,}',/*匹配JS HEX变形*/,'(\\\u\\d{2,}){3,}'/*匹配unicode变形*/ ]; var pattern=new RegExp(get_reg(keyword),"i"); if(pattern.test(code)){ return true; } return false; }
3. 检测的时候,逐层递归。
0x20 静态外联型XSS的防御
基本防御思想:动态监听元素的添加,并拦截
用到了HTML5中的MutationObserver,监听元素的变动(添加),在添加的时候,检测添加的内容(node.innerHTML)或URL(node.src)中是否含有恶意关键字、URL是否在白名单/黑名单中。如果检测到攻击,就删除当前元素。
var observer = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { var nodes = mutation.addedNodes; for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; if (node.tagName == 'SCRIPT') { //一些检测代码 } } } }
遇到的比较突出的问题是,删除的时候,由于多种原因(浏览器对HTML5的支持性、各浏览器的兼容性、删除时结点可能尚未被挂载到页面中等)可能会造成删除失败,恶意代码仍会执行。在实际应用中,没有找到合适的解决办法。但无论是否能够删除,添加的结点总是能够被捕获到,同样能起到很好的监听作用。
在处理的过程中,还遇到了黑名单和白名单。黑白名单都是从后台添加的,基本功能很简单:当遇到白名单,直接跳过,遇到黑名单,就直接删除。只是白名单的时候要提一点,既然是漏洞预警防火墙,所以必须有记录数据/拦截日志的地方(管理中心),而在发送数据的时候,不免要用到跨域,而且跨域的时候发送的数据还有可能携带XSS特征,很有可能被拦截掉,所以白名单中,一定要添加我们管理中心的URL。在我们的防火墙系统中,默认管理中心为:http://127.0.0.1:1337/index.html。
匹配黑白名单的时候,用到了简单的正则:
1. 从当前URL中获取host:port
src = script_src.match(/(http:\/\/|https:\/\/)?([^\/]*)/)[2]
2. 组装黑/白名单的正则:
var OutsiteWhiteList = ["127.0.0.1:8080","baidu.com"] /** 正则表达式生成函数 ** @input : keyword 数组形式,如['xss','x ss'] ** @output: 格式化的正则表达式,如(xss|x ss) **/ function get_reg(keyword){ var str='('; for(var i in keyword){ str+=keyword[i]+"|"; } return str.length>1?str.slice(0,-1)+')':false; }
0x30 动态外联型XSS的防御
基本防御思想:JavaScript Hijacking,也就是JS钩子,勾住一些关键函数,并添加检测代码
关于JavaScript Hijacking,有一篇很经典的文章可以参考:浅谈javascript函数劫持。说到关键函数,现在实现的钩子包含了常用的函数,包括createElement、setAttribute两个函数,实现了对最常用的动态创建元素的监控。
var raw_createElement = Document.prototype.createElement; Document.prototype.createElement = function () { var element = raw_createElement.apply(this, arguments); // 为脚本元素安装属性钩子 if (element.tagName == 'SCRIPT') { element.__defineSetter__('src', function (url) { element.setAttribute("src",url) }); } return element; //createElement函数需要返回一个对象 }; var raw_setAttribute = Element.prototype.setAttribute; Element.prototype.setAttribute = function (name, url) { if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) { var res = url_test(url); switch (res) { case 'white_list': break; case 'url_word': xss_notice(301, 'null', url); return; case 'black_list': xss_notice(302, 'null', url); return; default: break; } } raw_setAttribute.apply(this, arguments); //setAttribute并不需要返回值 };
能够实现如下格式的动态创建元素的检测:
//1. src赋值 var temp = document.createElement('script') temp.src = 'xss.js' document.body.appendChild(temp) //2. setAttribute赋值 var temp = document.createElement('script') temp.setAttribute('src','xss.js') document.body.appendChild(temp)
在开发过程中,遇到最突出的问题是钩子逻辑错误,导致无限循环(= =页面崩溃了N次)。出现的原因是这样:在给createElement - src类型做检测的时候,很脑残的把代码写成了这样:
var raw_createElement = Document.prototype.createElement; Document.prototype.createElement = function () { var element = raw_createElement.apply(this, arguments); // 为脚本元素安装属性钩子 if (element.tagName == 'SCRIPT') { element.__defineSetter__('src', function (url) { //正确写法:element.setAttribute("src",url) element.src=url //错误写法,无限循环调用自身 }); } return element; //createElement函数需要返回一个对象 };
除了用setAttribute来赋值外,还可以用钩子的方法来实现,勾住原始函数需要调用lookupsetter函数:
var raw_setter = HTMLScriptElement.prototype.__lookupSetter__('src');
同时为了防止我们的函数不会再次被钩子勾住,然后进行修改,我们还要对钩子进行一些处理,让它不可写。
//锁死call和apply,防止盗用和重写 Object.defineProperty(Function.prototype, 'call', { value: Function.prototype.call, writable: false, configurable: false, enumerable: true });
一定要提到的一点,是能够动态创建元素的函数不止这些,以上的函数,只是完成了最基本的钩子,有经验的攻击者很简单的可以绕过。
0x40 关于Iframe
以上说到的防火墙代码,都是针对当前的页面进行防御。如果通过iframe创建一个新页面,那不就绕过了?所以针对iframe做了如下的防御:
1. 不允许动态创建iframe。 直接在createElement的钩子里drop掉所有创建Iframe的语句。
2. 默认拦截所有的静态iframe,仅允许白名单中的iframe创建。 在MutationObserver中设置。
3. 在所有允许创建的iframe里嵌入防火墙代码,层层递归,保护安全。
4. 针对特别格式的,如srcdoc,直接禁掉
<iframe srcdoc="<script src=white.js><\/script>"></iframe> <script> //对于iframe,由于不常用,所以,监控所有的Iframe元素,直接用白名单过滤 if (node.tagName == 'IFRAME') { if (node.getAttribute('srcdoc')) { //动态添加的srcdoc 同样可以被拦截 delete_node(node, 401); } else if (node.src && !reg_test(IframeWhiteList, node.src)) { //如果没有被白名单匹配到 delete_node(node, 402); } } </script>
0x50 可执行函数重写
有很多函数,比如eval、setInterval等,相当于PHP中的eval,同样可以造成很大的危害。~。~所以继续上钩子
var raw_setInterval =setInterval; setInterval = function(func, delay){ if(xss_test(func)){ xss_notice(503,'',func); } else{ raw_setInterval(func,delay) } }; setInterval.constructor=undefined;
需要注意的一点,是要把constructor置为空,否则可以调用setInterval.constructor绕过。
0x60 总结
以上的描述基本能够应对常见的XSS攻击,并能起到一定程序的预警作用。我们已经基本完成了前端防火墙的开发,并且为其编写了一个基于express的后台,搭配使用,可以做到漏洞回放、日志查看、防火墙设置等基本功能。
以上大致的总结了各模块工作的基本模式,以及在开发过程中遇到的问题。一些细节写的不是太详细,如果你感兴趣,可以在http://git.oschina.net/friday_bb/waf看到我们的代码,欢迎来交流。