Glibc ghost(CVE-2015-0235)漏洞簡要分析及檢測

1.漏洞簡介

代碼審計公司Qualys的研究人員在glibc庫中的__nss_hostname_digits_dots()函數中發現了一個緩衝區溢出的漏洞,這個bug可以經過gethostbyname*()函數被本地或者遠程的觸發。應用程序主要使用gethostbyname*()函數發起DNS請求,這個函數會將主機名稱轉換爲ip地址。

2.POC驗證

測試漏洞的POC來自http://www.openwall.com/lists/oss-security/2015/01/27/9的第4節,主要代碼如下,原理是通過調用gethostbyname_r函數,關鍵參數爲temp和name。拿64位系統舉例,如果經過gethostbyname_r的調用剛好能夠覆蓋temp.buffer的1024個字節,那麼temp.canary就不會變,則glibc庫沒有該漏洞,但是如果temp.canary的前8字節被覆蓋爲0則說明漏洞存在:

#define CANARY "in_the_coal_mine"

struct {
  char buffer[1024];
  char canary[sizeof(CANARY)];
} temp = { "buffer", CANARY };

int main(void) {
  ... ...
  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);
  }
  ... ...
}

POC運行成功:

這裏寫圖片描述

3.漏洞原理與修複分析

漏洞原理

有漏洞的函數是nss/digits_dots.c中的__nss_hostname_digits_dots函數,而這個函數只有在nss/getXXbyYY.c和nss/getXXbyYY_r.c中被用到,且僅當定義了HANDLE_DIGITS_DOTS宏時纔會被調用,這個宏則是定義在inet目錄中的gethstbynm系列文件中,因此僅當調用gethostbyname*系列函數時會被調用:

#ifdef HANDLE_DIGITS_DOTS
  switch (__nss_hostname_digits_dots (name, resbuf, &buffer, NULL,
                      buflen, result, &status, AF_VAL,
                      H_ERRNO_VAR_P))
    {
    case -1:
      return errno;
    case 1:
      goto done;
    }
#endif

下面來進入__nss_hostname_digits_dots函數分析:

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;
      int addr_size;

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

      if (buffer_size == NULL)
        {
      if (buflen < size_needed)
        {
          if (h_errnop != NULL)
        *h_errnop = TRY_AGAIN;
          __set_errno (ERANGE);
          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;
    }

      memset (*buffer, '\0', size_needed);

      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)
            {
              *h_errnop = HOST_NOT_FOUND;
              if (buffer_size)
            *result = NULL;
              goto done;
            }
        }
          ... ...
          resbuf->h_name = strcpy (hostname, name);
          h_alias_ptr[0] = NULL;
          resbuf->h_aliases = h_alias_ptr;
          (*h_addr_ptrs)[0] = (char *) host_addr;
          (*h_addr_ptrs)[1] = NULL;
          resbuf->h_addr_list = *h_addr_ptrs;
          if (af == AF_INET && (_res.options & RES_USE_INET6))
            {
              ... ...
          goto done;
        }

          if (!isdigit (*cp) && *cp != '.')
        break;
        }
    }
... ...

done:
  return 1;
}

在函數的第21行計算需要的內存總量size_needed,包括了host_addr指針,h_addr_ptrs指針,name的長度還有一個’\0’的長度。24-46行是判斷計算出來的長度是否符合傳入的緩衝區的大小。48行將緩衝區置0,隨後計算host_addr 、h_addr_ptrs、h_alias_ptr和hostname的起始地址,可以看到這裏計算的存在於緩衝區中的長度比size_needed多了一個h_alias_ptr的指針長度,也就是32位系統多4字節,64位系統多8字節的長度。下圖比較直觀的給出了溢出的原因就是沒有考慮h_alias_ptr指針的長度。

這裏寫圖片描述

如要要使程序的流程執行到68行的strcpy函數,name就必須要滿足以下幾個條件:

