After reading online the details of a few published critical CVEs affecting ASUS routers, we decided to analyze the vulnerable firmware and possibly write an n-day exploit. While we identified the vulnerable piece of code and successfully wrote an exploit to gain RCE, we also discovered that in real-world devices, the “Unauthenticated Remote” property of the reported vulnerability doesn’t hold true, depending on the current configuration of the device.
Last year was a great year for IoT and router security. A lot of devices got pwned and a lot of CVEs were released. Since @suidpit and I love doing research by reversing IoT stuff, and most of those CVEs didn’t have much public details or Proof-of-Concepts yet, we got the chance to apply the CVE North Stars approach by clearbluejar.
In particular, we selected the following CVEs affecting various Asus SOHO routers:
The claims in the CVEs descriptions were pretty bold, but we recalled some CVEs published months before on the same devices (eg. CVE-2023-35086) that described other format string in the same exact scenario:
“An unauthenticated remote attacker can exploit this vulnerability without privilege to perform remote arbitrary code execution”
Take careful note of those claims cause they will be the base of all our assumptions from now on!
From the details of the CVEs we can already infer some interesting information, such as the affected devices and versions. The following firmware versions contain patches for each device:
Also, we can learn that the vulnerability is supposedly a format string, and that the affected modules are
Since we didn’t have any experience with Asus devices, we started by downloading the vulnerable and fixed firmware versions from the vendor’s website.
Once we got hold of the firmware, we proceeded by extracting them using Unblob.
By doing a quick
ripgrep search we figured out that the affected modules are not CGI files as one would expect, but they are compiled functions handled inside the
We then loaded the new and the old httpd binary inside of Ghidra, analyzed them and exported the relevant information with BinDiff’s BinExport to perform a patch diff.
A patch diff compares a vulnerable version of a binary with a patched one. The intent is to highlight the changes, helping to discover new, missing, and interesting functionality across various versions of a binary.
Patch diffing the
httpd binary highlights some changes, but none turned out to be interesting to our purpose.
In particular, if we take a look at the handlers of the vulnerable CGI modules, we can see that they were not changed at all.
Interestingly, all of them shared a common pattern. The input of the
notify_rc function was not fixed and was instead coming from the user-controlled JSON request. :money_with_wings:
notify_rc function is defined in
/usr/lib/libshared.so: this explains why diffing the
httpd binary was ineffective.
libshared.so resulted in a nice discovery: in the first few lines of the
notify_rc function, a call to a new function named
validate_rc_service was added.
At this point we were pretty much confident that this function was the one responsible to patch the format string vulnerability.
validate_rc_service function performs a syntax check on the
rc_service JSON field.
The Ghidra decompiled code is not trivial to read: basically, the function returns 1 if the
rc_service string contains only alphanumeric, whitespace, or the
; characters, while returns 0 otherwise.
Apparently, in our vulnerable firmware, we can exploit the format string vulnerability by controlling what ends up inside the
rc_service field. We didn’t have a device to confirm this yet, but we didn’t want to spend time and money in case this was a dead-end. Let’s emulate!
If you know us, we bet you know that we love Qiling, so our first thought was “What if we try to emulate the firmware with Qiling and reproduce the vulnerability there?”.
Starting from a Qiling skeleton project, sadly
httpd crashes and reports various errors.
In particular, the Asus devices use an NVRAM peripheral to store many configurations. The folks at firmadyne developed a library to emulate this behavior, but we couldn’t make it work so we decided to re-implement it inside of our Qiling script.
The script creates a structure in the heap and then hijacks all the functions used by
httpd to read/write the NVRAM redirecting the to the heap structure.
After that we only had to fix some minor syscalls’ implementation and hooks, and voilà! We could load the emulated router web interface from our browsers.
In the meantime we reversed the
do_set_iperf3_cli_cgi functions to understand what kind of input should we send along the format string.
Turns out the following JSON is all you need to exploit the
And we were welcomed with this output in the Qiling console:
At this point, the format string vulnerability was confirmed, and we knew how to trigger it via firmware emulation with Qiling.
Moreover, we knew that the fix introduced a call to
validate_rc_message in the
notify_rc function exported by the
libshared.so shared library.
With the goal of writing a working n-day for a real device, we purchased one of the target devices (Asus RT-AX55), and started analyzing the vulnerability to understand the root cause and how to control it.
Since the fix was added to the
notify_rc function, we started by reverse engineering the assembly of that function in the old, vulnerable version. Here follows a snippet of pseudocode from that function:
The function seems responsible for logging messages coming from various places through a single, centralized output sink.
logmessage_normal function is part of the same library and its code is quite simple to reverse engineer:
While Ghidra seems unable to recognize ✨automagically✨ the variable arguments list, the function is a wrapper around
syslog, and it takes care of opening the chosen log, sending the message and finally closing it.
The vulnerability lies in this function, precisely in the usage of the
syslog function with a string that can be controller by the attacker. To understand why, let us inspect the signature of it from the libc manual:
void syslog(int priority, const char *format, ...);
According to its signature,
syslog expects a list of arguments that resembles those of the
*printf family. A quick search shows that, in fact, the function is a known sink for format string vulnerabilities.
Format string vulnerabilities are quite useful for attackers, and they usually provide arbitrary read/write primitives. In this scenario, since the output is logged to a system log that is only visible to administrators, we assume an unauthenticated remote attacker should not be able to read the log, thus losing the “read” primitive of the exploit.
ASLR is enabled on the router’s OS, and the mitigation implemented at compile-time for the binary are printed below:
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x10000)
According to this scenario, a typical way of developing an exploit would consist in finding a good target for a GOT Overwrite, trying to find a function that accepts input controlled by the user and hijacking it to
Nevertheless, in pure Living Off The Land fashion, we spent some time looking for another approach that wouldn’t corrupt the process internals and would instead leverage the logic already implemented in the binary to obtain something good (namely, a shell).
One of the first things to look for in the binary was a place where the
system function was called, hoping to find good injection points to direct our powerful write primitive.
Among the multiple results of this search, one snippet of code looked worth more investigation:
Let’s briefly comment this code to understand the important points:
SystemCmd is a global variable which holds a string.
sys_script, when invoked with the
syscmd.s argument, will pass whatever command is present in
SystemCmd to the
system function, and then it will zero out the global variable again.
This seems a good target for the exploit, provided we can, as attackers:
Point 1 is granted by the format string vulnerability: since the binary is not position-independent, the address of the
SystemCmd global variable is hardcoded in the binary, so we do not need leaks to write to it.
In our vulnerable firmware, the offset for the
SystemCmd global var is
Regarding point 2, some endpoints in the web UI are used to legitimately execute commands through the
sys_script function. Those endpoints will call the following function named
ej_dump whenever a GET request is performed:
So once the
SystemCmd global variable is overwritten, simply visiting
Main_Netstat_Content.asp will trigger our exploit.
We will spare you a format string exploitation 101, just remember that with
%n you can write the number of characters written so far at the address pointed by its offset.
It turned out we had a few constraints, some of them typical of format string exploits, while others specific to our scenario.
The first problem is that the payload must be sent inside a JSON object, so we need to avoid “breaking” the JSON body, otherwise the parser will raise an error. Luckily, we can use a combination of raw bytes inserted into the body (accepted by the parser), double-encoding (
%25 instead of
% to inject the format specifiers) and UTF-encode the nullbyte terminating the address (
The second one is that, after being decoded, our payload is stored in a C string so null-bytes will terminate it early. This means we can only have one null-byte and it must be at the end of our format string.
The third one is that there is a limit on the length of the format string. We can overcome this by writing few bytes at a time with the
The fourth one (yes, more problems) is that in the format string there is a variable number of characters before our input, so this will mess with the number of characters that
%hn will count and subsequently write at our target address. This is because the
logmessage_normal function is called with the process name (either
httpsd) and the pid (from 1 to 5 characters) as arguments.
Finally, we had our payload ready, everything was polished out perfectly, time to perform the exploit and gain a shell on our device…
Sending our payload without any cookie results into a redirect to the login page!
At this point we were completely in shock. The CVEs report “an unauthenticated remote attacker” and our exploit against the Qiling emulator was working fine without any authentication. What went wrong?
While emulating with Qiling before purchasing the real device, we downloaded a dump of the NVRAM state from the internet. If the
httpd process loaded keys that were not present in the dump, we automatically set them to empty strings and some were manually adjusted in case of explicit crash/Segfault.
It turns out that an important key named
x_Setting determines if the router is configured or not. Based on this, access to most of the CGI endpoints is enabled or disabled.
The NVRAM state we used in Qiling contained the
x_Setting key set to 0, while our real world device (regularly configured) had it set to 1.
But wait, there is more!
We researched on the previously reported format string CVEs affecting the other endpoints, to test them against our setup. We found exploits online setting the Referer and Origin headers to the target host, while others work by sending plain GET requests instead of POST ones with a JSON body. Finally, to reproduce as accurately as possible their setup we even emulated other devices’ firmware (eg. the Asus RT-AX86U one).
None of them worked against an environment that had
x_Setting=1 in the NVRAM.
And you know what? If the router is not configured, the WAN interface is not exposed remotely, making it unaccessible for attackers.
This research left a bitter taste in our mouths.
At this point the chances are:
Anyway, we are publishing our PoC exploit code and the Qiling emulator script in our poc repository on GitHub.
If you know about an (un)fixed authentication-bypass (😉) in Asus devices drop us a message on X (formerly Twitter) or BlueSky. We would also love to hear a comment from Asus and/or from the researchers that reported the CVEs about their reasoning and considerations.
Are you developing embedded / IoT systems? Take security seriously! We can help you doing full-stack security testing from glitching your Secure Boot checks up to gaining code execution on your MQTT server. Get in touch with us to learn more.
30 gennaio 2024
Security Researcher e Senior Penetration Tester in Shielder.
In ufficio sono quello che usa il saldatore. Non solvo nessuna Crypto.
Security Researcher e Penetration Tester in Shielder. Umano, Caotico Buono. Seguace del Bushido e della Disney.