postMessage 相關漏洞分析與分享

前言

postMessage API 是在 HTML5 中引入的通信方法,可以在標籤中實現跨域通信。

跨域嘛,大家懂的。

要使用這個方法發送消息,只需要在目標窗口對象上進行 postMessage 函數的調用。也就是像這樣:

targetWindow.postMessage("hello world", "*");

注意 "*" 的部分,postMessage 的語法是 .postMessage(message, targetOrigin, [transfer])"*" 表示對接收消息的窗口無限制。

相信敏感的朋友已經想到,如果一個網頁存在 targetWindow = window.opener,然後又調用了 targetWindow.postMessage("hello world", "*");,那麼,對於任何打開這個網頁的網站,想接收到 "hello world" 消息,只需要有下面這樣的代碼:

window.addEventListener("message", function(message){
    console.log(message)
});

簡而言之,如果傳遞的消息不是 "hello world",而是用戶密碼啥的,大家懂的。信息泄露,往往就是這樣樸實無華,且枯燥。

不過,這只是 postMessage 相關漏洞的其中一種。存在有缺陷網頁向其他網頁傳遞消息的情況,自然也就同樣存在其他網頁可以向有缺陷網頁傳遞消息的情況。

單純從傳遞角度來說,想通過 postMessage 向任意一個網站傳遞信息,只需要 1. 獲取對應的 window 對象;2. 發送消息。

最簡單的方法,就是直接 let targetWindow = window.open("任意網址", '_blank');,然後 targetWindow.postMessage("hello world", "*");

傳遞很簡單,但只要對方網頁沒有相應的監聽,這就不會對對方網頁造成危害。

對於消息接收方來說,收到的 message 有三個屬性:

data:消息正文。比如前面例子中的 "hello world"。
origin:消息發送方窗口的 origin。例如 http://example.com:8080。這個參數是消息發送方無法操縱篡改的。不過當然,消息發送方可以在消息發送後再導航到不同的 origin 。
source:消息發送方窗口對象的引用,便於建立雙向通信。也是消息發送方無法操縱篡改的。

相信大部分朋友已經猜到了,使用 postMessage API 進行通信時,對於消息接收方,是有一個安全規範的。消息接收方應該始終使用 origin 或者 source 屬性驗證發送方的身份。而且,對於安全要求較高的網站,如果發送方可能是跨域的,那麼在驗證了 origin 或者 source 屬性後,最好仍然驗證接收到的消息的格式和語法,否則存在利用信任方網站安全漏洞導致攻擊的可能。

window.addEventListener('message', function (e) {
    if (e.origin !== "https://www.freebuf.com") {
        return;
    }
    ……
});

現實中,大量網頁沒有遵守這樣的安全規範,沒有驗證 origin 或者 source 屬性的情況比比皆是。很多情況下,這樣的缺陷並不會真正造成大的傷害,因爲很多網頁接收消息後只是把消息內容用來做判斷,進行一些頁面的不痛不癢的調整,這樣的越權意義不大。而在另外的情況下,網頁可能把消息內容放到了頁面中,而這,往往能形成 XSS。

案例

對於形成 XSS,分享一個我報告過並早已修復的案例。

postMessage 的 XSS 漏洞基本都需要讀代碼發現。首先通過工具和代碼發現存在相應監聽的網頁(後面會講到),然後再查看代碼看使用否有可利用點。

