This study concerns the firmware of the Wavlink Wireless-AC1200 Gigabit router as of June 2020. The vulnerabilities discussed here may or may not have been patched by the vendor, but this is a case of vulnerability research that will show the journey as the reward rather than its conclusion. The author conducted this research before the public disclosure of the vulnerabilities but after they had been independently discovered by other researchers and reported to the vendor.
The vendor makes firmware for their products available in the support section of their website; this is a common way to obtain IoT firmware and a helpful alternative from extracting it from the device memory. The firmware is unencrypted, so it can be easily extracted with binwalk. Dynamic analysis was done with access to a physical exemplar of the device, and static analysis was done through Ghidra.
The web interface of the Wavlink Wireless-AC1200 Gigabit router has several vulnerable endpoints that allow for the unrestricted copy of user-provided data onto the application stack or even directly to the command line to achieve arbitrary command execution.
Initial scans of the device suggest that the only exposed resource was the administrative web console, accessible to authenticated users on the LAN interface over HTTP on TCP port 80. The device can offer more services, but these are not enabled out-of-the-box and by default. As such, this investigation focuses on the web interface alone.
Figure 1 - An nmap scan of the exemplar device, showing only the web interface listening.
The usual tests — such as the typical command injections found on device diagnostic panels that allow a command injection in the parameters to a ping or traceroute
command — did not yield any immediately interesting results, which was disappointing.
The first (ultimately exploitable) interface that was examined here was found on the “USB storage” panel, which can be seen as the second option in Figure 3. The device has a USB port adjacent to its 802.2 Ethernet plugs, suggesting that it could offer network-attached storage (NAS) functionality.
Figure 2 - A photograph of the actual exemplar, showing USB availability.
A simple principle in vulnerability research is that the more components a piece of code interacts with and the more moving parts it has, the more likely there will be exploitable code lurking nearby. The presence of NAS capabilities is promising because it indicates the presence of code that simultaneously interacts with the device’s software layer, hardware layer, and plugged-in periphery (the USB storage itself).
Figure 3 - Management options available after authenticating to the administrative web panel. “USB Storage” can be seen as the second option.
The management interface for the USB Storage console is shown in Figure 4. The presence of the “Workgroup” field alone is promising, as this would suggest that this WiFi router may even attempt to interact over Server Message Block (SMB) — a big lift for an IoT router. I can’t count the number of times I’ve seen user-supplied input sent directly to the command line as an argument to the Unix smbpasswd
function.
Figure 4 - USB Storage options available to authenticated users.
Initial attempts to manipulate these settings failed due to the device not detecting a USB drive, as Figure 5 demonstrates. However, once an adequately formatted drive was plugged in, the device allowed an FTP username and password to be set. As suspected, it placed this user-provided input on the command line, as shown in Figure 6.
Figure 5 - Configuration changes to USB Storage options will not be saved unless an adequately formatted drive is manually plugged into the device’s USB port.
Figure 6 - A command injection in the ‘password’ field nets the first shell access directly to the device’s operating system.
While interesting, this vulnerability is challenging to get overwhelmingly excited about: it requires not only credentialed access to the administrative interface of the device but physical access to the device to manipulate its USB drive. The above exploit allows a researcher to interact with the individual operating system components (and exfiltrate them for reverse engineering purposes).
The device was a busy box-centric Linux system with a web interface powered by Lighttpd. The Common Gateway Interface (CGI) functionality was provided by individual binaries in /etc_ro/lighttpd/www/cgi-bin/, with web requests to CGI URIs launching these binaries directly. A quick look at nas.cgi
in Ghidra shows the command injection on line 38 in Figure 7, which sends a user-supplied password directly to the do_system function (itself simply a wrapper around the standard libc system call). Looking through the /cgi-bin/ directory reduces the task of finding a more interesting exploit to enumerating the CGI interfaces available to the user, shown in their completion in Figure 8.
Figure 7 - User input is placed on the command line as an argument to the chpasswd.sh script on Line 38, resulting in a command injection.
Several things stand out on the initial examination. The first important thing to note is that the CGI binaries often call the check_valid_user
function. This method looks to see if the IP address making the request is stored in a particular temporary file on the file system. Minimal testing shows that a client’s authentication status is not verified until a call to this method has been made, so the entirety of the code surface in each CGI binary before the invocation of this function is accessible unauthenticated.
Figure 9 – The decompilation of adm.cgi showing the check_valid_user method. All of the code prior to this call is executed prior to verifying the requestor’s authentication status.
Another interesting observation is the large number of methods that copy user-provided input directly onto the stack. For instance, the decompilation of wireless.cgi
shows the NewName
parameter argument taken from the body of a web request on Lines 14 and 15 followed by an unguarded strcpy
of this user input onto the stack on Line 34, as shown in Figure 10.
Figure 10 – Lines 14 and 34 demonstrate an unguarded strcpy of the NewName parameter from the request body directly onto the program stack.
This copy of user input onto the stack alone suggests a memory corruption exploit, which the following command can verify:
curl –XPOST \ http://target-ip/cgi-bin/wireless.cgi |
While promising, a return-oriented attack isn’t optimal here. Using the shell access to the operating system already obtained, the following command displays ‘1’, showing that Address Space Layout Randomization (ASLR) is weakly-enforcing, leaving a ROP chain a 1-in-256 chance of landing on the desired gadget:
cat /proc/sys/kernel/randomize_va_space 1 |
Furthermore, the command below shows that the little-endian binaries are compiled with the stack-guard byte set, so only the last gadget in a chain can land in a predetermined location within the binary. If a watchdog process is present to restart the web server after a crash then it’s not out of the question to repeatedly throw a return-oriented exploit and expect success eventually, but it’s also possible that there are better bugs lurking elsewhere in the code.
xxd /etc_ro/lighttpd/www/cgi-bin/wireless.cgi | head -n 10 |
Continuing to look through the CGI functions one will eventually examine live_api.cgi. This binary makes no call to check_valid_user, so any web requests to the URI /cgi-bin/live_api.cgi run this CGI application unauthenticated. Line 9 in Figure 11 shows that the QUERY_STRING environment variable, which (according to the Apache CGI specification) is the portion of the request URI immediately following a question mark and therefore user-provided, is stored in pcVar1
and on Line 19 of Figure 11 is sent to the satellite_status
method.
Figure 11 – A decompilation of live_api.cgi showing user input taken from the URI on Line 9 and being sent to satellite_status on Line 19.
Decompilation of the satellite_status
method, shown in Figure 12, shows that the query string itself (now param_1) is parsed for the page, id, and ip parameters. The ip parameter is copied via the sprintf function to a local variable on Line 38 of Figure 12 and, on Line 39, is passed to the do_system function. The absence of a call to check_user_auth indicates that arbitrary client input from an unauthenticated user in the URI will be placed directly onto the command line in the ip URI parameter, confirmed in Figure 13.
Figure 12 – A decompilation of the satellite_status function, which shows the query string (now param_1) parsed for the ip parameter on Lines 22 and 23, then to a call to do_system on lines 38 and 39.
Figure 13 – The proof is in the pudding, showing the exploit being used to effect a remote takeover of the device.
And there it is: a remote, unauthenticated (save for the LAN interface) exploit for an off-the-shelf WiFi router, from beginning to end.
Finding exploits in a fresh-to-the-market IoT device may seem like low-hanging fruit, as was mentioned at the beginning of this post, but the value of this investigation was in its journey rather than its destination.
An understanding of the circumvention of authentication and the location of the command injection would have been unlikely, or even impossible, without static analysis of the firmware. Were it not available online, it would have required dynamic access to the device’s operating system to obtain them, for which there would have been dim hopes dynamic access to the device. This level of access itself relied on both physical access to the device, and either specialized hardware or the guided guess as to the likely places in code that mistakes might have been made.
We hope you enjoyed this journey, and that you come back for more. Happy hacking!
Author:
David Baker, Senior Security Consultant, Testing, K logix