ZDI-20-709: Heap Overflow in the NETGEAR Nighthawk R6700 Router

June 25, 2020 | Guest Blogger

Pwn2Own competitions often inspire people to research products and technologies, even if the researcher does not actively participate in the contest. Such is the case here, where the security researcher known as d4rkn3ss took a closer look at one of the routers that were eligible for Pwn2Own Tokyo 2019, the NETGEAR Nighthawk R6700v3. He reported four different bugs in this router, and we recently disclosed some details about the bugs as they were past their disclosure deadline date. He has graciously provided a full write up of one of these vulnerabilities, ZDI-20-709.


At Pwn2Own Tokyo 2019, wireless routers were introduced as a new category. One of the routers targeted during the competition was the NETGEAR Nighthawk R6700v3. While I was not at the contest, it did inspire me to look at the device and see if I could find any vulnerabilities. In addition to what was found at the contest, I discovered a heap overflow vulnerability in the router that could allow malicious third parties to take control of the device from a local area network. In this post, I discuss the vulnerability in detail and provide a proof-of-concept exploit that should work out of the box against any router running firmware version V1.0.4.84_10.0.58.

The vulnerability exists in the httpd service (/usr/bin/httpd) running on affected devices. Unauthenticated attackers can send a specially crafted HTTP request to the httpd web service when connecting to the local network, which could result in remote code execution on the target system. Successful exploitation of this vulnerability may result in the complete compromise of a vulnerable system. The heap overflow vulnerability exists in the file upload function processing an imported configuration file.

Background

First, I will talk briefly about the design of the handling of HTTP requests on the router. With regards to the design, the web service doesn’t listen directly on port 80. However, there is another process that acts as a proxy and does listen on port 80. This process is the NGINX proxy. I haven't gone deep into it yet, so I'm not sure if it's a version of the NGINX web server in common use. I will explain more details about its function below.

Next, when an HTTP request reaches the httpd service, the main processing function is sub_159E8(). The execution flow of this function is as follows:

Figure 1 - Execution flow of sub_159E8 function

To begin, the program reads the HTTP request from the socket. It then performs a check to determine if the HTTP request is in the form of a file upload request. If this check returns false, the sub_10DC4 function gets called. This function is in charge of parsing HTTP requests, performing authentication, dispatching requests, and so on. Conversely, if the HTTP request is in the form of a file upload request, the code part shown as X will be executed. We see that sub_10DC4 is the main function for handling requests. The code part X is outside that function, so that is an area that should interest us. The vulnerability I will present in this blog is found there.

The Vulnerability

As mentioned above, the vulnerability is triggered by an upload over HTTP. Upload requests are handled by the endpoint /backup.cgi. During my testing of this functionality, I found two separate issues affecting this endpoint. The first involves missing authentication checks. An attacker can upload a new configuration file without authenticating. Nevertheless, we cannot replace the target’s credentials or change the setting of the target system, as there are authentication checks before applying the new configuration settings. The second issue is a classic heap overflow vulnerability within the file upload functionality.

The vulnerable function copies the contents of the uploaded file into a heap-based buffer of attacker-controlled size. The following is pseudo-code of the vulnerable function:

Figure 2 - Pseudo-code of the vulnerable function

To control the size of the heap-based buffer, an attacker can make use of a Content-Length header, but it’s not straightforward. Let’s go a bit deeper and explain why.

The HTTP Request to import a configuration file is as follows:

Figure 3 - HTTP request to import a configuration file

Figure 3 - HTTP request to import a configuration file

The HTTP request must satisfy several conditions. First, the URI must contain one of the following strings: backup.cgi, genierestore.cgi, or upgrade_check.cgi. Next, the request must be a multipart/form-data request with header name="mtenRestoreCfg. Finally, the filename cannot be an empty string. However, according to the design, the HTTP request must go to an NGINX proxy before being passed to the httpd service. The policy_default.conf configuration file of the NGNIX proxy is as follows:

Figure 4 - NGINX configuration

Figure 4 - NGINX configuration