在這個案例中,問題存在於一個可視化功能的網頁,網頁 window.addEventListener("message" 接收到消息後,首先會通過 Array.isArray(event.data) 驗證消息內容是否是數組。然後會通過 event.data.forEach(function (message) 進行數組遍歷。之後對 message.channel 進行 switch case 判斷並執行對應操作。

switch case 中的一個 "OpenNotification" 引起了我的注意,因爲和提示、彈出相關的功能一直都是 XSS 的重災區,而且在到讀代碼這一步之前,我就已經知道對應網頁所使用的 jQuery 版本存在一些和提示、彈出相關的 XSS 缺陷。

仔細查看了相應函數後,果不其然發現了 XSS 點。

到這一步就很清晰了,PoC 需要傳遞的消息是一個數組,數組的元素是一個對象,對象的 channel 屬性爲 "OpenNotification",message 屬性("OpenNotification" 中將這個屬性的值傳進了對應的有 XSS 點的函數)爲準備好的 XSS payload。最終 PoC 的代碼長這樣:

test.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="postMessage() XSS test">

    <title>postMessage() XSS test</title>

</head>
<body>

<div class="wrapper-page">

    <button onclick="openWin()">OPEN</button>
    <button onclick="postXSS()">POST</button>

</div>

<script src="test.js"></script>

</body>
</html>

test.js:

window.targetWindow=null;

window.openWin=function(){
    targetWindow = window.open(<有缺陷的網頁地址>, '_blank');
};

window.postXSS=function(str){
    targetWindow && targetWindow.postMessage([{"channel":"OpenNotification","message":<XSS payload>}], "*");
    console.log('post success');
};

工具

尋找這一類漏洞,需要識別出使用了相應功能的 js 文件。這一點通過 Burp 或 Fiddler 都可以輕鬆實現。

Burp 中,安裝 J2EEScan 插件能實現對 postMessage 功能的自動被動探測,探測到使用了相應功能的 js 文件,會自動生成 issue 顯示在 Target 選項卡中對應的域名下。

Fiddler 中,利用 FiddlerScript Editor,只需要在 OnBeforeResponse 函數中添加下面這樣的代碼,就可以自動將通過 Fiddler 的流量中符合條件的流量信息寫入 log 文件。

oSession.utilDecodeResponse();
var oBody = System.Text.Encoding.UTF8.GetString(oSession.responseBodyBytes);

var regPostMsg=/\.addEventListener\(\"message\"|\.addEventListener\(\'message\'/g;
if (regPostMsg.test(oBody)){
    var fso;
    var file;
    fso = new ActiveXObject("Scripting.FileSystemObject");
    file = fso.OpenTextFile("D:\\Fiddler\\catch\\postmsg.log",8 ,true, true); //這裏設置保存的文件路徑,需要事先建立好相應文件
    file.writeLine("Response url: " + oSession.url); //寫入流量 URL
    file.writeLine("Response header:" + "\n" + oSession.oResponse.headers); //寫入流量的響應頭
    file.writeLine("\n");
    file.close();
}

值得一提的是,Fiddler 的 AutoResponder 功能做代碼調試非常順滑,是我個人非常常用的一個功能。當你在 JavaScript 代碼中發現 postMessage 相關問題之後,你需要找到相應代碼的觸發方式。畢竟很多代碼並不是在加載網頁時就觸發的。甚至還有些代碼可能本來就是冗餘,根本無法觸發。

尤其對於一些複雜的功能頁面,找到相應代碼的觸發方式,可能需要經過繁瑣的調試過程。實戰中面對的往往是壓縮後的代碼,解壓後面對一大堆精簡的命名,理清楚代碼執行前提是一件很燒腦的事情。很多時候,把代碼下載下來添加 console.log,然後使用 Fiddler 的 AutoResponder 功能進行文件替換(AutoResponder 功能可以讓網站加載你本地的依賴文件而不是線上的),再在頁面中點擊各種功能尋找觸發點,是更高效的做法。

Fiddler AutoResponder 功能的教程,網上已經有很多,在此不作贅述。

除了 Burp 和 Fiddler 外,瀏覽器開發者工具也是可以查看監聽信息的,只是說平時的實際測試過程中,Burp 和 Fiddler 的方式會更高效。以 Google 瀏覽器爲例,Developer tools - Sources,點開 Global Listeners,裏面有 message 就說明存在對 message 的監聽,並且可以點開查看監聽存在的具體文件。

經驗

最後,尋找相應漏洞的過程中,還可能會遇到另一個東西,BroadcastChannel。

如果你遇到 addEventListener 建立在 BroadcastChannel 對象上,那麼,跨域的想法基本可以打消了。因爲和單純的 postMessage API 不同,BroadcastChannel 是一個同源通訊接口,實現的是同源下不同 Tab 頁、frame 等之間的通訊。它本身的性質就決定了它不會有單純 postMessage API 那樣大的利用空間。

BroadcastChannel 的語法大致長這個樣子:

// 連接到廣播頻道
let bc = new BroadcastChannel("test_channel");

// 發送消息
bc.postMessage("hello world");

// 接收消息
bc.onmessage = function (ev) { console.log(ev); }

BroadcastChannel 本身的同源限制決定了它不再需要像單純 postMessage 那樣進行驗證,接收的消息直接使用就可以。

文章首發於 FreeBuf.COM

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章