文件上传漏洞

文件上传漏洞

文件上传漏洞

简介

一些Web应用程序中允许上传图片,文本或者其他资源到指定的位置。
文件上传漏洞就是利用网页代码中的文件上传路径变量过滤不严将可执行的文件上传到一个到服务器中,再通过URL去访问以执行恶意代码

检测与绕过

Pass-01-js检查

一般都是在网页上写一段javascript脚本,校验上传文件的后缀名,有白名单形式也有黑名单形式。
这种直接禁用JS,或者burp改包等等都可以。

Pass-02-只验证Content-type

is_upload = false;msg = null;
if (isset(_POST['submit'])) { if (file_exists(UPLOAD_PATH)) { if ((_FILES['upload_file']['type'] == 'image/jpeg') || (_FILES['upload_file']['type'] == 'image/png') || (_FILES['upload_file']['type'] == 'image/gif')) {
temp_file =_FILES['upload_file']['tmp_name'];
img_path = UPLOAD_PATH . '/' ._FILES['upload_file']['name']
if (move_uploaded_file(temp_file,img_path)) {
is_upload = true; } else {msg = '上传出错!';
}
} else {
msg = '文件类型不正确,请重新上传!'; } } else {msg = UPLOAD_PATH.'文件夹不存在,请手工创建!';
}
}
抓包改Content-Type即可。

Pass-03-黑名单绕过

is_upload = false;msg = null;
if (isset(_POST['submit'])) { if (file_exists(UPLOAD_PATH)) {deny_ext = array('.asp','.aspx','.php','.jsp');
file_name = trim(_FILES['upload_file']['name']);
file_name = deldot(file_name);//删除文件名末尾的点
file_ext = strrchr(file_name, '.');
file_ext = strtolower(file_ext); //转换为小写
file_ext = str_ireplace('::DATA', '', file_ext);//去除字符串::DATA
file_ext = trim(file_ext); //收尾去空

    if(!in_array($file_ext, $deny_ext)) {
        $temp_file = $_FILES['upload_file']['tmp_name'];
        $img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;            
        if (move_uploaded_file($temp_file,$img_path)) {
             $is_upload = true;
        } else {
            $msg = '上传出错!';
        }
    } else {
        $msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
    }
} else {
    $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}

}
不允许上传.asp,.aspx,.php,.jsp后缀文件,但是可以上传其他任意后缀

.php .phtml .phps .php5 .pht

具体可以参考https://www.jianshu.com/p/1ccbab572974
前提是apache的httpd.conf中有如下配置代码

AddType application/x-httpd-php .php .phtml .phps .php5 .pht

或者上传.htaccess文件需要:
1.mod_rewrite模块开启。
2.AllowOverride All
文件内容

<FilesMatch "shell.jpg">
SetHandler application/x-httpd-php
</FilesMatch>

此时上传shell.jpg文件即可被当作php来解析。
或者

AddType application/x-httpd-php .jpg

另外基本上所有的黑名单都可以用Apache解析漏洞绕过。

Pass-04-.htaccess绕过

is_upload = false;msg = null;
if (isset(_POST['submit'])) { if (file_exists(UPLOAD_PATH)) {deny_ext = array(".php",".php5",".php4",".php3",".php2","php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2","pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf");
file_name = trim(_FILES['upload_file']['name']);
file_name = deldot(file_name);//删除文件名末尾的点
file_ext = strrchr(file_name, '.');
file_ext = strtolower(file_ext); //转换为小写
file_ext = str_ireplace('::DATA', '', file_ext);//去除字符串::DATA
file_ext = trim(file_ext); //收尾去空

    if (!in_array($file_ext, $deny_ext)) {
        $temp_file = $_FILES['upload_file']['tmp_name'];
        $img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
        if (move_uploaded_file($temp_file, $img_path)) {
            $is_upload = true;
        } else {
            $msg = '上传出错!';
        }
    } else {
        $msg = '此文件不允许上传!';
    }
} else {
    $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}

}
过滤了各种罕见后缀
array(".php",".php5",".php4",".php3",".php2","php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2","pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf");
但是没有过滤.htaccess,用上面的方法即可。

Pass-05-大小写绕过

