技术干货丨NETGEAR某设备分析

IoT 2年前 (2022) admin
553 0 0
技术干货丨NETGEAR某设备分析

签约作者:buggart


0x00 漏洞信息


编号:CVE-2021-34865

记录创建日期:20210617

漏洞类型:身份验证绕过安全漏洞


技术干货丨NETGEAR某设备分析


NETGEAR 强烈建议您尽快下载最新固件。固件修复目前适用于所有受影响的产品


如需下载固件,可前去官网或第三方网站

点击文末“阅读原文”获取网址


补丁比对


R6900v2的1.2.0.88 和1.2.0.76 下载下来,使用diaphora插件进行比对,关注path_exist()前面新增的代码块。


如需下载或安装BinDiff或diaphora

点击文末“阅读原文”获取网址


R6900v2

技术干货丨NETGEAR某设备分析


分析一下新增的strdecode()函数。


技术干货丨NETGEAR某设备分析


要注意sub_7D68()其实就是atoi()函数 ,将字符转换为数字。

‘A’‘a’转换为0xa,’6’转换为6。

所以strdecode就是循环遍历字符串,碰到%,将后面连续两个字符转换为16进制的数字。

这么看来,该漏洞很可能可以利用url的编码实现某些操作。


技术干货丨NETGEAR某设备分析


黑盒测试


尝试通过静态包去访问页面或者cgi。


