Upload-labs 项目地址:https://github.com/c0ny1/upload-labs
上传漏洞类型
靶机包含漏洞类型分类
文件上传漏洞测试流程
对文件上传的地方按照要求上传文件,查看返回结果(路径,提示等)
尝试上传不同类型的“恶意文件”,比如xx.php文件,分析结果
查看html源码,看是否通过js在前端做了上传限制,可以绕过
尝试使用不同方式进行绕过:黑名单绕过/MIME类型绕过/目录0x00截断绕过等
猜测或者结合其他漏洞(比如敏感信息泄露等)得到木马路径,连接测试
参考文章:
upload-labs通关记录_mb5ff2f1c4b5e55的技术博客_51CTO博客
Pass-01 前端校验,抓包改后缀即可。提示方式是使用alert弹窗
原理:js脚本在前端对文件类型进行了校验。但是总所周知外来数据都不可信,但是服务器信了,前端的数据我们想改就改。
可以看到提交时会返回检查函数
检查函数
这里修改前端代码让他上传无效,应该是缓存问题。无奈直接抓包了。
Pass-02 这里不是前端校验了,后端进行了校验,我们就思考后端使用了什么方法校验。
最简单的肯定是http的type类型校验,上传一个正常的图片可以看到其正常Content-Type:
于是我们上传一句话,修改type为这个即可。
Pass-03 这里type不行了,所以我们考虑其他校验方法。猜测是黑名单校验,那么我们考虑没在黑名单但是可以执行php的后缀。可以被执行的后缀有这些:
于是我们上传一个phtml试试,发现成功。
Pass-04 这里查看源代码发现几乎所有后缀都算在黑名单了:
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 . '文件夹不存在,请手工创建!' ; } }
同时后缀做了去除空格、去除dot、大小写处理,那么我们还有什么办法呢?有个很有用的东西叫.htaccess
,而且它没有在黑名单内,于是我们可以上传一个这个文件,内容:
1 SetHandler application/x-httpd-php
这行代表所有格式文件都可以用来当作php执行。然后我们再随便上传一个改了后缀的一句话,这张图片就可以执行。
Pass-05 这里其他的方法都杜绝了,但是大小写处理的语句被删了,所以改下大小写就可以绕过黑名单。
Pass-06 这里我们发现trim函数被去掉了,所以上传时数据包内的文件名以空格结尾就不会被修剪,所以在文件名后加一个空格,就可以绕过黑名单的检查!
Pass-07 这里没有对结尾的点做处理,那么文件提交到服务器后,windows会自动删除文件名结尾的dot,于是我们加一个点到文件名结尾,这样的话,最后进行黑名单检查的时候其实就只有一个点了,自然就可以绕过了!
Pass-08 这里缺少对::DATA
的处理!
利用的是Windows下NTFS文件系统的一个特性,即NTFS文件系统的存储数据流的一个属性DATA。当我们访问 a.asp::DATA 时,就是请求 a.asp 本身的数据,如果a.asp 还包含了其他的数据流,比如 a.asp:lake2.asp,请求 a.asp:lake2.asp::$DATA,则是请求a.asp中的流数据lake2.asp的流数据内容。
于是我们上传时在结尾时加上::DATA
,这样就可以绕过黑名单检查。
可以看到成功上传
Pass-09 这一关,保护全开,但是我们仔细审计代码:
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" ,".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 = 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.'/' .$file_name ; if (move_uploaded_file ($temp_file , $img_path )) { $is_upload = true ; } else { $msg = '上传出错!' ; } } else { $msg = '此文件类型不允许上传!' ; } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!' ; } }
之前我们知道windows会自动删除文件结尾的.和空格。那么我们这样实验一下:
写很多个空格和点,确认修改。然后刷新一下:
结尾所有的点和空格都消失了!那么我们再回头看看源码,服务端先去除空格,再去除最后的点,然后取最后点开始的部分。那么我们构造一个yjh.php. .
,让他去除最后的点后还有空格不就行了吗?这样deldot函数只能去除最后一个点,相当于后缀是一个空格。然后继续修剪掉空格,就只剩点了,就饶过了。php后跟着的点不能省,不然第二次修剪后还是.php后缀,我们要让他strrchr断在php之后。
上传成功
Pass-10 源码:
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)) { $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 = trim ($_FILES ['upload_file' ]['name' ]); $file_name = str_ireplace ($deny_ext ,"" , $file_name ); $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 = UPLOAD_PATH . '文件夹不存在,请手工创建!' ; } }
这里可以看到检测方式从黑名单改为了替换!但是它只替换了一次并且再无检测,我们直接双写!
成功绕过
Pass-11 检测方式变为了白名单,但是我们看到GET传了一个路径进去:
同时,图片路径使用直接拼接的方式:
于是我们可以改变文件复制的路径,也就可以修改文件复制后的名称了!由于是直接拼接,我们可以使用%00截断:
0x00是字符串的结束标识符,利用手动添加字符串标识符的方式来将后面的内容进行截断,而后面的内容又可以帮助我们绕过检测。于是路径拼接后到%00就断了,最后复制的路径名是:../upload/1.php
利用条件:
1、php版本小于5.3.4。php<5.3.4 版本中,存储文件时处理文件名的函数认为0x00是终止符
2、php的magic_quotes_gpc为OFF状态
Pass-12 这里和上一题一样,但是提交变为了POST。POST不会对URL进行解码,因此我们需要直接写入一个00字节
使用解码或者直接在hex里插入一个00字节,即可绕过!
Pass-13 这里源码使用了一个函数来检测文件’真正’的类型。方法是检测前两个字节(magic number)
于是我们可以使用图片马(前面有图片的文件头,但是后面跟着php的代码)我们构造一个这样的文件上传上去
上传成功,同时结合文件包含验证
1 http://127.0.0.1/include.php?file=./upload/rand.gif
Pass-14 同13,但是使用另一个函数进行判断
因此上传图片马同样可以绕过此函数。
Pass-15 这里也是使用图片马绕过,但是使用exif_imagetype()函数检测文件类型
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的图片文件!" ; } }
这里同样使用图片码+filetype绕过,但是上传后并不生效。将上传成功的图片与原图片马进行对比,可以看到区别很大:
回头再看源码,发现服务器对图片进行了二次渲染,导致图片中包含语句的部分丢失了(可以检查文件的哈希)。
于是我们将渲染后
的gif再拿出来,在不被渲染的部分覆盖成我们的一句话:
上传后,图片可以正常访问,可以成功连接
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 = '上传出错!' ; } }
可以看到,后端校验逻辑是:先获取文件后缀名,然后将文件移动到upload目录下,然后判断类型是否为白名单。如果在白名单内,则将文件名格式化。否则删除文件。
通过这个过程我们思考绕过方法。首先是文件后缀绕过,这里使用了字符串截断和白名单方式,因此不可行。其次通过文件包含图片马的方式,可以预料的是如果存在文件包含那么只要修改后缀就可以利用。
如果只能文件上传呢?我们可以推测,在php执行完文件移动操作后,且删除操作之前是有一小段时间的。那么我们可以利用这段极短的时间对webshell进行访问。因此可以构想:无限重复发包webshell,就有概率在webshell还没被删除之前成功访问。
条件竞争成功,成功返回页面
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" ); } ...... ...... ...... };
可以发现,检测相关函数被封装到了一个类里,其中对临时文件进行检查、移动、重命名的操作在upload函数内。函数会对上传的文件是否已上传、目标文件夹设置、扩展名(白名单)、尺寸、文件是否存在进行检查,全部通过后才对文件进行移动、重命名。因此我们选择直接上传图片马。
Pass-19 这一题采用的上传方式是用户自定义保存名称。我们试试图片马能否保存为php:
发现禁止上传。为了排除保存名称的前端校验,我们直接测试接口:
发现服务端存在校验。上传webshell并保存为正常后缀,发现能正常上传:
那么可以推测图片马的上传是没什么问题。那么我还是想做到传php文件上去。想想有什么方法。我们弄一个随机的后缀试试:
我擦,传上去了,说明服务端使用了黑名单而不是白名单。那么我们传些别的有用的后缀试试。发现均被包入黑名单。那么我们结合前面使用到的方法,结尾添加空白,发现成功绕过黑名单:
试试点:
同样成功(%00截断同样适用)。我们查看下源代码:
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 . '文件夹不存在,请手工创建!' ; } }
发现服务端使用了pathinfo
函数获取文件后缀并进行校验。
pathinfo($file_name,PATHINFO_EXTENSION)
获取获取上传文件后缀名
pathinfo(string $path [,int $options = PATHINFO_DIRNAME | PATHINFO_BASENAME | PATHINFO_EXTENSION | PATHINFO_FILENAME])
返回一个关联数组包含有path的信息。返回关联数组还是字符串取决于options
PATHINFO_DIRNAME:文件所在目录
PATHINFO_BASENAME:文件+后缀名
PATHINFO_EXTENSION:后缀名
PATHINFO_FILENAME:文件名
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 = "请选择要上传的文件!" ; }
可以看到检查过程:
第一步检查MIME类型白名单
第二步,检查文件名,若不是数组则打散为数组(同时文件名全部转为小写。explode类似python中的split)
第三步,将使用.
分割打散后的数组的最后一位取出,并使用end
函数进行白名单校验。
以上全部通过的话,则移动文件为upload目录下的 file数组首位和数组长度-1
为索引(count函数)的元素拼接而成的文件名。(reset重置数组内部指针到首位并返回元素)
可以发现,关键位置就在数组和后缀的白名单校验。因此我们可以想象,如果传入的本来就是一个数组,那么我们可以构造一个这样的数组:数组通过end函数取出的最后一位在白名单内,且首位为***.php
,同时要做到数组长度-1为索引的元素并不是end()取出的元素。例如:
1 2 3 4 Array { [0 ]=> "info.php" [2 ]=> "png" }
这样,end取出的png
在白名单内,同时首位元素为info.php,数组长度-1
为1,即空。于是通过白名单的是png,但是构造出的文件名却是”info.php” . null => “info.php”。于是我们构造如下数据包:
成功上传