1.name的第一個字符必須是數字
2.name的最後一個字符不能是‘.’
3.name的所有字符只能是數字或‘.’
4.如果是IPv4地址必須通過__inet_aton函數的驗證,如果是IPv6則需通過inet_pton,但是IPv6地址包含‘:’,所有這裏排除,因此格式必須爲下面幾種之一:"a.b.c.d""a.b.c","a.b","a",且a,b,c,d均不能超過無符號整形的最大值也就是0xFFFFFFFF
5.name的長度至少長於10241KB),至於爲什麼目前還不太清楚,可能是Linux的堆分配最小就是1024字節,後面需要再研究

修複分析

修復也非常容易,只需在計算size_needed的時候加上h_alias_ptr指針的長度就可以了,修復代碼如下:

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

升級之後的POC運行失敗:

這裏寫圖片描述

4.各種利用場景

4.1 WordPress的xmlrpc遠程調用pingback

Wordpress 3.5以上默認開啓xmlrpc功能,客戶端無需認證即可通過該功能調用pingback_ping函數,這個函數實現了wordpress得Pingback功能。首先看Wordpress 4.1.1的源碼,下面是pingback_ping的源碼,其中的wp_safe_remote_get是發送http請求的函數,解析根據url獲取IP地址也是這個函數內完成的:

    public function pingback_ping($args) {
        ... ...
        $pagelinkedfrom = $args[0];
        $pagelinkedto   = $args[1];
        ... ...
        // Let us check the remote site
        $http_api_args = array(
            'timeout' => 10,
            'redirection' => 0,
            'limit_response_size' => 153600, // 150 KB
            'user-agent' => "$user_agent; verifying pingback from $remote_ip",
            'headers' => array(
                'X-Pingback-Forwarded-For' => $remote_ip,
            ),
        );
        $request = wp_safe_remote_get( $pagelinkedfrom, $http_api_args );
        $linea = wp_remote_retrieve_body( $request );
        ... ...
    }

wp_safe_remote_get函數代碼,其調用的_wp_http_get_object函數獲取WP_Http類的一個對象,隨後調用其get函數:

function wp_safe_remote_get( $url, $args = array() ) {
    $args['reject_unsafe_urls'] = true;
    $http = _wp_http_get_object();
    return $http->get( $url, $args );
}

WP_Http的get函數代碼如下,這個函數會調用同一個類中的request函數:

public function get($url, $args = array()) {
        $defaults = array('method' => 'GET');
        $r = wp_parse_args( $args, $defaults );
        return $this->request($url, $r);
    }

WP_Http->request部分代碼如下,調用wp_http_validate_url函數對傳入的url進行驗證:

    public function request( $url, $args = array() ) {
        ... ...
        if ( function_exists( 'wp_kses_bad_protocol' ) ) {
            if ( $r['reject_unsafe_urls'] )
                $url = wp_http_validate_url( $url );
            $url = wp_kses_bad_protocol( $url, array( 'http', 'https', 'ssl' ) );
        }
        ... ...
    }

wp_http_validate_url代碼如下,在去掉首尾的“.”之後進行匹配,如果是url遵循標準的IPv4的IP地址,則直接獲取IP,否則調用PHP的gethostbyname函數以獲取IP地址:

function wp_http_validate_url( $url ) {
    ... ...
    $parsed_url = @parse_url( $url );
    ... ...
    if ( ! $same_host ) {
        $host = trim( $parsed_url['host'], '.' );
        if ( preg_match( '#^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $host ) ) {
            $ip = $host;
        } else {
            $ip = gethostbyname( $host );
            if ( $ip === $host ) // Error condition for gethostbyname()
                $ip = false;
        }
        ... ...
    }
    ... ...
}

接下去就進入PHP的源碼了,其中gethostbyname函數代碼如下,調用了php_gethostbyname函數:

PHP_FUNCTION(gethostbyname)
{
    char *hostname;
    int hostname_len;
    char *addr;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &hostname, &hostname_len) == FAILURE) {
        return;
    }

    addr = php_gethostbyname(hostname);

    RETVAL_STRING(addr, 0);
}

php_gethostbyname代碼如下,可以看到這裏直接調用了glibc中的gethostbyname函數,後續的執行便會造成緩衝區溢出:

static char *php_gethostbyname(char *name)
{
    struct hostent *hp;
    struct in_addr in;

    hp = gethostbyname(name);

    if (!hp || !*(hp->h_addr_list)) {
        return estrdup(name);
    }

    memcpy(&in.s_addr, *(hp->h_addr_list), sizeof(in.s_addr));

    return estrdup(inet_ntoa(in));
}

