In this post, we continue our analysis of the SolarCity ConnectPort X2e Zigbee device (referred to throughout as X2e device). In Part One, we discussed the X2e at a high level, performed initial network-based attacks, then discussed the hardware techniques used to gain a remote shell on the X2e device as a non-privileged system user. In this segment, we’ll cover how we obtained a privileged shell on the device locally using power glitching attacks, and explore CVE-2020-12878, a vulnerability we discovered that permitted remote privilege escalation to the root user. Combined with CVE-2020-9306 (discussed in Part One), this would result in a complete remote compromise of the X2e device.
Before we dive into next steps, let’s recap where we left off:
Knowing that UART is present and access to the bootloader would be extremely valuable, we decided to revisit that thread.
Figure 1 shows the U-Boot boot process displayed while connected via UART connection. In some cases, it is possible to send keyboard input to the device during a set period (usually one to four seconds) when the bootloader presents the message, “Hit any key to stop autoboot,” which interrupts the boot process and drops the user into a U-Boot shell. On the X2e, this feature has been disabled by setting the U-Boot configuration parameter CONFIG_BOOTDELAY to 0.
Figure 1: Uninterruptable U-Boot
bootloader output
One attack that has been documented to be successful to disrupt autoboot is to manipulate the bootloader’s ability to access the flash storage during the boot process. In certain circumstances where the U-Boot bootloader is unable to access its own configuration, it fails into a default environment, which may be less restricted. We decided to see if this would be possible on the X2e.
These attacks, known as glitch attacks (or more officially known as fault-injection), are a type of side channel attack that attempts to cause a microcontroller unit (MCU) to skip instructions, perform wrong instructions, or fail to access flash memory. Various types of glitching attacks exist including electrical, thermal, and radiation. Based on our objective, we opted to try glitching the power between the MCU and the Spansion NAND flash. Note that glitch attacks can often cause damage to the components on a board or put the device in an unusable state. These types of attacks should be tested as either a last resort or against a secondary device you are comfortable with damaging.
Based on previous research in this domain, we opted to target the data lines (I/O) between the MCU and NAND flash. Recall from Part One that the NAND flash on the X2e was the Spansion S34ML01G1, which was a 63-pin ball grid array (BGA) package. This chip is capable of supporting both 8-bit and 16-bit bus width, which corresponds to the number of I/O lines utilized. By using the datasheet for the flash and then querying the ONFI Device ID of our chip, we determined our chip was utilizing the 8-bit configuration, meaning eight I/O lines were present between the NAND flash and the MCU. For this attack, we focused on manipulating the power on the first (I/O0) data line. Figure 2 shows the configuration of the BGA-63 pins, with I/O0 highlighted.
Figure 2: Identifying I/O0 for NAND chip
in the Spansion datasheet
Because the pins are actually underneath the flash package, we needed to find an exposed lead that corresponded to I/O0 elsewhere on the PCB. One such method for tracing connections across a PCB is a continuity test. A continuity test (using a multimeter) sends a low current electrical signal across two points and produces an audible beep if the points are connected. Using this technique, we located an exposed test point (known as a via) on the bottom of the PCB. Figure 3 shows the I/O0 pin on the top of the PCB (under the NAND chip), and Figure 4 shows the I/O0 pin exposed on the bottom of the PCB.
Figure 3: I/O0 on top of PCB (under NAND chip)
Figure 4: I/O0 on bottom of PCB
With exposed access to I/O0 located, we experimented with connecting this pin directly to a known ground (GND) pin at various points during the boot process. Figure 5 shows the device powering on with the metal tweezers connecting I/O0 to GND.
Figure 5: Shorting I/O0 to GND
While connected to the UART interface, we noted several different outcomes. When shorting the pin immediately after powering on, the device failed to produce any output or boot. When shorting after the bootloader finished loading (and handing off to the Linux kernel), the device would also force reboot. However, when timed perfectly between the bootloader loading and attempting to read its configuration, we noted that the bootloader would present different output, and the option to interrupt the boot process was possible with a four-second delay. By pressing keyboard input, we were successfully able to drop into a U-Boot shell, which is shown in Figure 6.
Figure 6: Access to U-Boot bootloader shell
While this was great progress, we noted that the current failback bootloader configuration was completely inoperable and certain NAND blocks had been marked as bad (as expected). To get our device back to a working state, we needed to revisit the NAND dump we generated in Part One.
While the current configuration provided us a working shell, we needed to fix the damage we had done. This was performed in two steps: fixing the mistakenly marked bad blocks and then rebuilding the configuration. In our case, the nand utility and its sub-commands read, write, and scrub allowed us to inspect and manipulate pages and blocks of the NAND. The nand scrub command with a valid offset and size could be used to completely reset a segment of the NAND, which removed any bad block markers. The next challenge was determining what needed to be replaced in the damaged blocks and rebuilding the configuration.
Since we had a valid NAND image, we revisited the sections read by the bootloader to determine what changes were needed. The format did not match a known format, so we wrote a simple parser in Python to read the binary structure, shown in Figure 7.
Figure 7: Parsing bootloader nvram
configuration from flash
With details of how the configuration should look, we used the nand write to rebuild this section, byte by byte with the correct details. We also set the boot delay to be four seconds, so that we could always interrupt the bootloader once the new configuration was committed. Once we confirmed our changes were stable, we saved the configuration to flash and could access the bootloader without performing the aforementioned glitch attack.
Now that we have unrestricted access to the bootloader, we can finally influence the rest of the boot process and achieve a privileged shell. We alluded to this in Part One, but the easiest way to turn an unlocked U-Boot shell into a root Linux shell is to adjust the boot arguments that U-Boot passes to the Linux kernel. In our case, this was accomplished by using the setenv utility to change the std_bootarg environment variable to be init=/bin/sh and instructing U-Boot to resume the standard boot process. Figure 8 shows the Linux shell presented over UART.
Figure 8: root shell after bootloader
At this point, we’ve demonstrated a repeatable method for achieving local privilege escalation. In the final segment, we’ll complete our attack by exploring an avenue to remotely escalate privileges.
Since the X2e has only two available listening network services, it makes sense to reinvestigate these services. During Part One, we identified hardcoded credentials for the limited user python. This was useful for initial probing of the device while it was running, but where do we go from here?
Embedded devices typically only have a handful of users, with a majority of functionality being performed by the root user. This presents an interesting opportunity for us to abuse overlap between actions performed by the root user on contents owned and controlled by the python user.
By reviewing the boot process, we noted a large number of custom init scripts in the /etc/init.d/ directory. These scripts are executed at system start by the root user and were responsible for starting daemons and ensuring directories or files exist. One file in particular, /etc/init.d/S50dropbear.sh, was interesting to us, as it appeared to perform a number of actions on files within the directory specified by the $PYTHON_HOME variable, which was /WEB/python/, shown in Figure 9.
Figure 9: Unsafe operations on
$PYTHON_HOME directory
At first glance this may seem benign but considering that the /WEB/python/ directory is controllable by the python user, it means that we can potentially control actions taken by root. More specifically, the chown operation is dangerous, as the previous mkdir command can fail silently and result in an unsafe chown operation. To weaponize this, we can use symbolic links to point the /WEB/python/.ssh/ to other areas of the filesystem and coerce the root process into chown’ing these files to be owned by the python user. The process we took to exploit this was as follows:
While not the cleanest approach (it requires two reboots), it accomplishes the goal of achieving code execution as root. Figure 10 shows the output of our proof of concept. In this case, our malicious init script spawned a bind shell on TCP port 8080, so that we could connect in as root.
Figure 10: Exploiting chown vulnerability
to gain shell as user root
And there we have it: a remote connection as root, by abusing two separate vulnerabilities. While not explored in this series, another viable avenue of attack would be to explore potential vulnerabilities in the web server listening on TCP ports 80 and 443; however, this was not an approach that we took.
We covered a wide variety of topics in this two-part series, including:
We hope that readers were able to learn from our experiences with the X2e and will be inspired to use these techniques in their own analysis. Finally, Mandiant would like to thank both Tesla/SolarCity and Digi International for their efforts to remediate these vulnerabilities and for their cooperation with releasing this blog series.
Click to Open Code Editor