Last year, we disclosed two authentication bypass vulnerabilities, ZDI-20-1176 (ZDI-CAN-10754) and ZDI-20-1451 (ZDI-CAN-11355), affecting multiple NETGEAR products. Both of the vulnerabilities resided in the mini_httpd webserver. These vulnerabilities were discovered by an anonymous researcher and the researcher known as 1sd3d (Viettel Cyber Security) respectively. Both of the vulnerabilities share a similar root cause and are located very closely to one another. However, the two researchers identified the same vulnerabilities in two different groups of routers, and each researcher exploited the vulnerabilities differently. Because of this, it is interesting to compare and contrast how these researchers approached the same problem and to speculate how they reached the final goal of a viable exploit through different paths.
Thanks to the requirements of the GNU General Public License (GPL), NETGEAR has published the source code of their firmware. These two vulnerabilities can be understood in the most straightforward fashion by analyzing the GPL release of the firmware provided by NETGEAR. In this blog post, we’ll analyze GPL firmware version 22.214.171.124 of the NETGEAR R6120 router. If you also want to poke around, you can find the firmware from the vendor website.
Based on the firmware source code, we can tell the webserver is based on version 1.24 of the mini_httpd open-source project. The vulnerabilities reside in the code bolted on by NETGEAR and therefore do not affect the upstream open-source web server.
main() function is located in mini_http.c. This function is responsible for setting up the Berkeley-style sockets, SSL, and the listen-loop. To handle concurrent HTTP requests, the webserver forks itself when a TCP connection is received to handle each connection individually in a sub-process. Here is the edited
main() function of mini_http from GPL firmware source code from NETGEAR:
handle_request() function starting at line 1502 then takes over and handles all HTTP processing after the forking.
The function first initializes some variables and proceeds to read in the request line of an HTTP request from the socket at line 1608 using the helper function
handle_request() function then proceeds to use
strpbrk() to separate the HTTP request method from the request line. The rest of the request line is stored in the variable named
path at line 1611 and the function continues to process the request path and the request.
Things become interesting starting from line 2106, where the multi-condition if-statement first checks if the
path matches one of the strings in array
no_check_passwd_paths. This is defined at line 409 with
path_exists() (defined in
sc_util.c). The if-statement also checks if the
path variable contains the substring “PNPX_GetShareFolderList”. If either of the conditions are met, the
need_auth variable is set to 0. The
need_auth variable does exactly what it advertises. When set to 0, the authentication will be skipped. The following snippet shows how the
no_check_passwd_paths array of strings is defined:
The astute reader should have spotted the vulnerability by now. From
handle_request(), the program never handled a case where there are request parameters that are part of the request line. If an attacker sends an HTTP request with a request parameter that contains any of the strings in the
no_check_passwd_paths array, the attacker can satisfy the if-condition defined at line 2106 and bypass authentication.
Proof of Concept (PoC) and Exploitation
The anonymous researcher had provided a simple PoC to demonstrate the vulnerability (ZDI-20-1176):
This PoC allows the attacker to view the post-authentication page
passwordrecovered.htm without authentication. This PoC can be tested by simply navigating to the above path in a browser.
Finally, the researcher provided an additional PoC that allows the attacker to view the router admin password to gain full control of the device in the report.
For ZDI-20-1451, the researcher (1sd3d) noticed that the program actually had not yet parsed out the HTTP version in the
path variable, and the naïve
strstr() will match with “PNPX_GetShareFolderList” if they simply append it to the end of the HTTP version in a request and satisfy the if-condition defined at line 2110 to bypass authentication.
1sd3d then chained this vulnerability with a post-authenticated command injection ZDI-20-1423 (ZDI-CAN-11653) to gain full control of the device.
White Box vs Black Box
The anonymous report approached the bug from a white box code-audit side, while 1sd3d’s report approached it from a black box reverse engineering using Ghidra and its decompiler. With this in mind, we can speculate on why they exploited the vulnerabilities differently and found the vulnerabilities in different sets of routers.
The vulnerable code for ZDI-20-1451 is wrapped within an
#ifdef PNPX preprocessor directive. When approached from the white box side, it is hard to tell if the
PNPX directive was defined at compile time. It is possible that the vulnerable code is not compiled into the final firmware. In fact, this code was indeed not compiled into the firmware for the NETGEAR R6120 wireless router.
Writing a script to look for the vulnerable source code pattern of ZDI-20-1176 is therefore a more reliable way to find exploitable firmware when working with GPL source code. Naturally, the anonymous researcher chose to take advantage of the
no_check_passwd_paths array that is not wrapped in any preprocessor directive to proceed with exploitation.
When approached from the black box RE side, what you see is what the CPU sees. However, goto statements, de Morgan’s Law, and lack of variable names can often obscure the logic of vulnerabilities in decompiled code. ZDI-20-1451 was the more apparent of the two vulnerabilities when inspected in the researcher’s decompiled code.
The rather unique “PNPX_GetShareFolderList” string makes searching for the same vulnerability across the firmware of different devices easier. Running the binary through
strings and searching for the string should give good enough accuracy. Writing a script to search for ZDI-20-1176 in a disassembler will definitely require some scripting wizardry.
Each method has its advantages and blind spots. In this specific case, they both arrived at the same destination but took different approaches when it came to exploitation. This demonstrates how no one method is superior. However, it is possible only one method may take you further in your next bug hunting journey. That said, being proficient in both can only be beneficial in the long term.
In a world of move fast, break things, and deadline driven product development, NETGEAR developers should have done a better job in code review before this flaw was shipped. The declaration of
no_need_check_password_page local variable in the latter part of the code in addition to the
need_auth variable does not instill confidence in the code. Luckily, it seems that NETGEAR is moving away from this tech debt-laden codebase in newer products and firmware.
It is often possible to deduce research methodology from vulnerability reports. One important caveat is that the researchers may have decided to omit their black box or white box work from their submission for clarity and render the entire comparison in the blog moot. If that were the case, at least you have learned something about two router bugs.
The Battle Between White Box and Black Box Bug Hunting in Wireless Routers