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的長度至少長於1024(1KB),至於爲什麼目前還不太清楚,可能是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