OneShell

I fight for a brighter tomorrow

0%

CVE-2021-35973 netgear wac104登录认证绕过再分析

漏洞描述

CVE-2021-35973是发生在netgear wac104设备、固件版本1.0.4.15之前的身份认证绕过漏洞,漏洞产生的原因是在鉴权过程中,使用了strstr来判断:如果请求uri中包含currentsetting.htm,设置无需认证标志。因此攻击者可以在需要鉴权的uri中包含currentsetting.htm标志,从而达到认证绕过的目的。

漏洞描述:CVE - CVE-2021-35973

漏洞分析

netgear系列的固件可以直接从官网下载,而且可以直接使用binwalk解压,固件下载链接如下:
WAC104 | Access Point | NETGEAR Support

webserver架构分析

在文件系统中直接搜索字符串httpd可以定位到启动脚本,因此猜测httpd是通过程序/usr/sbin/rc启动的,而且一个是开放在WAN口,一个是开放在LAN口。

1
2
/usr/sbin/rc httpd start
/usr/sbin/rc remote_httpd start

对程序/usr/sbin/rc进行逆向,搜索字符串httpd,寻找相关字符串的交叉引用,可以看到应该是采用了mini_httpd作为webserver。

1
2
3
4
5
6
7
.data:00464610                 .word aUsrBinKillallR_0+0x18  # "httpd"
.data:00464614 .word mini_httpd_start
.data:00464618 .word mini_httpd_stop
.data:0046461C .word httpd_init
.data:00464620 .word aRemoteHttpd # "remote_httpd"
.data:00464624 .word mini_httpd_remote_start
.data:00464628 .word mini_httpd_remote_stop

该漏洞是WAN口、LAN口都存在,因此针对影响较大的WAN口的remote_httpd进行分析。根据如下的逆向逻辑,remote_httpd是链接到程序/sbin/mini_httpd,启动参数也很清晰,下一步对/sbin/mini_httpd进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ( access("/var/remote_httpd", 0) < 0 )
system("/bin/ln -sf /sbin/mini_httpd /var/remote_httpd");
nv_set_int("http", "remotemg_port", 0, v3);
v4 = (char *)nvram_get("product_name");
if ( !v4 )
v4 = "";
v7 = v4;
v5 = atoi(v1);
SYSTEM(
"/var/remote_httpd -p %d -P %d -d /www -S -E /etc/mini_httpd.pem -r \"NETGEAR %s\" -c '**.cgi' -t %d&",
v3,
v3,
v7,
60 * v5);

mini_httpd请求包处理逻辑

根据以往对mini_httpd代码的了解,其处理请求包的逻辑主要是由handler_request函数负责,可以通过搜索index页面字符串定位到该函数。如下是mini_httpd源码中的handle_request函数和其中设置的index页面字符串列表。

定位到handle_request函数:搜索如下列表中的字符串

1
2
3
   const char* index_names[] = {
"index.html", "index.htm", "index.xhtml", "index.xht", "Default.htm",
"index.cgi" };

mini_httpd收到一个数据请求包后会fork创建一个子进程来进行处理,这种方式如果在高并发场景会在进程创建、销毁过程中消耗大量的资源,但是在并发量低的嵌入式设备已经够用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Fork a sub-process to handle the connection. */
r = fork();
if ( r < 0 )
{
syslog( LOG_CRIT, "fork - %m" );
perror( "fork" );
exit( 1 );
}
if ( r == 0 )
{
/* Child process. */
client_addr = usa;
if ( listen4_fd != -1 )
(void) close( listen4_fd );
if ( listen6_fd != -1 )
(void) close( listen6_fd );
handle_request(); // 数据包处理逻辑
}
(void) close( conn_fd );
}

子进程中主要是通过handle_request函数来对数据进行处理的,主要是先解析请求行、再解析请求头。

  • 读取请求的第一行,获取到请求行,然后从行中解析到请求方法protocol、请求路径path和查询参数query
  • 随后解析header,主要实现是通过while循环继续逐行解析header中的字段,包括Authorization、Content-Length、Content-Type、Cookie、User-Agent等等常见的字段

如下是handle_request函数中读取请求行,获取到请求method_str、path、query、protocol。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   /* Parse the first line of the request. */
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';
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';
protocol += strspn( protocol, " \t\012\015" );
query = strchr( path, '?' );
if ( query == (char*) 0 )
query = "";
else
*query++ = '\0';

