无线路由器中白盒和黑盒漏洞搜寻之间的战斗

  • 无线路由器中白盒和黑盒漏洞搜寻之间的战斗已关闭评论
  • 15 views
  • A+

译文声明

本文是翻译文章,文章原作者Vincent Lee,文章来源:https://www.thezdi.com
原文地址:https://www.thezdi.com/blog/2021/3/11/the-battle-between-white-box-and-black-box-bug-hunting-in-wireless-routers

译文仅供参考,具体内容表达以及含义以原文为准

引言

去年,我们披露了两个影响多种NETGERA产品的身份验证绕过漏洞ZDI-20-1176(ZDI-CAN-10754)和ZDI-20-1451(ZDI-CAN-11355)。这两个漏洞分别由一位匿名研究人员和名为1sd3d(Viettel网络安全)的研究人员发现,同时,这两个漏洞都位于mini_httpd Web服务器中。虽然这两个漏洞有着相似的产生原因,并且产生漏洞的位置非常接近,但是,两位研究人员是在两组不同的路由器中发现的该漏洞,并且两位研究人员对漏洞的利用方式各有不同。对比这些研究人员如何处理相同的问题,并推测他们如何通过不同的途径实现可行的利用的最终目标是一件很有趣的事情。

漏洞概述

由于GNU通用公共许可证(GPL)的要求,NETGEAR已发布了其固件的源代码。通过分析NETGEAR提供的固件的GPL版本,可以用最直接的方式理解这两个漏洞。在此博客文章中,我们将分析NETGEAR R6120路由器的GPL固件版本1.0.0.72。如果您也想尝试,可以从供应商的网站上找到固件

根据固件源代码,我们可以知道Web服务器是基于mini_httpd开源项目的1.24版本的。该漏洞位于NETGEAR附加的代码中,因此不会影响上游开源的Web服务器。

main()函数位于mini_http.c中。该函数负责设置Berkeley风格的套接字,SSL和循环监听。为了处理并发的HTTP请求,当接收到TCP连接时,Web服务器会分叉自身,以在子进程中单独处理每个连接。这是main()来自NETGEAR的GPL固件源代码中mini_http的编辑功能:

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

// ...

1095 /* Main loop. */

1096 for (;;)