is_upload = false;msg = null;
if (isset(_POST['submit'])) { if (file_exists(UPLOAD_PATH)) {deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
file_name = trim(_FILES['upload_file']['name']);
file_name = deldot(file_name);//删除文件名末尾的点
file_ext = strrchr(file_name, '.');
file_ext = str_ireplace('::DATA', '', file_ext);//去除字符串::DATA
file_ext = trim(file_ext); //首尾去空

    if (!in_array($file_ext, $deny_ext)) {
        $temp_file = $_FILES['upload_file']['tmp_name'];
        $img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
        if (move_uploaded_file($temp_file, $img_path)) {
            $is_upload = true;
        } else {
            $msg = '上传出错!';
        }
    } else {
        $msg = '此文件类型不允许上传!';
    }
} else {
    $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}

}
过滤了.htaccess,并且代码中后缀转换为小写被去掉了,因此我们可以上传Php来绕过黑名单后缀。(在Linux没有特殊配置的情况下,这种情况只有win可以,因为win会忽略大小写)

Pass-06-空格绕过

Win下xx.jpg[空格] 或xx.jpg.这两类文件都是不允许存在的,若这样命名,windows会默认除去空格或点此处会删除末尾的点,但是没有去掉末尾的空格,因此上传一个.php空格文件即可。

Pass-07-点绕过

没有去除末尾的点,因此与上面同理,上传.php.绕过。

Pass-08-::$DATA绕过

NTFS文件系统包括对备用数据流的支持。这不是众所周知的功能,主要包括提供与Macintosh文件系统中的文件的兼容性。备用数据流允许文件包含多个数据流。每个文件至少有一个数据流。在Windows中,此默认数据流称为:DATA。上传.php::DATA绕过。(仅限windows)

Pass-09-.空格.绕过

move_upload_file的文件名直接为用户上传的文件名,我们可控。且会删除文件名末尾的点,因此我们可以结合Pass-7用.php.空格.绕过。
windows会忽略文件末尾的.和空格。

Pass-10-双写绕过

敏感后缀替换为空,双写.pphphp绕过即可。

Pass-11-00截断

CVE-2015-2348影响版本:5.4.x<= 5.4.39, 5.5.x<= 5.5.23, 5.6.x <= 5.6.7
exp:move_uploaded_file($_FILES['name']['tmp_name'],"/file.php\x00.jpg");源码中move_uploaded_file中的save_path可控,因此00截断即可。

Pass-12-略

把Pass-11的GET方式改成了POST,同理。

Pass-13-16-图片马

上传图片马。
function getReailFileType(filename){file = fopen(filename, "rb");bin = fread(file, 2); //只读2字节 fclose(file);
strInfo = @unpack("C2chars",bin);
typeCode = intval(strInfo['chars1'].strInfo['chars2']);fileType = '';
switch(typeCode){ case 255216:fileType = 'jpg';
break;
case 13780:
fileType = 'png'; break; case 7173:fileType = 'gif';
break;
default:
fileType = 'unknown'; } returnfileType;
}

is_upload = false;msg = null;
if(isset(_POST['submit'])){temp_file = _FILES['upload_file']['tmp_name'];file_type = getReailFileType($temp_file);

if($file_type == 'unknown'){
    $msg = "文件未知,上传失败!";
}else{
    $img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$file_type;
    if(move_uploaded_file($temp_file,$img_path)){
        $is_upload = true;
    } else {
        $msg = "上传出错!";
    }
}

}
制作图片马
copy smi1e.jpg /b + shell.php /a shell.jpg

Pass-14-getimagesize()

同Pass-13

Pass-15-exif_imagetype()

同Pass-13

Pass-16-二次渲染绕过

