Introduction
General application
architecture review
For the presented example lets assume that we’ve discovered a stored
XSS vulnerability in the mail box functionality (ie XSS can be achieved
by sending an email to a user) in email wrapper application and that the
current application is built specifically as a Desktop application.
If reviewing the source code grep for and find Electron native
functions such as: (Desktop.shell.openExternal
,
Desktop.shell.realPath
, etc …) This means that the
architecture of Electron native application (desktop app) is poorly
designed and contextIsolation
and
nodeIntegration
settings are potentially disabled. In case
where the Electron app had been configured securely, the Electron native
code loaded from remote origin would never execute. Those settings can
also be checked by decompiling the Desktop application’s
.asar
file, and reviewing the webPreferences
settings for all renderers in the desktop app.
Is it easy to achieve RCE
in Electron?
Planning an attack vector
While reviewing the code of our example decompiled desktop
application (.asar
file), it is not a surprise to find that
all the application window and tab renderers are set to
contextIsolation:false
. Any architectural solution that
allows Electron native functions to be loaded from remote origins with
application’s client-side JavaScript code introduces significant
security and maintainability issues.
Based on this, we could plan an attack vector to achieve client-side
RCE in two possible ways:
- Attack the
preload.js
script by abusing the already
existing code of the Desktop application
- Attack the native JavaScript functions by overwriting their logic
through global JavaScript
__proto__
object
Theory
The contextIsolation
is an Electron feature that allows
developers to run code in preload scripts and in Electron APIs in a
dedicated JavaScript context. In practice, this means that global
objects such as Array.prototype.push
or
JSON.parse
cannot be modified by scripts running in the
renderer process.
It introduces separated contexts between Electron’s native JavaScript
code and the web application’s JavaScript code, so that execution of the
application’s JavaScript code does not affect the native code.
If contextIsolation
is disabled, the web application’s
JavaScript code may affect execution of Electron’s native JavaScript
code on the renderer and preload scripts. This behavior is dangerous
because Electron allows the web application’s JavaScript code to use the
Node.js features regardless of the nodeIntegration
option.
By interfering with them from the function overridden in the web page,
RCE can be achieved even if nodeIntegration
is set to
false
.
Even if nodeIntegration: false
is used, to truly enforce
strong isolation and prevent the use of Node primitives
contextIsolation
must also be used.
Back to the attack
By reviewing the preload.js
scripts it’s fairly common
to identify functionality that could be abused in the attack, for
example require
function can be reassigned and redeclared
with a new namespace to avoid confusions with the require
functions from the main renderer.
// The `require` function reassignment / redeclaration
var ElectronRequire = require // The actual problem lies in this line of code
const {contextBridge} = ElectronRequire('electron')
const {readFile, writeFile} = ElectronRequire('fs/promises')
contextBridge.exposeInMainWorld('settingsAPI', {
getSettings: () => readFile('path/to/user-settings.json').then(JSON.parse),
saveSettings: (value) => writeFile('path/to/user-settings.json', JSON.stringify(value)),
})
The preload.js
file are special JS scripts that will be
executed before every web page load. It is declared individually for
each window:
new BrowserWindow({
webPreferences: {
preload: 'preload.js'
}
})
The preload.js
module is intended to create
narrow, controlled interfaces through which the renderer.js
can interact with the Node.js API:
- In the
preload.js
file you create a global method.
- And in the
renderer.js
file use it. All the methods
created in preload file will have access to Node.js even if they are
called in the renderer.
It simply means that ElectronRequire
function could be
used to import any module we would like to work with, and to use Node.js
features within the renderer. In this situation it is straight forward
to craft a payload for the exploitation process. Using our theoretical
found XSS vulnerability, It is possible to craft the following payload
to leverage the 0-click client-side RCE attack against desktop
users:
<img src=1 onerror='ElectronRequire("child_process").exec("uname -a",function(error,stdout,stderr){console.log(stdout);});'>
Summary
Developers can and do intentionally disable
contextIsolation
setting. They can implement native
Electron functions within normal business logic of client-side JS code,
instead of overwriting and limiting Electron event listeners that define
similar business logic (for example: new window, tab or file opening).
In the presented scenario, an attacker could send emails to application
users with the devastating XSS payload to compromise the user’s devices,
if the email is opened.
Developers should always enable isolation (set
contextIsolation
to true
, and
nodeIntegration
to false
), and for this
particular case refactor the rest of the functionality of the web
application that uses native Electron code and is loaded to the renderer
from the remote origin.
References
- https://mksben.l0.cm/2020/10/discord-desktop-rce.html?m=1
- https://www.electronjs.org/docs/latest/tutorial/security#14-do-not-use-openexternal-with-untrusted-content