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','&#120;&#115;&#115;','&#x78;&#x73;&#x73;','\\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看到我们的代码,欢迎来交流。

posted @ 2015-06-16 14:09  阔爱的贝贝  阅读(734)  评论(0编辑  收藏  举报