XSS 前端防火墙—无懈可击的钩子

发表于:2014-7-04 10:57

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:邹菜头    来源:51Testing软件测试网采编

  靠,这不算,这只是我们测试而已。现实中谁会放在全局变量里呢,这年头不套一个闭包的脚本都不好意思拿出来。
  好吧,我还是放闭包里,这总安全了吧。看你怎么隔空取物,从我闭包里偷出来。
  (function() {
  // 保存上级接口
  var raw_fn = Element.prototype.setAttribute;
  ...
  })();
  不过,真要偷出来,那绝对是没问题的!
  这个变量唯一用到的地方就是:
  raw_fn.apply(this, arguments)
  这可不是一个原子操作,而是调用了 Function.prototype.apply 这个全局函数。神马。。。这。是真的,不信你可以试试!
  不用说,你也懂了。我还是说完吧:我们可以重写 apply,然后随便给某个元素 setAttribute 下,就可以窃听到钩子传过来的 raw_fn 了。
  Function.prototype.apply = function() {
  console.log('哈哈,得到原始接口了:', this);
  };
  document.body.setAttribute('a', 1);
  Run
  这也太贱了吧,不带这样玩的。可人家就能用这招绕过你,又怎样。
  你会想,干脆把 Function.prototype.apply 也提前保存起来得了。然后一番折腾,你会发现代码变成 apply.apply.apply.apply...
  毕竟,apply 和 call 已是最底层了,没法再 call 自己了。
  这可怎么办。显然不能再用 apply 或 call 了,但不用它们没法把 this 变量传进去啊。回想下,有哪些方法可以控制 this 的:
  obj.method()
  method.call(obj)
  貌似也就这两类。排除了第二种,那只剩最古老的用法了。可是我们已经重写了现有的接口,再调用自己那就递归溢出了。
  但是,我们可以给原始接口换个名字,不就可以避免冲突了:
(function() {
// 保存上级接口
Element.prototype.__setAttribute = Element.prototype.setAttribute;
// 勾住当前接口
Element.prototype.setAttribute = function(name, value) {
// 额外细节实现 ...
// 向上调用
this.__setAttribute(name, value);
};
})();
Run
  这样倒是甩掉 apply 这个包袱了,但是无论取『__setAttribute』,还是换成其他名字,人家知道了,照样可以拿出原始接口。所以,我们得取个复杂的名字,最好每次还都不一样:
(function() {
// 取个霸气的名字
var token = '$' + Math.random();
// 保存上级接口
Element.prototype[token] = Element.prototype.setAttribute;
// 勾住当前接口
Element.prototype.setAttribute = function(name, value) {
// 额外细节实现 ...
// 向上调用
this[token](name, value);
};
})();
Run
  现在,你完全不知道我把原始接口藏在哪了,而且用 this[token](...) 这个巧妙的方法,同样符合刚才列举的第一类用法。
  问题似乎。。。解决了。但,总感觉有什么不对劲。。。人家不知道变量藏哪了,难道不可以找吗。把 Element.prototype 遍历下,一个个找过去,不相信会找不到:
for(var k in Element.prototype) {
console.log(k);
if (k.substr(0,1) == '$') {
console.error('楼上的,你这名字那么猥琐,敢露个面吗');
console.error(Element.prototype[k]);
}
}
Run
  取了个这么拉风的名字,就象是黑暗中的萤火虫,瞬间给揪出来了。你会说,为什么不取个再隐蔽点的名字,甚至还可以冒充良民,把从来不用的方法给替换了。
  不过,无论想怎么躲,都是徒劳的。有无数种方法可以让你原形毕露。除非 —— 根本不能被人家枚举到。
  属性隐身术
  如果没记错的话,主流 JavaScript 里好像还真有什么叫 enumerable、configurable 之类的东西。把它们搬出来,看看能不能赋予我们隐身功能?
  马上就试试:
  // 嘘~ 劳资要隐身了
  Object.defineProperty(Element.prototype, token, {
  value: Element.prototype.setAttribute,
  enumerable: false
  });
  Run
  神奇,红红的那坨字果然没出现。看来真的隐身了!
  到此,原函数泄露的问题,我们算是搞定了。
  不过暂时还不能松懈,为什么?连 apply 都能被山寨,那还有什么可以相信的!那些正则表达式的 test 方法、字符串的大小写转换、数组的 forEach 等等等等,都是可以被改写的。
  要是人家把 RegExp.prototype.test 重写了,并且总是返回 false,那么我们的策略判断就完全失效了。
  所以,我们得重复上面的步骤,把这些运行时要用到的全局方法,都得随机隐匿起来。
  锁死 call 和 apply
  不过,隐藏一个还好,大量的代码都用这种 Geek 的方式,显得很是累赘。
  既然能有隐身那样神奇的魔法,难道就没有其他类似的吗?事实上,Object.defineProperty 里还有很多有意思的功能,除了让属性不可见,还能不可写、不可删等等。
  可以让属性不可写?太好了,不如干脆把 Function.prototype.call 和 apply 都事先锁死吧,反正谁会无聊到重写它们呢。
Object.defineProperty(Function.prototype, 'call', {
value: Function.prototype.call,
writable: false,
configurable: false,
enumerable: true
});
// apply 也一样
马上看看效果:
Function.prototype.call = function() {
alert('hello');
};
console.log(Function.prototype.call);
果然还是
function call() { [native code] }
Run
  现在,我们大可放心的使用 call 和 apply,再也不用鼓捣那堆随机属性了。
  不过这种随机+隐藏的属性,今后还是有用武之地的,常常用来给公开的对象做个秘密的记号,所以没有白折腾。
  到此,我们终于可以松口气了。
32/3<123>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号