Metasploit的auxiliary/scanner/http/wordpress_ghost_scanner模塊可以攻擊有類似問題的wordpress網站(前提是開啓xmlrpc功能,3.5版本以上默認開啓),發送的數據包如下,可以看到在http包的數據部分,寫明瞭調用pingback.ping函數,且兩個參數均爲超長的滿足條件的url,達到遠程崩潰的目的,如果精心構造可以達到遠程執行任意代碼的目的:

這裏寫圖片描述

遠程攻擊成功:

這裏寫圖片描述

4.2 iputils工具集

iputils工具集是linux環境下一些實用的網絡工具的集合,包含以下幾個工具:

1. ping。使用 ping可以測試計算機名和計算機的ip地址,驗證與遠程計算機的連接。ping程序由ping.c ping6.cping_common.c ping.h 文件構成。
2. tracepath。與traceroute功能相似,使用tracepath測試IP數據報文從源主機傳到目的主機經過的路由。tracepath程序由tracepath.c tracepath6.c traceroute6.c 文件構成。
3. arping。使用arping向目的主機發送ARP報文,通過目的主機的IP獲得該主機的硬件地址。arping程序由arping.c文件構成。
4. tftpd。tftpd是簡單文件傳送協議TFTP的服務端程序。tftpd程序由tftp.h tftpd.c tftpsubs.c文件構成。
5. rarpd。rarpd是逆地址解析協議的服務端程序。rarpd程序由rarpd.c文件構成。
6. clockdiff。使用clockdiff可以測算目的主機和本地主機的系統時間差。clockdiff程序由clockdiff.c文件構成。
7. rdisc。rdisc是路由器發現守護程序。rdisc程序由rdisc.c文件構成。

經過查看代碼發現clockdiff和tracepath兩個程序的代碼調用的gethostbyname函數存在漏洞,其餘的均由於條件無法滿足無法觸發。

4.2.1 clockdiff

首先是clockdiff.c,這個程序在main函數中調用gethostbyname函數,代碼如下所示,第一次調用沒問題,第二次調用的參數就是用戶傳入並可控的:

int
main(int argc, char *argv[])
{
    ... ...
    (void)gethostname(hostname,sizeof(hostname));
    hp = gethostbyname(hostname);
    if (hp == NULL) {
        fprintf(stderr, "clockdiff: %s: my host not found\n", hostname);
        exit(1);
    }
    myname = strdup(hp->h_name);

    hp = gethostbyname(argv[1]);
    if (hp == NULL) {
        fprintf(stderr, "clockdiff: %s: host not found\n", argv[1]);
        exit(1);
    }
    ... ...
}

執行結果如下:

這裏寫圖片描述

4.2.2 tracepath

tracepath.c文件也是調用gethostbyname函數,參數也是用戶可控:

int
main(int argc, char *argv[])
{
    ... ...
    sin.sin6_family = AF_INET6;

    p = strchr(argv[0], '/');
    if (p) {
        *p = 0;
        sin.sin6_port = htons(atoi(p+1));
    } else
        sin.sin6_port = htons(0x8000 | getpid());
    he = gethostbyname2(argv[0], AF_INET6);
    ... ...
}

執行結果如下:

這裏寫圖片描述

4.3 Exim郵件服務器

在Exim郵件服務器中也會調用gethostbyname函數來解析SMTP客戶端發來的數據包,main函數中調用smtp_setup_msg函數用來處理接收到的數據包以及組裝服務器要響應的數據包。部分代碼如下:

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;

    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;

    if (!check_helo(smtp_cmd_data))
    {
        ... ...
        break;
    }
    helo_verified = helo_verify_failed = FALSE;
    if (helo_required || helo_verify)
      {
      BOOL tempfail = !smtp_verify_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的命令。當命令爲’HELO’或’EHLO’時便會執行到處理helo命令的部分。因爲SMTP的helo數據包後跟着的必須是IP地址,因此調用check_helo對這部分進行驗證,主要是對IPv4和IPv6的進行分別驗證,IP地址可以是[IPv4:地址]、[IPv6:地址]、[IP地址]、正常的IP地址格式。經過驗證之後會調用smtp_verify_helo函數,這個函數對IP地址的有效性進行驗證,但Exim的配置文件必須打開helo_verify_hosts或helo_try_verify_hosts開關,smtp_verify_helo函數部分代碼如下:

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);
    ... ...
    }
    ... ...
}