GET /dtree.css/../../setup.cgi HTTP/1.1Host: 192.168.1.1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0Accept: */*Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding: gzip, deflateConnection: closeReferer: http://192.168.1.1/
技术干货丨NETGEAR某设备分析


尝试传入参数。


GET /dtree.css/../setup.cgi?todo=backup_config HTTP/1.1Host: 192.168.1.1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding: gzip, deflateConnection: closeReferer: http://192.168.1.1/BAK_backup.htm&todo=cfg_initUpgrade-Insecure-Requests: 1
技术干货丨NETGEAR某设备分析


失败了。


0x01 mini_httpd分析


老老实实开始逆向分析。


对于缺乏逆向功底的我,是一个漫长痛苦的过程,可能有帮助的小技巧:

  1. 网上寻找源码

  2. 寻找框架开源代码

  3. 该设备或同类型设备的分析文章,尤其是CVE分析文章

  4. 结合设备尝试并抓包分析

  5. 先分析早期固件版本


使用后端是mini_httpd,一种小型嵌入式后端服务器框架,常见的还有lighthttpd、httpd等,或者通过一些脚本例如lua来充当后端。


技术干货丨NETGEAR某设备分析


handle_request流程分析


抓包发现认证字段:

Authorization: Basic YWRtaW46cXdlMTIzLi8=

一看就是base64加密。

admin:qwe123./

在shift+F12搜索password、auth关键字,锁定auth_check()函数

  if ( strncmp(::authorization, "Basic ", 6u) )    {      ......      send_Unauthorized()    }    userpasswd[b64_decode((authorization + 6), userpasswd, 499)] = 0;    mypassword = strchr(userpasswd, ':');    if ( !mypassword )    {      .....      send_Unauthorized()    }    *mypassword = 0;    mypassword_1 = mypassword + 1;    http_username = nvram_get("http_username");      ......    if ( !strcmp(userpasswd, http_username) )    {      ......      ::password_hash(mypassword_1, password_hash, 128);      http_password = nvram_get("http_password");      ......      if ( strcmp(password_hash, http_password) )      {      ......


找到这个之后,重点分析调用它的handle_request()函数,整理分析认证流程。


setsockopt()设置TCP套接字选项。

  if ( !do_ssl )  {    r = 1;    setsockopt(conn_fd, 6, 3, &r, 4u);          // 设置TCP套接字选项  }  if ( do_ssl )  {    ssl = SSL_new(ssl_ctx);    SSL_set_fd(ssl, conn_fd);    v0 = SSL_accept(ssl);    v1 = 1;    if ( !v0 )LABEL_108:      exit(v1);                                 // 报错退出  }


开始处理request请求,初始化了两个字段,用来表示request的大小和索引。

  request_size = 0;  request_idx = 0;


先处理第一行的request,检测是否合法,如果有ptimeout.cgi包含在内的话,检测someone_in_use字段,判断是否有人登录,如果没有人且超时的话就停止进程。防止第二个用户登录超时,影响前面正常登录的用户。

    method_str = get_request_line();     if (method_str == (char *)0)        send_error(400, "Bad Request", "", "Can't parse request.");    path = strpbrk(method_str, " t1215");    if (path == (char *)0)        send_error(400, "Bad Request", "", "Can't parse request.");    *path++ = '';    if (strstr(path, "ptimeout.cgi") != NULL)    {        if (someone_in_use == 0)        {            STILLTO        }    path += strspn(path, " t1215");    protocol = strpbrk(path, " t1215");    if (protocol == (char *)0)        send_error(400, "Bad Request", "", "Can't parse request.");    *protocol++ = '';        send_error(401, "Unauthorized", "", "Authorization required.");    }


使用一个while大循环处理request的剩余部分,并进行初始化。

 while ( 1 )                                   // 解析请求头的剩余部分  {    line = get_request_line();    if ( !line || !*line )      break;    if ( !strncasecmp(line, "Authorization:", 0xEu) )// 如果检测到Authorization字段    {      authorization = &line[strspn(line + 14, &str_tab) + 14];    }    else if ( !strncasecmp(line, "Content-Length:", 0xFu) )    {      content_length = atol(&cp[v20]);    }    else if ( !strncasecmp(line, "Content-Type:", 0xDu) )    {      content_type = &line[strspn(line + 13, &str_tab) + 13];    }    else if ( !strncasecmp(head_soapaction, "SOAPAction:", 11u) )// SOAPAction字段    {    ......  }


config_state环境变量来控制设备的状态,表示是否完成了初始化。


    if(host && (*nvram_safe_get("config_state") == 'b' ||*nvram_safe_get("config_state") == 'c')&& is_captive_detecting(host, useragent))    {     // netgear请求,如果路由器刚刚完成恢复出厂设置,iPhone应该显示WiFi连接图标,无需重定向到浏览器。      if ( is_captive_detecting(host, useragent) )      {        for_captive=1;        protocol = strpbrk(path, " t1215");        send_error(200, "OK", "", "Success");      }    }  }


对usb_session的接入处理

if (*nvram_get("http_server_wan_enable") == '1' && *nvram_get("fw_remote") == '0')    {        /*When router is AP Mode, NV lan_ipaddr is not br0's IP, so we cannot access DUT by it's br0's IP, but we should think br0 IP is dut. This issue occur when USB's https enable */        if (*nvram_safe_get(WIFI_AP_MODE) == '1')        {            is_dut = is_dut || (strcmp(host, getIPAddress(LAN_LOG_IFNAME)) == 0);        }        else if (!is_usb_session && !is_dut  && *nvram_get("config_state") == 's')        {            SC_CFPRINTF("should not do the exit since hijack like traffic meter will not workingn");            //exit(1);        }    }


检查lan口和远程访问的正确性,不是443报错403。

  if ( !check_valid_request() )                 // 是否为正确请求,本地或者远程正确端口  {    v15 = 403;    if ( port != 443 )    {      v16 = "Forbidden";      v17 = "";      v18 = "URL is illegal.";      goto SEND_ERROR;    }  }


通过setupwizard.cgi完成初始化,检查是否是lan口访问,如果不是的话,就drop掉请求。也就是说初始化设置智能通过lan口来进行。

  if ( strstr(path, "setupwizard.cgi") )        // 如果path包含setupwizard.cgi    for_setupwizard = 1;  if ( for_setupwizard == 1 && !check_lan_guest() )  {    system("/bin/echo genie from wan, drop request now > /dev/console");// 来自wan,现在drop请求    v1 = 0;    goto LABEL_108;                             // exit()  }  if ( for_setupwizard == 1 )  {    system("/bin/echo it is for setupwizard! >> /tmp/sw.log");    strcpy(fakepath, "/setupwizard.cgi HTTP/1.1rn");    path = fakepath;                            // path设置为setupwizard.cgi    if ( have_cookie == 1 )    {      soap_token = 0;      dword_2EB84 = 0;      dword_2EB88 = 0;      dword_2EB8C = 0;      byte_2EB90 = 0;      p1 = strchr(cookie, '=');      if ( p1 )        strlcpy(&soap_token, p1 + 1, 17);    }  }


修复Win10 IE11在进行出厂设置之后的向导问题,Win 10将打开一个新的Edge/Spartan窗口/选项卡,原始页面也进行向导。这是由Windows 10使用HTTP获取“NCSI.txt” 并被路由器劫持导致的。现在不劫持它,只有响应404。

  if ( access("/tmp/brs_hijack.out", 0) )    goto LABEL_238;  v65 = getIPAddress("group1");  v66 = host;                                     if ( !strcmp(host, "www.msftncsi.com") && strstr(path, "ncsi.txt")    || !strcmp(v66, "www.msftconnecttest.com") && strstr(path, "connecttest.txt") )  {    v15 = 404;    ::protocol = "HTTP/1.0";    v16 = "Not Found";    v17 = "";SEND_NOT_FOUND:    v18 = "File not found.";    goto SEND_ERROR;  }


如果config_state是blank状态的话,或者need_not_login为1的话,将need_not_login置为0,start_in_blankstate字段置为1,除非超时或者登出,不会再重置这个值。也就是说恢复设置后重新启动,会在need_not_auth状态,但在超时后,需要登录。

  config_state = nvram_get("config_state");  if ( !config_state )    config_state = "";  if ( *config_state == 'b' )                   // blank state    goto LABEL_244;  need_not_login = nvram_get("need_not_login");  if ( !need_not_login )    need_not_login = "";  if ( *need_not_login == '1' )  // 恢复设置后重新启动,会在need_not_auth状态,但在超时后,需要登录   {LABEL_244:    nvram_set("need_not_login", "0");    nvram_set("start_in_blankstate", "1");      // 不重置这个值直到超时或者登出  }


通过四种方法都可以 置need_auth为0。


1.path_exist判断不需要认证,且路径不包含VLAN_update_setting.htm。

2.POST请求且路径包含htpwd_recovery.cgi

3.路径的前39个字符为“/setup.cgi?todo=PNPX_GetShareFolderList”,请求为GET且路径不包含’htm’

4.config_state为c,路径包含”sso”。


  if ( path_exist(path, no_auth_html, method_str_1) && !strstr(path, "VLAN_update_setting.htm") )    goto LABEL_256;  v97 = path;  if ( !strncmp(path, "/htpwd_recovery.cgi?", 'x14') )  {    v98 = ::method_str(3);                      // POST请求    if ( !strcasecmp(method, v98) )      goto LABEL_256;  }  if ( !strncmp(v97, "/setup.cgi?todo=PNPX_GetShareFolderList", 39u) )  {    v99 = ::method_str(1);                      // GET请求    if ( !strcasecmp(method, v99) && !strstr(v97, "htm") )      goto LABEL_256;  }  v100 = nvram_get("config_state");  if ( !v100 )    v100 = "";  if ( *v100 == 'c' && strstr(path, "sso") )  {LABEL_256:    someone_in_use = 0;    need_auth = 0;    if ( strstr(path, "currentsetting.htm") )      for_setupwizard = 1;  }


no_auth_html保存了一些不需要验证的html页面。


技术干货丨NETGEAR某设备分析


只要在path里找到了不需要认证的页面,就将no_need_check_password_page置为1。


  v101 = no_auth_html;  no_need_check_password_page = 0;              // 不需要password_page标志  while ( *v101 )  {    if ( strstr(path, *v101) )      no_need_check_password_page = 1;    ++v101;  }


如果路径不包含.cgi直接请求.htm的话,自动在中间插入”/setup.cgi?next_file=”


  v148 = path;  if ( !strstr(path, ".cgi") && strstr(v148, ".htm") && !strstr(v148, "shares") )// 没有.cgi请求.htm  {    v149 = strlen(v148);    if ( v149 >= 482 )    {      v15 = 404;      v16 = "Not Found";      v17 = "";LABEL_439:      v18 = "No such file.";      goto SEND_ERROR;    }    strlcpy(fakepath, v148, v149 - 8);    v150 = strrchr(fakepath, '/');    strlcpy(firstdir, fakepath, v150 - fakepath);    v151 = path;    if ( *path == '/' )      path = v151 + strlen(firstdir) + 1;    snprintf(fakepath, 0x200u, "%s/setup.cgi?next_file=%s", firstdir, path);// 自动插入/setup.cgi?next_file=    path = fakepath;  }


补丁修复,CVE-2019-17137,后面加%00currentsetting.htm可以直接绕过验证。


  if ( strstr(path, "%00") || (strdecode(v161, v161), tem_path = path, *path != '/') )  {    v15 = 400;    v16 = "Bad Request";    v17 = "";    v18 = "Bad filename.";    goto SEND_ERROR;  }


还通过strdecode()对url编码进行解码。


对// ./ /../等进行处理


技术干货丨NETGEAR某设备分析


但需要注意的,这里是对临时变量进行处理,全局的path没有改变,而真正用的时候用的又是全局的path,过滤了个寂寞。


有一个疑似配置文件的操作。


技术干货丨NETGEAR某设备分析


最后执行do_file函数并释放ssl套接字。


技术干货丨NETGEAR某设备分析


path_exist流程分析


先进行url转码

v6 = LODWORD(method);if ( strcasestr(path, "%2f", method) || strcasestr(path, "%2e", v5) || strstr(path, "%20") || strstr(path, "%26") )// url编码转为16进制strdecode(path, path);


如果method不为GET,就返回0。


  v7 = 0;  if ( method_2 != 'G' )                        // 不为GET,return 0    return v7;


如果是GET请求包,就进一步进行判断。


如果path的前11个字符包含/setup.cgi?的话,先判断是否有next_file。


如果有next_file参数,且path包含&符号,将next_file后面的&符号的其他参数置空,只取next_file。


然后判断next_file是否需要认证,如果不需要认证直接返回1。


如果path没有&符号,判断next_file是否需要认证,如果需要认证retur 0。


技术干货丨NETGEAR某设备分析


auth_check流程分析


在do_file函数的开始对传入的请求路径,如果need_auth字段为1的话调用auth_check进行检测。


技术干货丨NETGEAR某设备分析


在auth_check()的开头,先对for_setupwizard进行检测。


技术干货丨NETGEAR某设备分析


如果for_setupwizard字段为1,就可以跳过检查。


所以不论是控制了for_setupwizard还是need_auth都可以绕过验证。



由于文章字数受限

可点击下方【阅读原文】阅读全篇。



技术干货丨NETGEAR某设备分析



技术干货丨NETGEAR某设备分析



原文始发于微信公众号(IOTsec Zone):技术干货丨NETGEAR某设备分析

版权声明:admin 发表于 2022年9月8日 下午5:13。
转载请注明:技术干货丨NETGEAR某设备分析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...