Download as pdf or txt
Download as pdf or txt
You are on page 1of 6

THE BATTLE BETWEEN WHITE BOX AND

BLACK BOX BUG HUNTING IN WIRELESS


ROUTERS
March 11, 2021 | Vincent Lee

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.

The Vulnerabilities

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 1.0.0.72 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.

The 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:

1 558 int main(int argc, char **argv)


2 // ...
3 1095 /* Main loop. */
4 1096 for (;;)
5 1097 {
6 // ...
7 1149
8 1150 /* Accept the new connection. */
9 1151 sz = sizeof(usa);
10 1152 if (listen4_fd != -1 && FD_ISSET(listen4_fd, &lfdset))
11 1153 conn_fd = accept(listen4_fd, &usa.sa, &sz); // [ZDI] Accepting an IPv4 co
12 1154 else if (listen6_fd != -1 && FD_ISSET(listen6_fd, &lfdset))
13 1155 conn_fd = accept(listen6_fd, &usa.sa, &sz); // [ZDI] Accepting an IPv6 co
14 1156 else
15 // ...
16 1178
16 1178
17 1179 /* Fork a sub-process to handle the connection. */
18 1180
19 // ...
20 1217 r = fork();
21 1218 if (r < 0)
22 1219 {
23 1220 #ifdef SYSLOG
24 1221 syslog(LOG_CRIT, "fork - %m");
25 1222 perror("fork");
26 1223 #endif
27 1224 exit(1);
28 1225 } else if (r == 0)
29 1226 {
30 1227 /* Child process. */
31 1228 client_addr = usa;
32 1229 if (listen4_fd != -1)
33 1230 (void)close(listen4_fd);
34 1231 if (listen6_fd != -1)
35 1232 (void)close(listen6_fd);
36 1233 SC_CFPRINTF("=====go to Handle_request!\n");
37 1234 //log_debug("=====go to Handle_request!\n");
38 1235 handle_request(); // [ZDI] forked child process proceeds to han
39 1236 #ifdef IP_ASSIGN_CHK
40 1237 /* after get the last file of warning_pg.htm, we can stop dnshj */
41 1238 if (access("/tmp/stop_conflict_warning", F_OK) == 0)
42 1239 {
43 1240 unlink("/tmp/lan_ip_auto_changed");
44 1241 unlink("/tmp/stop_conflict_warning");
45 1242 system("/usr/sbin/rc dnshj stop");
46 1243 }
47 1244 #endif
48 1245 exit(0);
49 1246 }

NETGEAR-R6120-1.0.0.72-snippet-1.cpp
hosted with ❤ by GitHub view raw

The handle_request() function starting at line 1502 then takes over and handles all HTTP processing
after the forking.

1 1499 /* This runs in a child process, and exits when done, so cleanup is
2 1500 ** not needed.
3 1501 */
4 1502 static void handle_request(void)
5 1503 {
6 1504 char *method_str;
7 1505 char *line;
8 1506 char *cp;
9 1507 int r, file_len, i;
10 // ...
11 1530
12 1531 /* Initialize the request variables. */
13 1532 remoteuser = (char *)0;
14 1533 method = METHOD_UNKNOWN;
15 1534 path = (char *)0;
16 1535 file = (char *)0;
17 1536 pathinfo = (char *)0;
18 1537 query = "";
19 1538 protocol = (char *)0;
20 1539 status = 0;
21 1540 bytes = -1;
22 1541 req_hostname = (char *)0;
23 1542
24 1543 authorization = (char *)0;
25 1544 content_type = (char *)0;
26 1545 content_length = -1;
27 1546 cookie = (char *)0;
27 1546 cookie (char )0;
28 1547 host = (char *)0;
29 1548 if_modified_since = (time_t) - 1;
30 1549 referrer = "";
31 1550 useragent = "";
32 1551 #ifdef SC_BUILD
33 1552 accept_language = "";
34 1553 need_auth = 1; /* all of files need auth check by default */
35 1554 #endif
36 // ...
37 1607 /* Parse the first line of the request. */
38 1608 method_str = get_request_line();
39 1609 if (method_str == (char *)0)
40 1610 send_error(400, "Bad Request", "", "Can't parse request.");
41 1611 path = strpbrk(method_str, " \t\012\015");
42 // ...
43 1720 // qqq
44 1721 /* Follow Netgear request, if router just done factory reset, iphone should
45 1722 * show WiFi connection icon without redirect to browsers . Bollen_Chen*/
46 1723 if(host && (*nvram_safe_get("config_state") == 'b' || *nvram_safe_get("config_state") == 'c
47 1724 && is_captive_detecting(host, useragent))
48 1725 {
49 1726 for_captive=1;
50 1727 protocol = strpbrk(path, " \t\012\015");
51 1728 send_error(200, "OK", "", "Success");
52 1729 }
53 1730
54 // ...
55 2093 /*No login required */
56 2094 if (*nvram_safe_get("config_state") == 'b' /*blank state */
57 2095 // || strstr(path,"BRS_top.html") /*Genie Wizard auto refresh timer*/
58 2096 // || strstr(path,"BRS_netgear_success.html") /*This page will link to NTGR page, should not
59 2097 /*reboot after restore, stay in NEEDNOTAUTH state, but after timeout, require login */
60 2098 || (*nvram_safe_get("need_not_login") == '1'))
61 2099 {
62 2100 SC_CFPRINTF("Genie Wizard, set start_in_blankstate = 1\n");
63 2101 nvram_set("need_not_login", "0");
64 2102 nvram_set("start_in_blankstate", "1"); /*do not reset this value until timeout or
65 2103 }
66 2104
67 2105 SC_CFPRINTF("path is <%s>, need_auth = %d\n", path, need_auth);
68 2106 if (path_exist(path, no_check_passwd_paths, method_str) || // [ZDI] ZDI-CAN-10754
69 2107 /* for "htpwd_recovery.cgi", POST should not auth, GET need auth */
70 2108 (strstr(path, "htpwd_recovery.cgi") && strcasecmp(method_str, get_method_str(METHOD_POS
71 2109 #ifdef PNPX
72 2110 || (strstr(path, "PNPX_GetShareFolderList")) // [ZDI] ZDI-CAN-11355
73 2111 #endif
74 2112 #ifdef SSO
75 2113 || ( *nvram_safe_get("config_state") == 'c' && strstr(path, "sso"))
76 2114 #endif
77 2115 )
78 2116 {
79 2117 need_auth = 0;
80 2118 /* for hi-jack page, should allow 2 user access at same time. */
81 2119 someone_in_use = 0;
82 2120 if (strstr(path, "currentsetting.htm") != NULL)
83 2121 {
84 2122 for_setupwizard = 1;
85 2123 }
86 2124 }
87 // ...
88 4443 static char *get_request_line(void)
89 4444 {
90 4445 int i;
91 4446 char c;
92 4447
93 4448 for (i = request_idx; request_idx < request_len; ++request_idx)
94 4449 {
95 4450 c = request[request_idx];
96 if ( '\0 2' || '\0 ')
96 4451 if (c == '\012' || c == '\015')
97 4452 {
98 4453 request[request_idx] = '\0';
99 4454 ++request_idx;
100 4455 if (c == '\015' && request_idx < request_len && request[request_idx] == '\0
101 4456 {
102 4457 request[request_idx] = '\0';
103 4458 ++request_idx;
104 4459 }
105 4460 return &(request[i]);
106 4461 }
107 4462 }
108 4463 return (char *)0;
109 4464 }

NETGEAR-R6120-1.0.0.72-snippet-2.cpp
hosted with ❤ by GitHub view raw

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 get_request_line(). The
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:

1 406 /* Ron */
2 407
3 408 /* Request variables. */
4 409 static char *no_check_passwd_paths[] = { "currentsetting.htm", "update_setting.htm",
5 410 "debuginfo.htm", "important_update.htm", "MNU_top.htm",
6 411 // "warning_pg.htm","debug.htm",
7 412 "warning_pg.htm", "POT.htm",
8 413 "multi_login.html", "401_recovery.htm", "401_access_denied.htm",
9 414 #ifdef SSO
10 415 "sso.html","sso_loading.html","BRS_sso_redirect.html","BRS_sso_hijack.html",
11 416 #endif
12 417 "BRS_netgear_success.html", "BRS_top.html", "BRS_miiicasa_success.html",
13 418 "tc_exist_unit_hijack.htm","BRS_data_detail.htm","BRS_full_tcn.htm","BRS_hijack_success.htm"
14 419 NULL
15 420 };

NETGEAR-R6120-1.0.0.72-snippet-3.cpp
hosted with ❤ by GitHub view raw

The astute reader should have spotted the vulnerability by now. From main() to 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):
1 http://<router ip>/passwordrecovered.htm&next_file=update_setting.htm

NETGEAR-R6120-1.0.0.72-snippet-4.console
hosted with ❤ by GitHub view raw

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.

1 GET /passwordrecovered.htm HTTP/1.1PNPX_GetShareFolderList\r\n

NETGEAR-R6120-1.0.0.72-snippet-5.console
hosted with ❤ by GitHub view raw

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.
Decompiled code view of the NETGEAR R7450 firmware in Ghidra from submitter’s report.

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.

Conclusion

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.

Footnote

 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.

You can find me on Twitter @TrendyTofu, and follow the team for the latest in exploit techniques and
security patches.

NETGEAR
Reverse Engineering
Research

You might also like