1097 {

// ...

1149

1150 /* Accept the new connection. */

1151 sz = sizeof(usa);

1152 if (listen4_fd != -1 && FD_ISSET(listen4_fd, &lfdset))

1153 conn_fd = accept(listen4_fd, &usa.sa, &sz); // [ZDI] Accepting an IPv4 connection

1154 else if (listen6_fd != -1 && FD_ISSET(listen6_fd, &lfdset))

1155 conn_fd = accept(listen6_fd, &usa.sa, &sz); // [ZDI] Accepting an IPv6 connection

1156 else

// ...

1178

1179 /* Fork a sub-process to handle the connection. */

1180

// ...

1217 r = fork();

1218 if (r < 0)

1219 {

1220 #ifdef SYSLOG

1221 syslog(LOG_CRIT, "fork - %m");

1222 perror("fork");

1223 #endif

1224 exit(1);

1225 } else if (r == 0)

1226 {

1227 /* Child process. */

1228 client_addr = usa;

1229 if (listen4_fd != -1)

1230 (void)close(listen4_fd);

1231 if (listen6_fd != -1)

1232 (void)close(listen6_fd);

1233 SC_CFPRINTF("\=====go to Handle_request!\n");

1234 //log_debug("=====go to Handle_request!\n");

1235 handle_request(); // [ZDI] forked child process proceeds to handle the connection in handle_request()

1236 #ifdef IP_ASSIGN_CHK

1237 /* after get the last file of warning_pg.htm, we can stop dnshj */

1238 if (access("/tmp/stop_conflict_warning", F_OK) == 0)

1239 {

1240 unlink("/tmp/lan_ip_auto_changed");

1241 unlink("/tmp/stop_conflict_warning");

1242 system("/usr/sbin/rc dnshj stop");

1243 }

1244 #endif

1245 exit(0);

1246 }
接下来,`handle_request()`从第1502行开始的函数将接管并处理分支之后的所有HTTP处理。
1499 /* This runs in a child process, and exits when done, so cleanup is

1500 ** not needed.

1501 */

1502 static void handle_request(void)

1503 {

1504 char *method_str;

1505 char *line;

1506 char *cp;

1507 int r, file_len, i;

// ...

1530

1531 /* Initialize the request variables. */

1532 remoteuser = (char *)0;

1533 method = METHOD_UNKNOWN;

1534 path = (char *)0;

1535 file = (char *)0;

1536 pathinfo = (char *)0;

1537 query = "";

1538 protocol = (char *)0;

1539 status = 0;

1540 bytes = -1;

1541 req_hostname = (char *)0;

1542

1543 authorization = (char *)0;

1544 content_type = (char *)0;

1545 content_length = -1;

1546 cookie = (char *)0;

1547 host = (char *)0;

1548 if_modified_since = (time_t) - 1;

1549 referrer = "";

1550 useragent = "";

1551 #ifdef SC_BUILD

1552 accept_language = "";

1553 need_auth = 1; /* all of files need auth check by default */

1554 #endif

// ...

1607 /* Parse the first line of the request. */

1608 method_str = get_request_line();

1609 if (method_str == (char *)0)

1610 send_error(400, "Bad Request", "", "Can't parse request.");

1611 path = strpbrk(method_str, " \t\012\015");

// ...

1720 // qqq

1721 /* Follow Netgear request, if router just done factory reset, iphone should

1722 * show WiFi connection icon without redirect to browsers . Bollen_Chen*/

1723 if(host && (*nvram_safe_get("config_state") == 'b' || *nvram_safe_get("config_state") == 'c')

1724 && is_captive_detecting(host, useragent))

1725 {

1726 for_captive=1;

1727 protocol = strpbrk(path, " \t\012\015");

1728 send_error(200, "OK", "", "Success");

1729 }

1730

// ...

2093 /*No login required */

2094 if (*nvram_safe_get("config_state") == 'b' /*blank state */

2095 // || strstr(path,"BRS_top.html") /*Genie Wizard auto refresh timer*/

2096 // || strstr(path,"BRS_netgear_success.html") /*This page will link to NTGR page, should not require username/password.*/

2097 /*reboot after restore, stay in NEEDNOTAUTH state, but after timeout, require login */

2098 || (*nvram_safe_get("need_not_login") == '1'))

2099 {

2100 SC_CFPRINTF("Genie Wizard, set start_in_blankstate = 1\n");

2101 nvram_set("need_not_login", "0");

2102 nvram_set("start_in_blankstate", "1"); /*do not reset this value until timeout or log out */

2103 }

2104

2105 SC_CFPRINTF("path is <%s>, need_auth = %d\n", path, need_auth);

2106 if (path_exist(path, no_check_passwd_paths, method_str) || // [ZDI] ZDI-CAN-10754

2107 /* for "htpwd_recovery.cgi", POST should not auth, GET need auth */

2108 (strstr(path, "htpwd_recovery.cgi") && strcasecmp(method_str, get_method_str(METHOD_POST)) == 0)

2109 #ifdef PNPX

2110 || (strstr(path, "PNPX_GetShareFolderList")) // [ZDI] ZDI-CAN-11355

2111 #endif

2112 #ifdef SSO

2113 || ( *nvram_safe_get("config_state") == 'c' && strstr(path, "sso"))

2114 #endif

2115 )

2116 {

2117 need_auth = 0;

2118 /* for hi-jack page, should allow 2 user access at same time. */

2119 someone_in_use = 0;

2120 if (strstr(path, "currentsetting.htm") != NULL)

2121 {

2122 for_setupwizard = 1;

2123 }

2124 }

// ...

4443 static char *get_request_line(void)

4444 {

4445 int i;

4446 char c;

4447

4448 for (i = request_idx; request_idx < request_len; ++request_idx)

4449 {

4450 c = request[request_idx];

4451 if (c == '\012' || c == '\015')

4452 {

4453 request[request_idx] = '\0';

4454 ++request_idx;

4455 if (c == '\015' && request_idx < request_len && request[request_idx] == '\012')

4456 {

4457 request[request_idx] = '\0';

4458 ++request_idx;

4459 }

4460 return &(request[i]);

4461 }

4462 }

4463 return (char *)0;

4464 }
``
该函数首先初始化一些变量,然后使用辅助函数get_request_line()从第1608行的套接字中读取HTTP请求的[请求行](https://tools.ietf.org/html/rfc7230#section-3.1.1)。然后
handle_request()函数继续使用strpbrk()`来将HTTP请求方法与请求行分开。请求行的其余部分存储在第1611行的名为path的变量中,该函数继续处理请求路径和请求。

从第2106行开始,事情变得很有趣,多条件if语句首先检查path路径是否与数组no_check_passwd_paths中的字符串之一匹配。这是在第409行使用path_exists()定义的(在sc_util.c中定义)。if语句还会检查path变量是否包含子字符串“ PNPX_GetShareFolderList”。如果满足两个条件中的任何一个,那么need_auth变量将设置为0,该need_auth变量将完全执行其通告。设置为0时,将跳过身份验证。以下代码段显示了如何定义字符串的no_check_passwd_paths数组:
```
406 /* Ron */

407

408 /* Request variables. */

409 static char *no_check_passwd_paths[] = { "currentsetting.htm", "update_setting.htm",

410 "debuginfo.htm", "important_update.htm", "MNU_top.htm",

411 // "warning_pg.htm","debug.htm",

412 "warning_pg.htm", "POT.htm",

413 "multi_login.html", "401_recovery.htm", "401_access_denied.htm",

414 #ifdef SSO

415 "sso.html","sso_loading.html","BRS_sso_redirect.html","BRS_sso_hijack.html",

416 #endif

417 "BRS_netgear_success.html", "BRS_top.html", "BRS_miiicasa_success.html",

418 "tc_exist_unit_hijack.htm","BRS_data_detail.htm","BRS_full_tcn.htm","BRS_hijack_success.htm",

419 NULL

420 };
``
精明的读者现在应该已经发现了该漏洞。从
main()handle_request(),该程序从未处理过存在属于请求行的请求参数的情况。如果攻击者发送的HTTP请求的请求参数包含no_check_passwd_paths`数组中的任何字符串,则攻击者可以满足在第2106行中定义的if条件并绕过身份验证。

PoC和开发

匿名研究人员提供了一个简单的PoC来演示此漏洞(ZDI-20-1176):

http://<router ip>/passwordrecovered.htm&next\_file=update\_setting.htm

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

此PoC允许攻击者无需身份验证即可查看身份验证后的页面passwordrecovered.htm。可以通过在浏览器中导航到以上路径来测试此PoC。

最后,研究人员提供了一个附加的PoC,允许攻击者查看路由器的管理员密码,以完全控制报告中的设备。

对于ZDI-20-1451,研究人员(1sd3d)注意到该程序实际上尚未解析出该path变量中的HTTP版本,如果只是将它们简单地附加到请求中HTTP版本的末尾,strstr()则将与“ PNPX_GetShareFolderList”匹配并满足在第2110行中定义的if条件以绕过身份验证。

GET /passwordrecovered.htm HTTP/1.1PNPX\_GetShareFolderList\\r\\n

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

然后1sd3d使用了绕过身份验证的命令注入ZDI-20-1423(ZDI-CAN-11653)链接此漏洞,以完全控制设备。

白盒与黑盒

匿名研究人员的报告从白盒代码审核方处理了该漏洞,而1sd3d的报告从黑盒使用Ghidra及其反编译器进行了逆向工程处理。考虑到这一点,我们可以推测到为什么他们使用不同的方法利用了这些漏洞,并在不同的路由器集中发现了这些漏洞。

ZDI-20-1451的易受攻击的代码包含在#ifdef PNPX预处理程序指令中。当从白盒侧访问时,很难判断PNPX指令是否在编译时定义。易受攻击的代码可能没有编译到最终固件中。实际上,此代码的确没有编译到NETGEAR R6120无线路由器的固件中。

因此,编写脚本来查找ZDI-20-1176的易受攻击的源代码模式是使用GPL源代码时查找可利用固件的更可靠方法。自然,匿名研究人员选择利用未包装在任何预处理程序指令中的no_check_passwd_paths数组来继续开发。

从黑匣子RE端接近时,您所看到的就是CPU所看到的。但是,goto语句,摩根定律(de Morgan's Law)以及缺少变量名通常会掩盖反编译代码中的漏洞逻辑。使用研究人员的反编译代码检查时,ZDI-20-1451在这两个漏洞中更为明显。

Fig1.png

提交者报告中显示了Ghidra中NETGEAR R7450固件的反编译代码视图。

相当独特的“PNPX_GetShareFolderList”字符串使跨不同设备的固件搜索相同漏洞变得更加容易。通过strings运行二进制文件并搜索字符串应具有足够好的准确性。编写脚本以在反汇编程序中搜索ZDI-20-1176肯定需要一些脚本向导。

结论

每种方法都有其优势和盲点。在这种特定情况下,他们俩都到达了相同的目的,但在利用时却采取了不同的方法。这证明了没有一种方法是完美的。但是,可能只有一种方法可以使您在下一个Bug搜索旅程中走得更远。就是说,精通这两个方面从长远来看是有益的。

在瞬息万变的突发事件和按时交货的产品开发世界中,NETGEAR开发人员应该在此缺陷发布之前在代码审查方面做得更好。除了need_auth变量之外,在代码的后半部分声明no_need_check_password_page局部变量也不会使人们对代码充满信心。幸运的是,在新产品和固件中,NETGEAR似乎正在摆脱这种技术负担沉重的代码库。

脚注

我们通常可以从漏洞报告中推断出研究方法。但有一个重要的警告是,为了清晰起见,研究人员可能已决定从提交的内容中省略其黑盒或白盒的工作,并在博客中进行整个比较。如果真是这样,那么至少您已经学到了有关两个路由器错误的知识。

您可以在Twitter @TrendyTofu上找到我,并关注该团队以获取最新的利用技术和安全补丁。