這裏會調用host_find_byname函數來獲得helo後的IP地址,

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 */
     ... ...
    }
    ... ...
}

可以看到這裏調用了gethostbyname2和gethostbyname針對IPv6和IPv4地址進行處理,傳入超長數據便會觸發漏洞。telnet客戶端連接之後如下所示,最後一個錯誤是本次觸發的,前幾個錯誤是之前測試的時候觸發的:

這裏寫圖片描述

5.Snort檢測規則

5.1 針對xmlrpc調用的snort規則

這條規則僅檢測攻擊wordpress的pingback函數:

alert http any any -> $HOME_NET 80 (msg:"Glibc Ghost Attack - Wordpress xmlrpc call ping request"; flow:to_server, established; content:"xmlrpc.php"; http_uri; nocase; content:"methodCall"; http_client_body; nocase; content:"pingback.ping"; http_client_body;nocase; threshold:type limit, track by_src, count 1, seconds 120; reference:cve,2015-0235; classtype:current-event; sid:201502350; rev:1;)
規則說明:
1.目的端口爲80,url種包含"xmlrpc.php"
2.http數據部分包含"methodCall",往後能匹配"pingback.ping"
3.同IP發動的攻擊120秒內只發一次報警
4.CVE索引號CVE-2015-0235,類型爲即時事件

5.2 針對Exim郵件服務器的SMTP攻擊

攻擊的msf模塊在http://packetstormsecurity.com/files/130974/Exim-GHOST-glibc-gethostbyname-Buffer-Overflow.html處可以找到,直接放在metasploit的exploit/linux/smtp目錄下就可以用了。針對這個攻擊模塊的snort檢測規則如下,當然這裏的IP地址可以換成IPv4的形式:

alert smtp any any -> $HOME_NET 25 (msg:"Glibc Ghost Attack - Exim remote script execute"; content:"HELO [1:2:3:4:5:6:7:8%eth0:"; reference:cve,2015-0325; threshold:type limit, track by_src, count 1, seconds 120; classtype:current-event; sid:130102354; rev:2;)

而另外的兩條針對通用利用的規則來自於Emerging Threats,鏈接是http://rules.emergingthreats.net/open/suricata/rules/emerging-exploit.rules,主要原理就是檢查HELO命令的參數是否爲超過1024字節並且以數字開頭,由數字和點組成:

alert tcp $EXTERNAL_NET any -> $HOME_NET [25,465,587] (msg:"ET EXPLOIT CVE-2015-0235 Exim Buffer Overflow Attempt (HELO)"; flow:to_server,established; content:"HELO "; nocase; content:!"|0a|"; within:1024; pcre:"/^\s*?\d[\d\x2e]{255}/R"; reference:url,openwall.com/lists/oss-security/2015/01/27/9; classtype:attempted-admin; sid:2020325; rev:2;)

alert tcp $EXTERNAL_NET any -> $HOME_NET [25,465,587] (msg:"ET EXPLOIT CVE-2015-0235 Exim Buffer Overflow Attempt (EHLO)"; flow:to_server,established; content:"EHLO "; nocase; content:!"|0a|"; within:1024; pcre:"/^\s*?\d[\d\x2e]{255}/R"; reference:url,openwall.com/lists/oss-security/2015/01/27/9; classtype:attempted-admin; sid:2020326; rev:4;)

參考鏈接

http://www.openwall.com/lists/oss-security/2015/01/27/9
http://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_ghost_scanner
http://ftp.gnu.org/gnu/glibc/
http://codex.wordpress.org.cn/XML-RPC_Support
http://packetstormsecurity.com/files/130974/Exim-GHOST-glibc-gethostbyname-Buffer-Overflow.html
http://rules.emergingthreats.net/open/suricata/rules/emerging-exploit.rules
http://www.freebuf.com/news/57729.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章