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.html
。2.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
代码。因为当定义了一个监听器后,需要按照事件数据流来分析代码是否以容易被攻击的函数结束。这里推荐两种方法来检测函数调用:
- 1 . J2EEScan,从git仓库(https://github.com/ilmila/J2EEScan)可以获得更新版本,而不是从
Burp AppStore
。 - 2 . BurpBounty (https://github.com/wagiro/BurpBounty),定义一组用于搜索关键字的被动响应字符串,如
postMessage
、addEventListener("message
、.on("message"
。
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 .
mktoResponse
为PostMessage
的第一个JSON元素,以调用函数:
else if (d.mktoResponse){
onResponse(d.mktoResponse)
}
- 2 . 为了能执行这个函数,需要一个JSON结构数据,其元素有
for
、error
和data
。如果error
为false
,则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对象,包含callback
、key
、description
:
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 // "'"<b>\"
// 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