php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Sec Bug #69364 PHP Multipart/form-data remote dos Vulnerability
Submitted: 2015-04-03 07:16 UTC Modified: 2015-05-21 05:00 UTC
From: liushusheng at baidu dot com Assigned: stas
Status: Closed Package: HTTP related
PHP Version: Irrelevant OS: all
Private report: No CVE-ID: 2015-4024
 [2015-04-03 07:16 UTC] liushusheng at baidu dot com
Description:
------------
PHP Multipart/form-data remote dos Vulnerability 
Author: Shusheng Liu, The Department of Security Cloud, Baidu,China

1. Description: 
    PHP is vulnerable to a remote denial of service, caused by repeatedly allocate memory、concatenate string、copy string and free memory when PHP parses header areas of body part of HTTP request with multipart/form-data. By sending multiple HTTP multipart requests to an affected application containing malicious header area of body part, a remote attacker could exploit this vulnerability to cause the consumption of CPU resources. 

2. Analysis 

2.1. Entry-point of The Remote Denial of Service Vulnerability 
The vulnerable function is multipart_buffer_headers that is called internally by the function SAPI_POST_HANDLER_FUNC in main/rfc1867.c. SAPI_POST_HANDLER_FUNC is the entry-point function which parses body parts of HTTP request with multipart/form-data. 

There is a remote denial of service vulnerability when multipart_buffer_headers is called. The source code of the multipart_buffer_headers function is shown as follows: 
------------------------------------------------------------------------------------------------------------------- 
/* parse headers */ 
static int multipart_buffer_headers(multipart_buffer *self, zend_llist *header TSRMLS_DC) 
{ 
char *line; 
mime_header_entry prev_entry = {0}, entry; 
int prev_len, cur_len; 

/* didn't find boundary, abort */ 
if (!find_boundary(self, self->boundary TSRMLS_CC)) { 
return 0; 
} 

/* get lines of text, or CRLF_CRLF */ 

while( (line = get_line(self TSRMLS_CC)) && line[0] != '\0' ) 
{ 
/* add header to table */ 
char *key = line; 
char *value = NULL; 

if (php_rfc1867_encoding_translation(TSRMLS_C)) { 
self->input_encoding = zend_multibyte_encoding_detector(line, strlen(line), self->detect_order, self->detect_order_size TSRMLS_CC); 
} 

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

return 1; 
} 
--------------------------------------------------------------------------------------------------------------- 

2.2 Analyze The Vulnerable Function 
Now, we detailedly analyze logic of multipart_buffer_headers, and then we prove denial of service vulnerability of multipart_buffer_headers. 
Step 1. The multipart_buffer_headers executes while loop cycle to parse current body part headers, if the boundary string was found. 
while( (line = get_line(self TSRMLS_CC)) && line[0] != '\0') 
Step 2. 
Step 2.1. When parseing current body part headers which is represented as (header, value), the multipart_buffer_headers function firstly call get_line function to read a line of characters, but get_line return a line when it meets character '\n', not '\r\n'. After getting a line which is stored in the variable 'line', the multipart_buffer_headers function parses the variable line. 

Step 2.2. If the first character of the line is blank character (corresponding code "if (!isspace(line[0]))"), or the variable line contains character ':', the multipart_buffer_headers function successfully parse a header pair that is represented as entry=(header,value). And then, it calls zend_llist_add_element function to store entry, and use variable prev_entry to record lastest parsed name value pair entry. After that , it go to next cycle. 

Step 2.3. In this step, the multipart_buffer_headers function thinks current line is not a new header, and current line should be append to value of prev_entry. Thus, prev_entry and current line merge into a new entry by executing the following codes: 
------------------------------------------------------------------------------------------------------------------------ 
prev_len = strlen(prev_entry.value); 
cur_len = strlen(line); 

entry.value = emalloc(prev_len + cur_len + 1); //allocate (prev_len + cur_len) bytes memory. 
memcpy(entry.value, prev_entry.value, prev_len); //copy prev_len bytes. 
memcpy(entry.value + prev_len, line, cur_len); // cope (prev_len + cur_len) bytes memory. 
entry.value[cur_len + prev_len] = '\0'; 

entry.key = estrdup(prev_entry.key); 

zend_llist_remove_tail(header); // free memory 
---------------------------------------------------------------------------------------------------------------------- 

2.3 The Remote Denial of Service Vulnerability 
If value of body part header consists of n lines, and first character of each line is not blank character, and each line did constains character ':', the multipart_buffer_headers function executes Step 2.3 (n-1) times when 
the multipart_buffer_headers function parse the header. The block of code allocates memory once, executes string copy operation twice, frees memory once. Each time mergence of entry.value increase length of body part header's value, thus string copy operations will cause the consumption of CPU resources, and then the service is not available. If n is the length of body part header's value, and copying one byte is the unit time complexity,the time complexity of multipart_buffer_headers function is O(n*m). 

For example: 
------WebKitFormBoundarypE33TmSNWwsMphqz 
Content-Disposition: form-data; name="file"; filename="s 
a 
a 
a 
a" 
Content-Type: application/octet-stream 

<?php phpinfo();?> 
------WebKitFormBoundarypE33TmSNWwsMphqz 

The value of Content-Disposition consists of 5 lines, and the length of the value of Content-Disposition is 5. The multipart_buffer_headers function executes Step 2.3 4 times. The first time execution copys 2 bytes, The second execution copys 3 bytes, The third time execution copys 4 bytes, The fourth time execution copys 5 bytes. Thus, the multipart_buffer_headers function executes 14 times byte copy operation. 

Default maximum size of body part is 2097152 bytes (2M), It is enough to cause the consumption of CPU resources by sending multiple HTTP multipart requests to an affected application containing malicious header area of body part.

Test script:
---------------
The poc was sent to the email security@php.net by liushusheng@baidu.com.


Expected result:
----------------
Poc prints response time of a dos-attack request。response time of the dos-attack request more than 5 seconds.


Patches

patch-5.4 (last revision 2015-05-10 06:00 UTC) by stas@php.net)

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2015-05-07 07:45 UTC] liushusheng at baidu dot com
what is state of the vulnerability?
 [2015-05-07 07:49 UTC] stas@php.net
