文章首发于先知社区:https://xz.aliyun.com/t/4029 [toc] upload-labs是一个使用php语言编写的,专门收集渗透测试和CTF中遇到的各种上传漏洞的靶场。旨在帮助大家对上传漏洞有一个全面的了解。目前一共20关,每一关都包含着不同上传方式。 项目地址:https://github.com/c0ny1/upload-labs 2019年1月13日添加了第20关。 测试环境:
PHP/5.6.27 Apache/2.4.23 windows
Pass-01-js检查 这种直接禁用JS,或者burp改包等等都可以。
Pass-02-只验证Content-type 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $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-黑名单绕过 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 $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 ); $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
后缀文件,但是可以上传其他任意后缀
1 .php .phtml .phps .php5 .pht
前提是apache的httpd.conf
中有如下配置代码
1 AddType application/x-httpd-php .php .phtml .phps .php5 .pht
或者上传.htaccess
文件 需要:1.mod_rewrite模块开启
。2.AllowOverride All
文件内容
1 2 3 <FilesMatch "shell.jpg"> SetHandler application/x-httpd-php </FilesMatch>
此时上传shell.jpg
文件即可被当作php来解析。 或者
1 AddType application /x-httpd-php .jpg
另外基本上所有的黑名单都可以用Apache解析漏洞绕过。
Pass-04-.htaccess绕过 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 $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 ); $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 . '文件夹不存在,请手工创建!' ; } }
过滤了各种罕见后缀
1 $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" );
但是没有过滤.htaccess
,用上面的方法即可。
Pass-05-大小写绕过 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 $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 ); $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会忽略文件末尾的.
和空格。 另外这里发现$_FILES['upload_file']['name']
获取的是文件名中/
后面的字符串,本来还想用move_uploaded_file
会忽略/.
的trick绕过。
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-图片马 Pass-13-unpack 上传图片马。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 function getReailFileType ($filename ) { $file = fopen($filename , "rb" ); $bin = fread($file , 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' ; } return $fileType ; } $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 = "上传出错!" ; } } }
制作图片马
1 copy smi1e.jpg /b + shell.php /a shell.jpg
Pass-14-getimagesize() 同Pass-13
Pass-15-exif_imagetype() 同Pass-13
Pass-16-二次渲染绕过 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 $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/2657 jpg
和png
很麻烦,gif
只需要找到渲染前后没有变化的位置,然后将php
代码写进去,就可以了。
Pass-17-条件竞争 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $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-条件竞争 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 $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 ; } } class MyUpload {...... ...... ...... var $cls_arr_ext_accepted = array ( ".doc" , ".xls" , ".txt" , ".pdf" , ".gif" , ".jpg" , ".zip" , ".rar" , ".7z" ,".ppt" , ".html" , ".xml" , ".tiff" , ".jpeg" , ".png" ); ...... ...... ...... 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 ( $this ->cls_file_exists == 1 ){ $ret = $this ->checkFileExists(); if ( $ret != 1 ){ return $this ->resultUpload( $ret ); } } $ret = $this ->move(); if ( $ret != 1 ){ return $this ->resultUpload( $ret ); } if ( $this ->cls_rename_file == 1 ){ $ret = $this ->renameFile(); if ( $ret != 1 ){ return $this ->resultUpload( $ret ); } } return $this ->resultUpload( "SUCCESS" ); } ...... ...... ...... }
因为move在rename之前 move操作进行了一次文件保存 然后rename进行了一次更改文件名 因此我们可以通过条件竞争来上传图片马。
Pass-19-/.
绕过 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 $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会忽略掉文件末尾的/.
。但是Pass9中的文件名是从$_FILES['upload_file']['tmp_name']
中获取的,这里是用户可控的。 因此构造 当然也可以用move_uploaded_file
函数的00截断漏洞绕过。
Pass-20-数组+/.绕过 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 $is_upload = false ;$msg = null ;if (!empty ($_FILES ['upload_file' ])){ $allow_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));
。 并且后缀有白名单过滤
1 2 $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
。
解析漏洞 解析漏洞是服务器端中间件的问题,可以根据不同的解析漏洞应用在以上题目的情况中。 可以参考我的另一篇文章:文件解析漏洞总结
后记 虽然大部分都是学过的知识,但是在复现的过程中通过Debug学到了不少之前不知道的东西。