前一段时间,在EtherDream大神的博客里看到关于XSS防火墙的一系列文章,觉得很有意思。刚好科创要做一个防火墙,就把XSS前端防火墙作为一个创新点,着手去实现了。

在实现过程中,由于各种原因,比如说JavaScript不熟练啦,SQL注入防火墙的干扰啦,出现了一系列问题,在文章中也会提到。

0x00 对XSS的分类

根据触发方式和可执行脚本的位置,把XSS分成如下几类,每一种都有不同的防御方式:

1) 内嵌型,直接内嵌在HTML标签中的一些可执行JS代码的属性中,如:

1
2
3
<a href="#" onclick="alert(document.cookie)">test</a>
<img src="1" onerror="alert(document.cookie)">
<a href="javascript:alert(document.cookie)">test</a>

以上是比较基础也比较有代表性的几个例子,当然还有一些变形:

1
2
3
<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) 静态外联型,成块/文件直接嵌套在页面中,如:

1
2
<script src="evil.js"></script>
<SCRIPT TYPE="text/javascript">alert('hacked by bb');</SCRIPT>

这一块儿变形比较少,因为再怎么变,也都要引入一个script标签。而针对script标签(不包括script脚本的内容)的防御,莫过于URL黑白名单、URL关键字。

3) 动态外联型,动态添加script标签(恶意代码在script标签中)

1
2
3
4
5
<script type="text/javascript">
var temp = document.createElement('script')
temp.src = 'xss.js'
document.body.appendChild(temp)
</script><br/>

最常见的类型如上,在能执行脚本的地方,就能添加新的script标签。

4) 其他,iframe是比较危险的标签,为了防止通过iframe绕过防火墙,我们也做了一些处理。

1
2
<iframe srcdoc="<script>alert('bb')<\/script>"></iframe><br/>
<iframe src="eval.htm"></iframe><br/>

0x10 内嵌型XSS的防御

针对内嵌型XSS的防御思想是: 在触发事件执行代码前,检测代码是否有害。

为了实现这个,需要用到addEventListener对全局的事件进行监听。为了获取所有的可触发的事件,我们遍历了document所有的事件(以on开头,如onclickonmouseout):

1
2
3
4
5
for (var k in document) {
if (/^on./.test(k)) {
//绑定事件
}
}

绑定事件,同时做到在触发事件之前先触发我们的检测脚本,需要用到addEventListener中的第三条属性,也就是:

1
2
3
document.addEventListener('click', function (e) {
//检测代码
}, true);

在这一块儿,出现的比较突出的问题是:

  1. 性能问题。 鼠标的移动,会触发很多次检测,而大多数检测都是没有意义的。
  2. 判断是否为恶意代码问题。 要有一个比较完善的判断机制。
  3. JS事件冒泡机制。JS事件会一层一层向外冒泡,可以参考js冒泡机制。

解决方案:

  1. 添加了hash机制,对已经扫描过的事件,直接跳过。
  2. 完善恶意代码检测函数,我们初步的检测代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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是否在白名单/黑名单中。如果检测到攻击,就删除当前元素。

1
2
3
4
5
6
7
8
9
10
11
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

    1
    src = script_src.match(/(http:\/\/|https:\/\/)?([^\/]*)/)[2]
  2. 组装黑/白名单的正则:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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函数劫持。说到关键函数,现在实现的钩子包含了常用的函数,包括createElementsetAttribute两个函数,实现了对最常用的动态创建元素的监控。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
2
3
4
5
6
7
8
9
//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类型做检测的时候,很脑残的把代码写成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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函数:

1
var raw_setter = HTMLScriptElement.prototype.__lookupSetter__('src');

同时为了防止我们的函数不会再次被钩子勾住,然后进行修改,我们还要对钩子进行一些处理,让它不可写。

1
2
3
4
5
6
7
//锁死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,直接禁掉
1
2
3
4
5
6
7
8
9
10
11
12
13
<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 可执行函数重写

有很多函数,比如evalsetInterval等,相当于PHP中的eval,同样可以造成很大的危害。所以继续上钩子

1
2
3
4
5
6
7
8
9
10
11
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的后台,搭配使用,可以做到漏洞回放、日志查看、防火墙设置等基本功能。

以上大致的总结了各模块工作的基本模式,以及在开发过程中遇到的问题。一些细节写的不是太详细,如果你感兴趣,可以在 基于NodeJS的web应用防火墙(waf) 看到我们的代码,欢迎来交流。