前言
postMessage API 是在 HTML5 中引入的通信方法,可以在标签中实现跨域通信。
跨域嘛,大家懂的。
要使用这个方法发送消息,只需要在目标窗口对象上进行 postMessage
函数的调用。也就是像这样:
targetWindow.postMessage("hello world", "*");
注意 "*"
的部分,postMessage
的语法是 .postMessage(message, targetOrigin, [transfer])
,"*"
表示对接收消息的窗口无限制。
相信敏感的朋友已经想到,如果一个网页存在 targetWindow = window.opener
,然后又调用了 targetWindow.postMessage("hello world", "*");
,那么,对于任何打开这个网页的网站,想接收到 "hello world" 消息,只需要有下面这样的代码:
window.addEventListener("message", function(message){
console.log(message)
});
简而言之,如果传递的消息不是 "hello world",而是用户密码啥的,大家懂的。信息泄露,往往就是这样朴实无华,且枯燥。
不过,这只是 postMessage 相关漏洞的其中一种。存在有缺陷网页向其他网页传递消息的情况,自然也就同样存在其他网页可以向有缺陷网页传递消息的情况。
单纯从传递角度来说,想通过 postMessage
向任意一个网站传递信息,只需要 1. 获取对应的 window 对象;2. 发送消息。
最简单的方法,就是直接 let targetWindow = window.open("任意网址", '_blank');
,然后 targetWindow.postMessage("hello world", "*");
。
传递很简单,但只要对方网页没有相应的监听,这就不会对对方网页造成危害。
对于消息接收方来说,收到的 message 有三个属性:
data
:消息正文。比如前面例子中的 "hello world"。
origin
:消息发送方窗口的 origin
。例如 http://example.com:8080
。这个参数是消息发送方无法操纵篡改的。不过当然,消息发送方可以在消息发送后再导航到不同的 origin 。
source
:消息发送方窗口对象的引用,便于建立双向通信。也是消息发送方无法操纵篡改的。
相信大部分朋友已经猜到了,使用 postMessage API 进行通信时,对于消息接收方,是有一个安全规范的。消息接收方应该始终使用 origin
或者 source
属性验证发送方的身份。而且,对于安全要求较高的网站,如果发送方可能是跨域的,那么在验证了 origin
或者 source
属性后,最好仍然验证接收到的消息的格式和语法,否则存在利用信任方网站安全漏洞导致攻击的可能。
window.addEventListener('message', function (e) {
if (e.origin !== "https://www.freebuf.com") {
return;
}
……
});
现实中,大量网页没有遵守这样的安全规范,没有验证 origin
或者 source
属性的情况比比皆是。很多情况下,这样的缺陷并不会真正造成大的伤害,因为很多网页接收消息后只是把消息内容用来做判断,进行一些页面的不痛不痒的调整,这样的越权意义不大。而在另外的情况下,网页可能把消息内容放到了页面中,而这,往往能形成 XSS。
案例
对于形成 XSS,分享一个我报告过并早已修复的案例。
postMessage 的 XSS 漏洞基本都需要读代码发现。首先通过工具和代码发现存在相应监听的网页(后面会讲到),然后再查看代码看使用否有可利用点。
在这个案例中,问题存在于一个可视化功能的网页,网页 window.addEventListener("message"
接收到消息后,首先会通过 Array.isArray(event.data)
验证消息内容是否是数组。然后会通过 event.data.forEach(function (message)
进行数组遍历。之后对 message.channel
进行 switch case 判断并执行对应操作。
switch case 中的一个 "OpenNotification" 引起了我的注意,因为和提示、弹出相关的功能一直都是 XSS 的重灾区,而且在到读代码这一步之前,我就已经知道对应网页所使用的 jQuery 版本存在一些和提示、弹出相关的 XSS 缺陷。
仔细查看了相应函数后,果不其然发现了 XSS 点。
到这一步就很清晰了,PoC 需要传递的消息是一个数组,数组的元素是一个对象,对象的 channel
属性为 "OpenNotification",message
属性("OpenNotification" 中将这个属性的值传进了对应的有 XSS 点的函数)为准备好的 XSS payload。最终 PoC 的代码长这样:
test.html:
postMessage() XSS test
test.js:
window.targetWindow=null;
window.openWin=function(){
targetWindow = window.open(<有缺陷的网页地址>, '_blank');
};
window.postXSS=function(str){
targetWindow && targetWindow.postMessage([{"channel":"OpenNotification","message":}], "*");
console.log('post success');
};
工具
寻找这一类漏洞,需要识别出使用了相应功能的 js 文件。这一点通过 Burp 或 Fiddler 都可以轻松实现。
Burp 中,安装 J2EEScan 插件能实现对 postMessage 功能的自动被动探测,探测到使用了相应功能的 js 文件,会自动生成 issue 显示在 Target 选项卡中对应的域名下。
Fiddler 中,利用 FiddlerScript Editor,只需要在 OnBeforeResponse
函数中添加下面这样的代码,就可以自动将通过 Fiddler 的流量中符合条件的流量信息写入 log 文件。
oSession.utilDecodeResponse();
var oBody = System.Text.Encoding.UTF8.GetString(oSession.responseBodyBytes);
var regPostMsg=/\.addEventListener\(\"message\"|\.addEventListener\(\'message\'/g;
if (regPostMsg.test(oBody)){
var fso;
var file;
fso = new ActiveXObject("Scripting.FileSystemObject");
file = fso.OpenTextFile("D:\\Fiddler\\catch\\postmsg.log",8 ,true, true); //这里设置保存的文件路径,需要事先建立好相应文件
file.writeLine("Response url: " + oSession.url); //写入流量 URL
file.writeLine("Response header:" + "\n" + oSession.oResponse.headers); //写入流量的响应头
file.writeLine("\n");
file.close();
}
值得一提的是,Fiddler 的 AutoResponder 功能做代码调试非常顺滑,是我个人非常常用的一个功能。当你在 JavaScript 代码中发现 postMessage 相关问题之后,你需要找到相应代码的触发方式。毕竟很多代码并不是在加载网页时就触发的。甚至还有些代码可能本来就是冗余,根本无法触发。
尤其对于一些复杂的功能页面,找到相应代码的触发方式,可能需要经过繁琐的调试过程。实战中面对的往往是压缩后的代码,解压后面对一大堆精简的命名,理清楚代码执行前提是一件很烧脑的事情。很多时候,把代码下载下来添加 console.log
,然后使用 Fiddler 的 AutoResponder 功能进行文件替换(AutoResponder 功能可以让网站加载你本地的依赖文件而不是线上的),再在页面中点击各种功能寻找触发点,是更高效的做法。
Fiddler AutoResponder 功能的教程,网上已经有很多,在此不作赘述。
除了 Burp 和 Fiddler 外,浏览器开发者工具也是可以查看监听信息的,只是说平时的实际测试过程中,Burp 和 Fiddler 的方式会更高效。以 Google 浏览器为例,Developer tools - Sources,点开 Global Listeners,里面有 message 就说明存在对 message 的监听,并且可以点开查看监听存在的具体文件。
经验
最后,寻找相应漏洞的过程中,还可能会遇到另一个东西,BroadcastChannel。
如果你遇到 addEventListener 建立在 BroadcastChannel 对象上,那么,跨域的想法基本可以打消了。因为和单纯的 postMessage API 不同,BroadcastChannel 是一个同源通讯接口,实现的是同源下不同 Tab 页、frame 等之间的通讯。它本身的性质就决定了它不会有单纯 postMessage API 那样大的利用空间。
BroadcastChannel 的语法大致长这个样子:
// 连接到广播频道
let bc = new BroadcastChannel("test_channel");
// 发送消息
bc.postMessage("hello world");
// 接收消息
bc.onmessage = function (ev) { console.log(ev); }
BroadcastChannel 本身的同源限制决定了它不再需要像单纯 postMessage 那样进行验证,接收的消息直接使用就可以。
文章首发于 FreeBuf.COM