upload-labs 一共20关,是一个使用php语言编写的,专门收集渗透测试和CTF中遇到的各种上传漏洞的靶场...
项目地址:https://github.com/c0ny1/upload-labs
最近在练习 upload-labs 里面的关卡,耐心的做完题目后受益良多...
Pass-04 这一关主要考察了 .htaccess
的利用
由于黑名单没有过滤掉 .htaccess
,所以我们能够上传.htaccess
文件导致其后上传的其他文件能够解析成 php,从而拿到shell
但是我在 windows 上直接创建 .htaccess
时会弹窗报错,告知必须输入文件名
因为我们输入的是 .htaccess
,猜测 windows 把它当做了是一个文件的后缀名,所以提示我们输入文件前面的名字
那么为什么不能这样进行创建呢?
具体可见:The Old New Thing: https://devblogs.microsoft.com/oldnewthing/?p=22763
当时就直接改成 .htaccess.txt
了,然后通过传统的办法,利用 burpsuite 抓包改包。重新改为 .htaccess
但是后知后觉的了解了一下 windows 的特性,发现有如下解决办法
方法一:
弹窗提示的原因是因为没有文件名,那么我们创建 .htaccess.
windows又会自动的忽略掉后面的 .
而成功创建
方法二:
命令行窗口 + echo命令
开启命令行窗口,直接在命令行窗口输入 echo [你要输入的文字] > .htaccess
不仅能成功创建,还能直接将内容输入进文件内
主要探究创建文件时的windows特性...
网上找了一圈资料没发现有具体的描述,看了upload-labs的一些其他writeup,发现里面都是一句,由windows特性可知...
我也不知道核心原理是什么,我就自行探索一下,有错误的地方大佬们请批评指正
系统:windows10旗舰版
创建文件 1.php
——》 设置为参照物
创建文件 1.php.
——》进行一次弹窗 ——》确定更改后变为 1.php
且无论后面加多少个.
——》均进行一次弹窗 ——》确定更改后变为 1.php
创建文件 1.php[空格]
——》直接变为 1.php
且无论后面加多少个空格 ——》均直接变为 1.php
创建文件 1.php[空格].
——》进行一次弹窗 ——》确定更改后变为 1.php
创建文件 1.php.[空格]
——》进行一次弹窗 ——》确定更改后变为 1.php
创建文件 1.php.[空格]
——》进行一次弹窗 ——》确定更改后变为 1.php
创建文件 1.php.[空格].
——》进行一次弹窗 ——》确定更改后变为 1.php
然后尝试在文件后面不断循环几次 .
和空格
最后就不断弹窗,删除不了文件也没法重命名,打不开任何文件,只要点击鼠标就会弹窗(仿佛被植入xss...
最后通过重启电脑解决
问:为什么要研究windows创建文件的特性?
答:通过windows特性,我们就是为了构造出一些特殊的文件名,既能绕过waf,又使php文件能正常解析
先贴一个针对5,6,7,9关过滤相对完全的代码
<?php $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 = $_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 . '文件夹不存在,请手工创建!'; } } ?>
针对上述相对过滤完全的代码,在没有中间件漏洞的情况下,对于我这种菜鸡来说还是难以突破的(大佬们可以指点一下...
先说一下 Pass-05
它的源码中缺少 $file_ext = strtolower($file_ext); //转换为小写
这一个过滤
所以常规的后缀名大小写混写就可以绕过黑名单的限制
.php
——》 .phP
再说一下 Pass-06
它的源码中缺少 $file_ext = trim($file_ext); //首尾去空
这个过滤点
所以我们构造 ma.php[空格]
,上传后经过滤如下
然后成功创建文件
再看一下 Pass-07
它的源码中缺少 $file_name = deldot($file_name);//删除文件名末尾的点
这个过滤点
所以我们构造 ma.php.
,上传后经过滤如下
最后通过windows的特性变为 ma.php
可能是因为做了6,7关(特别是第7关),突然好像有所顿悟,又回头看了看第5关...
于是有了下面的错误思路
回去再次读了一遍第5关的源码后发现只对 .
做出了一次过滤
那么我们可以传入 ma.php.[空格].
于是在幻想中,过滤情形应该是这样的
结果...
所以说实际情况是
因为strrchr()函数的原因,所以中间件漏洞可以在此成功利用
PHP strrchr() 函数
部分中间件漏洞总结
第 9 关就不一样
第 9 关拼接的路径为 $img_path = UPLOAD_PATH.'/'.$file_name;
所以采用上面的办法可以很顺利的完成绕过
补:deldot函数从后向前检测,当检测到末尾的第一个点时会继续它的检测,但是遇到空格会停下来
对比一下 Pass-05 和 Pass-09 会发现,Pass-09之所以能够成功执行的原因是因为他所拼接的文件路径是最开始读到的文件名
不难看出问题的关键还是拼接路径的方式,如果是拼接最开始读取到的文件名,显然是极其不安全的
对这个的思考来源于Pass-16 和一篇先知的文章:upload-labs之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的图片文件!"; } }
先知这篇文章中提到:
在这里有一个问题,如果作者是想考察绕过二次渲染的话,在move_uploaded_file($tmpname,$target_path)返回true的时候,就已经成功将图片马上传到服务器了,所以下面的二次渲染并不会影响到图片马的上传.如果是想考察文件后缀和content-type的话,那么二次渲染的代码就很多余.(到底考点在哪里,只有作者清楚.哈哈)
先知的文章将过程写的很详细,想到了不久之前的 DDCTF 中也有一道二次渲染的考题
DDCTF中的 Upload-IMG 这题,设计到了GD库的二次渲染,通过一个小脚本即可绕过:绕过GD库渲染的WEBSHELL生成器
官方的writeup如下
经验就是
1、图片找的稍微大一点 成功率更高
2、shell语句越短成功率越高
3、一张图片不行就换一张 不要死磕
4、可以把gd处理的图片再用工具跑一遍再传
Pass-14 中涉及到了 getimagesize
getimagesize() 函数将测定任何 GIF,JPG,PNG,SWF,SWC,PSD,TIFF,BMP,IFF,JP2,JPX,JB2,JPC,XBM 或 WBMP 图像文件的大小并返回图像的尺寸以及文件类型及图片高度与宽度。
前端时间P牛更新了他的博客,imagemagick邂逅getimagesize的那点事儿: https://www.leavesongs.com/PENETRATION/when-imagemagick-meet-getimagesize.html
有兴趣的大佬们可以深究一下...