Nobody had time to look into it yet, but I'll try to do it soon.
 [2015-05-10 06:00 UTC] stas@php.net
The following patch has been added/updated:

Patch Name: patch-5.4
Revision:   1431237650
URL:        https://bugs.php.net/patch-display.php?bug=69364&patch=patch-5.4&revision=1431237650
 [2015-05-10 06:03 UTC] stas@php.net
Patch at https://gist.github.com/smalyshev/da7fce7fdebed433184b should fix the issue, please verify. This applies to 5.4, should also work with 5.5 and 5.6.
 [2015-05-12 04:49 UTC] liushusheng at baidu dot com
I had verified that patch at https://gist.github.com/smalyshev/da7fce7fdebed433184b fixes the vulnerability of 5.5.20 Basically, but not perfectly. Using smart_str as buffer, the smart_str_appends function will not copy header value every line repeatly. But the total length of smart_str always increases 128-byte multiples, the smart_str_appends function will repeatly copy header value each 128 bytes. 

After patched, testing shows that 520000-bytes header value will cost 51 ms. If a Multipart/form-data request consist of n body parts,patched php 5.5.20 will costs (51 * n) ms. In the default configuration of php, maximal of n is 20. Thus, patched php 5.5.20 pay 1020ms to deal a malicious requests without considering limitation of post body size.

Moreover, "buf_value.c = NULL;" lead to memory leak, if  smart_str do not have automatic memory recovery mechanism.
 [2015-05-12 05:15 UTC] stas@php.net
Nor sure I understand, why you're saying buf_value.c = NULL leads to memory leak? Which memory is leaked? How did you reproduce the memory leak, with which tool and what was the output of the tool?
 [2015-05-12 05:43 UTC] liushusheng at baidu dot com