然后是while循环处理header,获取字段。在源码中包括:Authorization、Content-Length、Content-Type、Cookie、Host、If-Modified-Since、Referer、Referrer、User-Agent这些字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   /* Parse the rest of the request headers. */
while ( ( line = get_request_line() ) != (char*) 0 )
{
if ( line[0] == '\0' )
break;
else if ( strncasecmp( line, "Authorization:", 14 ) == 0 )
{
cp = &line[14];
cp += strspn( cp, " \t" );
authorization = cp;
}
else if ( strncasecmp( line, "Content-Length:", 15 ) == 0 )
{
cp = &line[15];
cp += strspn( cp, " \t" );
content_length = atol( cp );
}

获取到了如上的重要字段后,就开始对数据包的合法性进行判断,例如:

  • 请求方法method是否合理:GET、HEAD、POST、PUT、DELETE、TRACE
  • 请求路径path必须以反斜杠/开头、对path进行目录穿越相关字符进行处理、检查文件是否存在
  • 然后根据path是文件夹或文件,分别调用do_dir和do_file进行处理,二者最终都会进行权限检查函数auth_check

在权限检查函数auth_check中,输入为请求的path转换的实际路径file所在的文件夹dirname,如果权限检查通过,则继续直接随后的数据包处理流程;如果权限检查是否,则通过send_authenticate函数返回401,然后结束当前连接的生命周期。

权限检查的流程则是:

  1. 如果dirname中没有.htpasswd文件,那么直接认证通过。就相当于是在需要授权访问的文件夹中添加该文件,不需要授权访问的文件夹中没有该文件
  2. 源码中采用的校验方式是BASIC认证,请求包中带上username和base64编码的password,然后和.htpasswd文件中保存的账号信息进行对比,如果比较通过则直接返回。

因此,平常漏洞挖掘中比较关心的登录认证流程就非常清晰:main -> handle_request -> do_file/do_dir -> auth_check。一般情况下,厂商会根据自己的业务逻辑修改相关的函数,但是根据源码我们还是能通过一些字符串特征来定位到关键函数,例如:

  • 通过搜索index相关的页面字符串,可以定位到handle_request
    1
    2
    3
    4
    5
    6
    7
    .data:0041E030 index_names:    .word aSetupCgi          # DATA XREF: handle_request+38↑o
    .data:0041E030 # "setup.cgi"
    .data:0041E034 .word aIndexHtml # "index.html"
    .data:0041E038 .word aIndexHtm # "index.htm"
    .data:0041E03C .word aIndexXhtml # "index.xhtml"
    .data:0041E040 .word aIndexXht # "index.xht"
    .data:0041E044 .word aDefaultHtm # "Default.htm"
  • 通过搜索字符串.htpasswd可以直接定位到do_file、auth_check函数。do_file函数中会检查请求文件是否为.htpasswd,auth_check函数则是需要读取账号信息、调用字符串比较函数等

可以看出,mini_httpd的登录认证处理流程还是比较简单的。

漏洞触发

通过之前的源代码梳理,也明白了mini_httpd的登录认证流程,那么可以通过搜索字符串的技巧直接定位到auth_check函数。auth_check函数开头有一段导致后续认证绕过的逻辑,其中有一个g_bypass_flag=1时可以直接通过认证。

1
2
3
4
5
6
7
8
9
10
11
12
if ( g_bypass_flag == 1 )
{
if ( !sub_4062C0() )
{
system("/bin/echo genie from wan, drop request > /dev/console");
exit(0);
}
result = system("/bin/echo genie from lan, ok > /dev/console");
}
else {
......
}

查看变量g_bypass_flag的交叉引用,赋值的地方一共包含如下的三处:

  1. 当请求path中包含currentsetting.htm的时候
    1
    2
    if ( strstr(v86, "currentsetting.htm") )
    g_bypass_flag = 1;
  2. SOAPAction相关,设计的初衷应该是可以访问任意SOAP的xml。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    else
    {
    v26 = strncasecmp(v34, "Accept-Language:", 16);
    v27 = v34;
    if ( v26 )
    {
    v30 = v34 + 11;
    if ( !strncasecmp(v27, "SOAPAction:", 11) )
    {
    v31 = strspn(v30, " \t");
    v32 = strcasestr(&v30[v31], "urn:NETGEAR-ROUTER:service:");
    v33 = 0;
    if ( v32 )
    {
    while ( 1 )
    {
    v11 = *(char *)(v32 + v33 + 27);
    if ( v11 == ':' )
    break;
    byte_420224[v33++] = v11;
    }
    byte_420224[v33] = 0;
    g_bypass_flag = 1;
    }
    }
    }
  3. 请求path中包含setupwizard.cgi,但是随后的处理逻辑会调用exit退出,因此无法利用。这个可能是当设备首次启动、开始安装向导触发的。
    1
    2
    if ( strstr((const char *)g_path, "setupwizard.cgi") )
    g_bypass_flag = 1;

再次返回到mini_httpd的源代码中,结合固件中的反汇编

  • 首先通过查找method后的第一个空格、换行、制表符的方式,获取到path。但是随后没有对path中是否包含%00进行判断。
    1
    2
    v10 = strpbrk(v8, " \t\n\r");
    g_path = v10;
  • 获取到的path在内存中大概是:uri\0currentsetting.htm,这导致,strstr函数返回一个非空值,就设置了g_bypass_flag,从而通过了auth_check
    1
    2
    3
    4
    5
    6
    ......
    v86 = (const char *)g_path;
    ......
    if ( strstr(v86, "currentsetting.htm") )
    g_bypass_flag = 1;
    ......

小结

本文首先分析了mini_httpd中如何对请求包进行处理,登录认证的大致逻辑是什么;随后介绍了搜索字符串的方式在mini_httpd程序中快速定位到请求包处理函数handle_request和权限处理函数auth_check;最后反编译+结合源码的方式,分析了认证绕过的触发逻辑。

这个漏洞实际上刚开始接触IoT漏洞的时候分析过,但是当时对漏洞的触发、利用逻辑虽然分析了,但是不是很有逻辑,后续回头看感觉分析得真烂。这次结合mini_httpd源码分析,也学习到了以后如何去寻找相关的认证绕过思路。