OneShell

I fight for a brighter tomorrow

0%

IoT小设备常见webserver分析及漏洞挖掘思路浅析:mini_httpd篇

简介

mini_httpd是一个小型的HTTP服务器,它的代码非常小巧,仅有几千行代码,因此它适用于嵌入式设备和低功耗系统。它是由Jef Poskanzer开发的,可以在多种操作系统上运行,包括Linux、FreeBSD、Solaris、Windows等。mini_httpd支持动态内容的生成,包括CGI、SSI以及FastCGI,同时它也支持虚拟主机和基本的身份验证。mini_httpd的主要特点是快速、轻量级、安全和易于使用。

mini_httpd的代码下载地址:mini_httpd

数据包处理逻辑

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

1
2
3
4
5
6
7
8
9
10
11
/* Fork a sub-process to handle the connection. */
r = fork();
...
if ( r == 0 )
{
/* Child process. */
...
handle_request(); // 数据包处理逻辑
}
...
}

子进程中主要是通过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
   /* 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 )
{
...
}
else if ( strncasecmp( line, "Content-Length:", 15 ) == 0 )
{
...
}

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

  • 请求方法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"
  • 函数handle_request中的逻辑是由开发者定义,因此可能发生缓冲区溢出、命令注入等常见漏洞形式
  • 通过搜索字符串.htpasswd可以直接定位到do_file、auth_check函数。do_file函数中会检查请求文件是否为.htpasswd,auth_check函数则是需要读取账号信息、调用字符串比较函数等

经典案例

案例1:发生在auth_check中的认证绕过

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

通过之前的源代码梳理,也明白了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
    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:");
    ......
    if ( v32 )
    {
    ......
    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;
    ......

案例2:发生在函数handle_request

CVE-2021-34979

发生在NETGEAR R6260,固件版本V1.1.0.78_1.0.1中,处理SOAPAction标头由于未判断全局数组spapServiceName的边界,导致越界写。写入的数据会以环境变量的形式传递到setupwizard.cgi中,进而造成缓冲区溢出。

越界写:发生在处理数据包的函数handle_request中,未判断边界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
else if (strncasecmp(line, "SOAPAction:", 11) == 0)
{
char *pTemp = NULL;
cp = &line[11];
cp += strspn(cp, " \t");
pTemp = strcasestr(cp, "urn:NETGEAR-ROUTER:service:");
if (pTemp != NULL)
{
int i = 0;
pTemp += strlen("urn:NETGEAR-ROUTER:service:");
while (*pTemp != ':' && *pTemp != '\0')
{
soapServiceName[i++] = *pTemp; // <-- Out-Of-Bounds Write
pTemp++;
}
}
}

后续调用setupwizard.cgi时,环境变量会传入,并且造成缓冲区溢出。

1
2
3
4
5
6
7
8
9
10
bool check_soap_login_record()
{
...
v1 = getenv("SOAP_LOGIN_TOKEN");
...
if ( !v3 )
{
...
strcat((char *)v25, v1);
...

小结

mini_httpd主要容易发生漏洞的地方就是在处理数据包的函数handle_request处,因为此处是开发者主要添加自己代码的地方,例如对header的处理、认证的自我实现方式等等。