is_upload = false;msg = null;
if (isset(_POST['submit'])){ // 获得上传文件的基本信息,文件名,类型,大小,临时文件路径filename = _FILES['upload_file']['name'];filetype = _FILES['upload_file']['type'];tmpname = $_FILES['upload_file']['tmp_name'];

$target_path=UPLOAD_PATH.'/'.basename($filename);

// 获得上传文件的扩展名
$fileext= substr(strrchr($filename,"."),1);

//判断文件后缀与类型,合法才进行上传操作
if(($fileext == "jpg") && ($filetype=="image/jpeg")){
    if(move_uploaded_file($tmpname,$target_path)){
        //使用上传的图片生成新的图片
        $im = imagecreatefromjpeg($target_path);

        if($im == false){
            $msg = "该文件不是jpg格式的图片!";
            @unlink($target_path);
        }else{
            //给新图片指定文件名
            srand(time());
            $newfilename = strval(rand()).".jpg";
            //显示二次渲染后的图片(使用用户上传图片生成的新图片)
            $img_path = UPLOAD_PATH.'/'.$newfilename;
            imagejpeg($im,$img_path);
            @unlink($target_path);
            $is_upload = true;
        }
    } else {
        $msg = "上传出错!";
    }

}else if(($fileext == "png") && ($filetype=="image/png")){
    if(move_uploaded_file($tmpname,$target_path)){
        //使用上传的图片生成新的图片
        $im = imagecreatefrompng($target_path);

        if($im == false){
            $msg = "该文件不是png格式的图片!";
            @unlink($target_path);
        }else{
             //给新图片指定文件名
            srand(time());
            $newfilename = strval(rand()).".png";
            //显示二次渲染后的图片(使用用户上传图片生成的新图片)
            $img_path = UPLOAD_PATH.'/'.$newfilename;
            imagepng($im,$img_path);

            @unlink($target_path);
            $is_upload = true;               
        }
    } else {
        $msg = "上传出错!";
    }

}else if(($fileext == "gif") && ($filetype=="image/gif")){
    if(move_uploaded_file($tmpname,$target_path)){
        //使用上传的图片生成新的图片
        $im = imagecreatefromgif($target_path);
        if($im == false){
            $msg = "该文件不是gif格式的图片!";
            @unlink($target_path);
        }else{
            //给新图片指定文件名
            srand(time());
            $newfilename = strval(rand()).".gif";
            //显示二次渲染后的图片(使用用户上传图片生成的新图片)
            $img_path = UPLOAD_PATH.'/'.$newfilename;
            imagegif($im,$img_path);

            @unlink($target_path);
            $is_upload = true;
        }
    } else {
        $msg = "上传出错!";
    }
}else{
    $msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
}

}
判断了后缀名、content-type,以及利用imagecreatefromgif判断是否为gif图片,最后再做了一次二次渲染,绕过方法可以参考先知的文章,写的很详细:https://xz.aliyun.com/t/2657jpg和png很麻烦,gif只需要找到渲染前后没有变化的位置,然后将php代码写进去,就可以了。

Pass-17-条件竞争

is_upload = false;msg = null;

if(isset(_POST['submit'])){ext_arr = array('jpg','png','gif');
file_name =_FILES['upload_file']['name'];
temp_file =_FILES['upload_file']['tmp_name'];
file_ext = substr(file_name,strrpos(file_name,".")+1);upload_file = UPLOAD_PATH . '/' . $file_name;

if(move_uploaded_file($temp_file, $upload_file)){
    if(in_array($file_ext,$ext_arr)){
         $img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
         rename($upload_file, $img_path);
         $is_upload = true;
    }else{
        $msg = "只允许上传.jpg|.png|.gif类型文件!";
        unlink($upload_file);
    }
}else{
    $msg = '上传出错!';
}

}
可以看到文件先经过保存,然后判断后缀名是否在白名单中,如果不在则删除,此时可以利用条件竞争在保存文件后删除文件前来执行php文件。
利用bp不断发送上传包和请求包。

Pass-18-条件竞争

//index.php中
is_upload = false;msg = null;
if (isset(_POST['submit'])) { require_once("./myupload.php");imgFileName =time();
u = new MyUpload(_FILES['upload_file']['name'], _FILES['upload_file']['tmp_name'],_FILES['upload_file']['size'],imgFileName);status_code = u->upload(UPLOAD_PATH); switch (status_code) {
case 1:
is_upload = true;img_path = u->cls_upload_dir .u->cls_file_rename_to;
break;
case 2:
msg = '文件已经被上传,但没有重命名。'; break; case -1:msg = '这个文件不能上传到服务器的临时文件存储目录。';
break;
case -2:
msg = '上传失败,上传目录不可写。'; break; case -3:msg = '上传失败,无法上传该类型文件。';
break;
case -4:
msg = '上传失败,上传的文件过大。'; break; case -5:msg = '上传失败,服务器已经存在相同名称文件。';
break;
case -6:
msg = '文件无法上传,文件不能复制到目标目录。'; break; default:msg = '未知错误!';
break;
}
}

