In this installment of our Breaking BHAD series we explore how to gain arbitrary command execution as root on WeMo devices with a SQL injection vulnerability in the rule updating process. We demonstrate this by obtaining a root shell over telnet, but could just as easily have downloaded and executed any custom code compiled for the MIPS architecture, like a botnet. Of note is that the exploit must be launched from a system on the same network as the target device. This includes scenarios where an attacker has compromised a system on the network via phishing or drive-by download, as well as when the attacker is physically present on the same network.
This vulnerability was patched as of November 1, 2016 in WeMo firmware versions 10884 or 10885, depending on the device.
Rules are meant to be broken
One piece of functionality that makes WeMo devices popular is the ability to create per device rules. The list of possible rules differs from device to device, but generally they allow a user to trigger a change in a device’s behavior based on some criteria, like time. For example, a WeMo Switch that is connected to a lamp might have a rule to turn off every evening at 10pm, thereby ensuring that the user never forgets to turn off the lights before they go to bed. Another example is turning the WeMo Slow Cooker from medium to low heat after 5 hours of cooking to ensure that dinner stays warm, but doesn’t overcook.
Rules are created, modified, and deleted with an interface in the mobile app (Figure 1). Internal to the app the rules are stored in a SQLite database, and any changes made to the rules are just updates to the tables in this database. When the user is done editing the rules for a device, the SQLite database file is compressed and uploaded to the device. Upon receipt of the rules file the device uncompresses it and uses a set of SQL queries to pull rule information from the new database and update its in-memory rules. We found that these SQL queries are vulnerable to SQL injection.
The Vulnerability
The libUPnPHndlr.so library contains the code that is responsible for updating the rules on the device using the SQLite database sent from the app. After reverse engineering this library we are able to summarize the vulnerable part of the rule updating algorithm as follows:
LoadRulesInMemory() { // Build query to retrieve the Type and RuleID of enabled rules snprintf(query, 256, "SELECT Type, RuleID FROM RULES WHERE STATE="1""); // Execute Query table <- WeMoDBGetTableData(query); // Call FetchTargetDeviceID() on each enabled rule foreach row in table: FetchTargetDeviceId(row[‘RuleID’]); } FetchTargetDeviceId(char *RuleID) { // Build vulnerable query using RuleID snprintf(query, 256,"SELECT DeviceID FROM devicecombination WHERE SensorID="%s" AND RuleID="%s" limit 1;",g_RulesDB, RuleID); // Execute Query WeMoDBGetTableData(query); }
First, the LoadRulesInMemory() function executes a SQL query to retrieve the Type and RuleID of all enabled rules (i.e. STATE=”1”) listed in the RULES table (Line 3). Next, each of the RuleID values returned from this query are fed to the FetchTargetDeviceId() function (Lines 9-10), which uses the RuleID to build a query to retrieve the DeviceID from the devicecombination table (Lines 15-16). Below is an benign example of this updating process, beginning with a RULES table that contains one rule of type “Time Interval” and having a RuleID of 1. The RuleID is bolded to make it easier to trace the path from the RULES table to the final query string.
+--------------------------------------------------------------------------+ |RuleID|Name |Type |RuleOrder|StartDate |EndDate |State|Sync | |------|----------|-------------|---------|----------|--------|-----|------| | 1 |Timer Rule|Time Interval| 2 | 12201982 |07301982| 1 |NOSYNC| +--------------------------------------------------------------------------+ SELECT Type, RuleID FROM RULES WHERE STATE="1"; +--------------------+ |Type |RuleID| |-------------|------| |Time Interval| 1 | +--------------------+ SELECT DeviceID FROM devicecombination WHERE SensorID="g_RulesDB" AND RuleID="1" limit 1;
The vulnerability arises from the use of the format specifier “%s” to substitute the value of the RuleID column into the second query string (Line 16 in update algorithm), without first sanitizing the value of the RuleID column. To see why that is a problem observe the effect of building that second query when the value of the RuleID column is a carefully crafted malicious value.
+--------------------------------------------------------------------------+ |RuleID|Name |Type |RuleOrder|StartDate |EndDate |State|Sync | |------|----------|-------------|---------|----------|--------|-----|------| | ";-- |Timer Rule|Time Interval| 2 | 12201982 |07301982| 1 |NOSYNC| +--------------------------------------------------------------------------+ SELECT Type, RuleID FROM RULES WHERE STATE="1"; +--------------------+ |Type |RuleID| |-------------|------| |Time Interval| ";-- | +--------------------+ SELECT DeviceID FROM devicecombination WHERE SensorID="g_RulesDB" AND RuleID="";--" limit 1;
By setting the value of RuleID to begin with a double-quote the call to snprintf creates a SQL query that attempts to match on an empty RuleID. Furthermore, the semicolon ends the current SQL query and the dash-dash serves to comment out the rest of the line. This textbook SQL injection attack won’t do much harm in its current form, but could be much more pernicious if additional SQL queries are stack after the semicolon and before the dash-dash.
The astute observer might immediately cry foul of this example, noting that RuleID is supposed to be an integer. Indeed, every rule we created with the smartphone app gave integer RuleID values, and we even found other SQL query format strings that imply RuleID should be an integer by using the format specifier %d:
SELECT DeviceID FROM devicecombination where RuleID="%d";
However, SQLite does not enforce column type constraints , so there is no way to ensure that the value of the RuleID column is an integer unless additional checks are added to the code after the value is retrieved from the database. If the format string had used %d instead of %s, then it would have simply converted RuleID string to some nonsensical integer and the query would have either failed or returned no results. While it is unclear the effect that would have had on the state of the device, it would have prevented the SQL injection. Similarly, and more correctly, using the sqlite3 snprintf() function provided by the SQLite library would sanitize the value by escaping quote and double-quote characters in character string values.
The Payload
The most obvious way execute arbitrary code with a SQLite injection vulnerability is to attempt to load a shared library with the load_extension() functionality. However, the libsqlite3.so library disables this functionality by default, because it’s an obvious security vulnerability. Another technique found in some SQLite injection cheat sheets is to use an ATTACH DATABASE query to create a SQLite file in the web server’s root directory, and populate it with PHP code that the PHP interpreter will execute when the file is requested.
ATTACH DATABASE ‘/var/www/lol.php’ AS lol; CREATE TABLE lol.pwn (dataz text); INSERT INTO lol.pwn (dataz) VALUES (‘<?system($_GET[‘cmd’]); ?>’);--
ATTACH DATABASE first looks to see if a file exists at the specified location. If so, then it attempts to open it as a SQLite database. If not, then it will create the file and leave it open for reading and writing as a SQLite database. In the example above the file lol.php is create in /var/www/ and accessible via the database name lol. Then, a table called pwn is created in that database and a string is inserted into the table. When the file is requested via the web server the php file extension will cause the web server to invoke PHP interpreter. The interpreter searches through the file for token <?
and attempts to interpret the text after that and until the terminating ?>
token as PHP code. In this case, the code will execute whatever is in the cmd
GET request variable as a command on the system. That’s quite a handy way to go from SQL injection to arbitrary command execution. The only problem is that WeMo devices don’t have the PHP command interpreter. However, it did give us an idea.
WeMo devices are based on the OpenWRT embedded Linux distribution, which uses the BusyBox tool suite to implement most of the basic Linux commands. By default BusyBox uses the ash shell to implement /bin/sh. So we decided to see if we could create a SQLite database file that can be executed as an ash shell script using only SQL statements. This is trickier than the PHP case, because ash’s command parser is more complicated than PHP. However, it is significantly easier than attempting this feat with a more complicated command interpreter, like bash. As this is a new technique that other security researchers may wish to leverage we’ve documented it separately here. The malicious database that we created executes telnetd -l /bin/sh
, which drops any connection into a root shell without asking for a username or password.
Gaining Execution
Now that we have a way to use the SQL injection vulnerability to create a valid shell script on the device, the next step is to figure out how to execute that file. The big hurdle to overcome is the fact that ATTACH DATABASE does not create the file with execute permissions. So whatever method we discover must not depend on executing the file directly. After spending a lot of time searching through the device’s startup scripts we noticed that several of them used this function called include
followed the name of a directory. The include
function’s definition is in /etc/functions.sh
and is as follows:
Essentially, it will search the specified directory for files with a .sh
extension and source them into the calling file. What that means is that ash will read each file’s contents and execute it line by line, regardless of whether or not the file has execute permissions. That’s exactly what we need. All we have to do is find a directory that is passed to the include
function and create our shell script there.
In /etc/init.d/network the start() function calls include() and passes it the /lib/network directory (Figure 7). If we create our script in that directory, then when the device boots our script will execute. Unfortunately, to have a reliable exploit we can’t just wait for the device owner to reboot their device. Sending the StopPair message to the WifiSetup1 UPnP endpoint will cause the networking to restart, and call our script in the process.
Connecting the dots
Figure 4 presents the actual malicious database file that will be uploaded to the device. It contains two rows, where the RuleID column of each row injects a set of SQL statements. The first row uses the ATTACH DATABASE query to create a SQLite database called pwn.sh
in /lib/network
directory. A second statement in that same row then creates the echo table in the pwn.sh
database. The second row inserts a value into the echo table that when executed will start telnetd, and immediately drop any connection into an unauthenticated root shell.
Finally, Figure 5 shows the series of UPnP messages needed to upload this database and execute the file, and Figure 6 shows the end result.
Takeaways
Arbitrary Command Execution
While we chose to execute telnetd to demonstrate that we could achieve a root shell, an attack could use this exploit to run any command as root on the device. A more realistic attack would install a botnet, such as Mirai, or some other kind of malware that allows the attacker persistent access to the device.
Attackers have more access than device owner
What’s particularly interesting about an attacker gaining root access to a WeMo device is that the attacker now has more access to the device than its owner. By design, there is no way for the user to get a root console on the device, or execute arbitrary commands. In fact, the only way to remove the attacker’s access to the device is with a firmware update. Simply power cycling the device or using the factory-reset feature will not remove the file that has been written to the device. Since the release of firmware updates are determined by the vender, there is really nothing the user can do if they find that their devices has been compromised. What’s worse is that once the attacker has root access it’s trivial to break the firmware update process and prevent the user from ever fixing their device.
The shaky foundations of IoT
The SQL injection vulnerability only gave us an arbitrary write primitive. It was the ash shell in BusyBox that provided the ability to execute commands, and the design of OpenWRT that allowed us to trigger execution. The latter two are part of an open source ecosystem on which many IoT products are based. Therefore, it would be wrong to view this vulnerability as simply a secure coding failure. Vendors need to pay as much attention to securing the base software of their devices as they do attending to secure coding practices, like input sanitization.