Therefore, to bypass the NGINX proxy, I chose this URI:

Figure 5 - URI to bypass proxy

The processing of the file upload occurs in the sub_159E8 function. From here, the program extracts the Content-Length value from the header:

Figure 6 - Content-Length extraction

The above code snippet first locates the Content-Length header within the entire HTTP request by using the stristr function, and then extracts and converts the value of the header from string to an integer by a loop via a minimal implementation of the atoi function:

Figure 7 - Loop to convert string to integer

However, we cannot directly pass an arbitrary value to the Content-Length header because of the NGINX proxy. Besides filtering requests, the proxy also rewrites requests. It makes sure that Content-Length value equals the size of the post data, and it puts the Content-Length header in the first header of the request. Therefore, we can’t forge the Content-Length header in another header. However, the logic for the extraction of the Content-Length header is flawed. It performs the stristr function on the entire HTTP request instead of just the request headers! As such, it is possible to place a Content-Length header in the URI that will be interpreted by httpd service as follows:

Figure 8 - URI to forge Content-Length value

Figure 8 - URI to forge Content-Length value

Since the request line appears before the HTTP headers, with the above URI, the string passed to code in Figure 7 is 111 HTTP/1.1. In this way, we can completely control the value of Content-Length and trigger the integer overflow vulnerability.

By the way, one amusing thing about the atoi implementation in Figure 7 above is that it doesn’t stop when it hits non-numeric characters. Instead, it continues until it finds the newline sequence \r\n, parsing any other character it finds as if it were a decimal digit. To determine the numerical value of each character, the ASCII character code for the digit 0 is subtracted from the character’s code. This formula yields the expected values when parsing digits 0 through 9. When parsing non-digit characters, it produces invalid results. For example, when parsing the space character (ASCII 0x20), it calculates that its value as a digit is 0x20 – 0x30, or 0xfffffff0. Due to the invalid calculations, the string 111 HTTP/1.1 in the example above produces a final computed value of 0x896ebfe9! To exert control over this value, I used a brute force program that substitutes various Content-Length values and simulates the atoi loop until an appropriate value is found. The solution it produced was 4156559 HTTP/1.1, which evaluates to ffffffe9, which is a nice, reasonably sized negative value.

Continuing down the code path:

Figure 9 - Integer Overflow vulnerability

First, the program compares the value of Content-Length with 0x20017 using an unsigned comparison. If the value is greater than 0x20017, the assembly code at address 0x17370 will be executed. Then, the value stored in dword_19A08 and dword_19A104 is equal to 0 because of the import configuration request. Next, the program checks the value of the pointer stored in dword_1A870C. If this value isn’t equal to zero, the memory held by this pointer will be freed. Then, the program allocates memory for storing the file content by calling malloc, passing the value of Content-Length plus 0x258. The result is stored in dword_1A870C. Because we can completely control the value of Content-Length, we can trigger an integer overflow vulnerability here by setting the Content-Length value to a negative number.

Next, the program copies the entire file contents to the buffer allocated above. This results in a heap overflow vulnerability.

Figure 10 - Heap Buffer Overflow Vulnerability

Exploit Considerations

Here are a few things to consider as we craft the exploit:

        -- We have a heap overflow vulnerability that allows us to write arbitrary data to heap memory, including null bytes.
        -- Because of the poor implementation of ASLR, heap memory is located at a constant address.
        -- uClibc is used in the system. This is a minimal libc version of glibc, so the malloc and free functions have simple implementations.
        -- After calling memcpy()and achieving a heap overflow, sub_21A58() will be called to return an error page. In sub_21A58(), fopen() is called to open a file. In fopen(), malloc() is called twice, with sizes 0x60 and 0x1000 respectively. Each of these allocations is freed afterwards. In summary, the sequence of memory allocations and frees will be as follows:

Figure 11 – Sequence of memory allocation operations

Additionally, we can send an Import String Table request to invoke another malloc and free in sub_95AF4(). This is the function used to calculate the checksum of the String Table Upload file. The pseudo-code is as follows:

Figure 12 – Pseudo-code from sub_95AF4()

The HTTP request to import the string table is as follows:

Figure 13 - Import string table HTTP request

Exploit Technique

The heap buffer overflow gives us the ability to conduct a fastbin dup attack. “Fastbin dup” is a type of attack that corrupts the state of the heap so that a subsequent call to malloc returns a chosen address. Once malloc has returned a chosen address, we can write arbitrary data to that address (a write-what-where). Overwriting a GOT entry then yields remote code execution. In particular, we can overwrite the GOT entry for free(), redirecting it to system(), so that a buffer containing attacker-provided data will be executed by the shell.

However, in our case, it’s not easy to conduct the fastbin dup attack. Recall that, with each request, an additional malloc(0x1000) call occurs. This produces a call to __malloc_consolidate() function, destroying the fastbin.

As mentioned above, the system uses the uClibc library, so the free() and malloc() functions are quite different from glibc’s implementation. Here’s a look at the free() function:

Figure 14 – Implementation of free() in uClibc

At line 22, note the lack of a bounds check while accessing the fastbins array. This can lead to an out-of-bounds write to the fastbins array.

Checking the malloc_state struct and the fastbin_index macro, both are defined in malloc.h:

Figure 15 - malloc_state struct and fastbin_index macro definition

Figure 15 - malloc_state struct and fastbin_index macro definition

The max_fast variable is located immediately before the fastbins array. Therefore, if we set the size of a chunk to be 8, then when this chunk is freed, fastbin_index(8) will return a value of -1 and max_fast will be overwritten by a large value (a pointer). Note that a chunk never has a size of 8 when the heap is functioning correctly. This is because the metadata that is part of a chunk takes up 8 bytes, so a size of 8 would imply that there are zero bytes of user data.

Once max_fast has been changed to a large value, __malloc_consolidate() will no longer be called during malloc(0x1000). This allows us to proceed with the fastbin dup attack.

In summary, the exploit process is as follows:
       -- Issue a request triggering the heap overflow vulnerability, overwriting the PREV_INUSE flag of a chunk so that it incorrectly indicates that the previous chunk is free.
       -- Due to the incorrect PREV_INUSE flag, we can get malloc() to return a chunk that overlaps an actual existing chunk. This lets us edit the size field in the existing chunk’s metadata, setting it to the invalid value of 8.
       -- When this chunk is freed and placed on the fastbin, malloc_stats->max_fast is overwritten by a large value.
       -- Once malloc_stats->max_fast has been changed, __malloc_consolidate() will no longer be called during calls to malloc(0x1000). This allows us to proceed with the fastbin attack.
       -- Trigger the heap overflow vulnerability again, overwriting the fd (forward) pointer of a free fastbin chunk with a chosen target address.
       -- A subsequent call to malloc() will return our chosen target address. We can use this to write the chosen data to the target address.
       -- Use this write-what-where primitive to write to address free_got_addr. The data we write there is system_plt_addr.
       -- Finally, when freeing a buffer containing an attacker-supplied string, system() is called instead of free(), producing remote code execution.

The layout of heap memory and step-by-step exploitation process are in the PoC file below.

Conclusion

As of the publication of this blog, the vendor has stated, “NETGEAR plans to release firmware updates that fix these vulnerabilities for all affected products that are within the security support period.” They do have a hotfix in beta that can be downloaded from here. This has not been tested to see if it sufficiently addresses the root cause of this vulnerability. The ZDI published its advisory on June 15. In their disclosure, they note, “Given the nature of the vulnerability, the only salient mitigation strategy is to restrict interaction with the service to trusted machines. Only the clients and servers that have a legitimate procedural relationship with the service should be permitted to communicate with it. This could be accomplished in several ways, most notably with firewall rules/whitelisting.” Until the patch is available, this is the best advice to minimize risk while using this device.


Thanks again to the security researcher known as d4rkn3ssor providing this great write-up. Hopefully, he will decide to participate in a future Pwn2Own event and submit more bugs. Until then, follow the team for the latest in exploit techniques and security patches.