//myupload.php
class MyUpload{
......
......
......
var $cls_arr_ext_accepted = array(
".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
".html", ".xml", ".tiff", ".jpeg", ".png" );

......
......
......
/** upload()
**
** Method to upload the file.
** This is the only method to call outside the class.
** @para String name of directory we upload to
** @returns void
**/
function upload( $dir ){

$ret = $this->isUploadedFile();

if( $ret != 1 ){
  return $this->resultUpload( $ret );
}

$ret = $this->setDir( $dir );
if( $ret != 1 ){
  return $this->resultUpload( $ret );
}

$ret = $this->checkExtension();
if( $ret != 1 ){
  return $this->resultUpload( $ret );
}

$ret = $this->checkSize();
if( $ret != 1 ){
  return $this->resultUpload( $ret );    
}

// if flag to check if the file exists is set to 1

if( $this->cls_file_exists == 1 ){

  $ret = $this->checkFileExists();
  if( $ret != 1 ){
    return $this->resultUpload( $ret );    
  }
}

// if we are here, we are ready to move the file to destination

$ret = $this->move();
if( $ret != 1 ){
  return $this->resultUpload( $ret );    
}

// check if we need to rename the file

if( $this->cls_rename_file == 1 ){
  $ret = $this->renameFile();
  if( $ret != 1 ){
    return $this->resultUpload( $ret );    
  }
}

// if we are here, everything worked as planned :)

return $this->resultUpload( "SUCCESS" );

}
......
......
......
}
因为move在rename之前
move操作进行了一次文件保存
然后rename进行了一次更改文件名

Pass-19-/.绕过

is_upload = false;msg = null;
if (isset(_POST['submit'])) { if (file_exists(UPLOAD_PATH)) {deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");

    $file_name = $_POST['save_name'];
    $file_ext = pathinfo($file_name,PATHINFO_EXTENSION);

    if(!in_array($file_ext,$deny_ext)) {
        $temp_file = $_FILES['upload_file']['tmp_name'];
        $img_path = UPLOAD_PATH . '/' .$file_name;
        if (move_uploaded_file($temp_file, $img_path)) { 
            $is_upload = true;
        }else{
            $msg = '上传出错!';
        }
    }else{
        $msg = '禁止保存为该类型文件!';
    }

} else {
    $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}

}
这里是我在Pass9提到的一个trick,move_uploaded_file会忽略掉文件末尾的/.。但是这里是用户可控的。因此构造shell.php/.
当然也可以用move_uploaded_file函数的00截断漏洞绕过。

Pass-20-数组+/.绕过

is_upload = false;msg = null;
if(!empty(_FILES['upload_file'])){ //检查MIMEallow_type = array('image/jpeg','image/png','image/gif');
if(!in_array(_FILES['upload_file']['type'],allow_type)){
msg = "禁止上传该类型文件!"; }else{ //检查文件名file = empty(_POST['save_name']) ?_FILES['upload_file']['name'] : _POST['save_name']; if (!is_array(file)) {
file = explode('.', strtolower(file));
}

    $ext = end($file);
    $allow_suffix = array('jpg','png','gif');
    if (!in_array($ext, $allow_suffix)) {
        $msg = "禁止上传该后缀文件!";
    }else{
        $file_name = reset($file) . '.' . $file[count($file) - 1];
        $temp_file = $_FILES['upload_file']['tmp_name'];
        $img_path = UPLOAD_PATH . '/' .$file_name;
        if (move_uploaded_file($temp_file, $img_path)) {
            $msg = "文件上传成功!";
            $is_upload = true;
        } else {
            $msg = "文件上传失败!";
        }
    }
}

}else{
msg = "请选择要上传的文件!"; } 可以发现file_name经过reset(file) . '.' .file[count($file) - 1];处理。

如果上传的是数组的话,会跳过file = explode('.', strtolower(file));。并且后缀有白名单过滤
ext = end(file);
allow_suffix = array('jpg','png','gif'); 而最终的文件名后缀取的是file[count(file) - 1],因此我们可以让file为数组。file[0]为smi1e.php/,也就是reset(file),然后再令file[2]为白名单中的jpg。此时end(file)等于jpg,file[count(file) - 1]为空。而 file_name = reset(file) . '.' . file[count(file) - 1];,也就是smi1e.php/.,最终move_uploaded_file会忽略掉/.,最终上传smi1e.php。

中间件解析漏洞

解析漏洞是服务器端中间件的问题
详情可参考
https://www.smi1e.top/%E6%96%87%E4%BB%B6%E8%A7%A3%E6%9E%90%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容