sorry , i am fault, memory leak is not exist.
 [2015-05-12 19:40 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=4605d536d23b00813d11cc906bb48d39bdcf5f25
Log: Fixed bug #69364 - use smart_str to assemble strings
 [2015-05-12 19:40 UTC] stas@php.net
-Status: Open +Status: Closed
 [2015-05-12 22:58 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=4605d536d23b00813d11cc906bb48d39bdcf5f25
Log: Fixed bug #69364 - use smart_str to assemble strings
 [2015-05-13 10:53 UTC] jpauli@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=676e0c0f7b294f8948f77a213d22ce9cdd014e4f
Log: Fixed bug #69364 - use smart_str to assemble strings
 [2015-05-15 05:37 UTC] laruence@php.net
-Assigned To: +Assigned To: laruence
 [2015-05-15 05:37 UTC] laruence@php.net
does this needs a CVE id?
 [2015-05-15 05:42 UTC] stas@php.net
yes,
 [2015-05-15 05:45 UTC] liushusheng at baidu dot com
yes , i need CVE-ID. Why this vulnerability did have cve-id.
 [2015-05-15 05:46 UTC] liushusheng at baidu dot com
yes , i need CVE-ID. Why this vulnerability did not have cve-id.
 [2015-05-15 13:40 UTC] laruence@php.net
-Assigned To: laruence +Assigned To: stas
 [2015-05-15 13:40 UTC] laruence@php.net
please apply a CVE ID for this, I don't know how to. .thanks :)
 [2015-05-15 15:07 UTC] kaplan@php.net
We already asked for a CVE.
 [2015-05-19 03:04 UTC] welpher dot yu at gmail dot com
what about 5.3?
 [2015-05-19 03:47 UTC] stas@php.net
5.3 is EOL since last year: http://php.net/supported-versions.php
 [2015-05-19 05:32 UTC] laruence@php.net
-CVE-ID: +CVE-ID: 2015-4024
 [2015-05-20 03:07 UTC] mrbaiwei at gmail dot com
we need php-5.3.29/php-5.2.17 patch for this bug,can you additional provide it?
 [2015-05-20 03:38 UTC] czxin788 at qq dot com
I have download the path from https://bugs.php.net/patch-display.php?bug_id=69364&patch=patch-5.4&revision=1431237650&download=1,but I don't know how to install the path.I found it is a text.
 Expect your answer,thank you!
 [2015-05-20 08:20 UTC] lishaobang at nawang dot cn
#cd php-5.4.12
#patch -p1 < patch-5.4.patch.txt
./configure --prefix=/usr/local/php --with-config-file-path=/usr/local/php/etc --with-mysql=/usr/local/mysql/ --with-mysqli=/usr/local/mysql/bin/mysql_config --with-pdo-mysql=/usr/local/mysql/ --with-iconv-dir=/usr/local --enable-fpm --disable-phar --with-fpm-user=www --with-fpm-group=www --with-pcre-regex --with-config-file-scan-dir=/usr/local/php/etc/php.d --with-zlib --with-bz2 --enable-calendar --with-curl --enable-dba --with-libxml-dir --enable-ftp --with-gd --with-jpeg-dir --with-png-dir --with-zlib-dir --with-freetype-dir --enable-gd-native-ttf --enable-gd-jis-conv --with-mhash --enable-mbstring --with-mcrypt --enable-pcntl --enable-xml --disable-rpath --enable-shmop --enable-sockets --enable-zip --enable-bcmath --enable-ipv6 --with-xmlrpc --enable-exif --enable-soap --enable-pdo --with-pdo-mysql --enable-sysvsem --enable-inline-optimization --with-curlwrappers
#make ZEND_EXTRA_LIBS='-liconv'
then it come error:
/root/soft/soft/php5.4/php-5.4.12/main/rfc1867.c: 在函数‘multipart_buffer_headers’中:
/root/soft/soft/php5.4/php-5.4.12/main/rfc1867.c:427: 错误:‘buf_value’未声明(在此函数内第一次使用)
/root/soft/soft/php5.4/php-5.4.12/main/rfc1867.c:427: 错误:(即使在一个函数内多次出现,每个未声明的标识符在其
/root/soft/soft/php5.4/php-5.4.12/main/rfc1867.c:427: 错误:所在的函数内也只报告一次。)
/root/soft/soft/php5.4/php-5.4.12/main/rfc1867.c:427: 错误:‘key’未声明(在此函数内第一次使用)
make: *** [main/rfc1867.lo] 错误 1
 [2015-05-21 02:05 UTC] 376166899 at qq dot com
what about PHP-4.4? I do not know how to text it
 [2015-05-21 05:00 UTC] rasmus@php.net
There will be no further releases of PHP 4
 [2015-05-21 07:01 UTC] blackhat2014 at 163 dot com
patching file main/rfc1867.c
Hunk #2 FAILED at 399.
1 out of 5 hunks FAILED -- saving rejects to file main/rfc1867.c.rej

php5.4.13 how to fix it!
I use the patch.
 
PHP Copyright © 2001-2017 The PHP Group
All rights reserved.
Last updated: Tue Feb 28 10:01:41 2017 UTC