文件上传漏洞一直是一种危害比较大的漏洞,多的我也不再解释,这一篇文章就是针对upload-labs靶场而写的。这个靶场,我们需要做到的就是成功上传webshell到目标主机上,并能够连接成功。
第一关是一个针对于前端的验证,也就是使用JavaScript进行验证。先写一个php文件,你可以写一个一句话木马,也可以写一个phpinfo,因为phpinfo看起来更加直接一点。我这里文件内容就写一个一句话木马吧。,
[]
里面可以是数字,也可以是字符串,这个将会是你使用webshell管理器连接时候的密码。
进入正题,选择我们要上传的php文件,我这里就以关卡来命名,点击上传,出现提示,这明显的是一个js的alert提示框。我们就来尝试进行绕过。
绕过前端的验证有两种方法:
先来第一种,以谷歌浏览器为示范,在“设置->隐私设置和安全性->网站控制”,将JavaScript设置为禁止,再次上传,上传成功。
接着第二种方法,首先要通过其前端的验证,前端要求我们只能上传那三种类型的文件,那么我们改名为.jpg
文件吧
上传成功,给我们返回了图片,虽然这图片不能看,但不影响,毕竟它本来就不是图片
右键图片,复制出图片地址,http://192.168.2.134/upload-labs-master/upload/1.php
进行访问果然是一片空白,但没关系,因为他本来就是一片空白,哈哈ヽ( ̄▽ ̄)ノ。这时我们就需要上webshell管理器,比较著名的有菜刀,蚁剑,冰蝎。。。这里就用蚁剑吧,这是蚁剑的首页
右键空白处添加数据,连接密码就是之前设的那个
之后连接成功就可以访问到对方电脑里的东西了
或者是右键单击数据,启用虚拟终端,我们来whoami
一下,可以看到我们现在是管理员用户,所以能干的事情就比较多。如果我们不是管理员权限,而是一个普通的web用户,那可以做的事就非常少,我们就需要“提权”,如果能获取到system权限,那就可以横着走了。这里只是提出这样一个概念,因为我也不会。
这关就算完成了,这关其实很简单,主要就是介绍了一下前端的验证,以及文件上传可以干个啥事。
这一关希望大家记住一句话 “所有前端的验证机制都是不安全的”,因为前端的东西是用户可控制的。
这里来进行一波代码审计,看一下有些什么函数
include '../config.php';
include '../head.php';
include '../menu.php';
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
//取出文件上传后临时存储的文件名
$img_path = UPLOAD_PATH . '/' . $_FILES['upload_file']['name'];
//生成一个新的文件存储路径,文件名保持文件上传前的文件名
if (move_uploaded_file($temp_file, $img_path)){
//move_uploaded_file函数把上传的文件移动到新的位置,成功则返回true,失败则返回false
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
?>
这里先是包含几个文件,然后出现了一些新的函数
file_exists
函数检查文件或目录是否存在。如果指定的文件或目录存在则返回 TRUE,否则返回 FALSE。
而UPLOAD_PATH
这个常量在config.php
文件中,有这么一段代码define("UPLOAD_PATH", "../upload");
,进行了设置。
那这个$_FILES['upload_file']['tmp_name']
又是哪儿冒出来的呢,upload_file
就是文件上传的表单的名字,如下图
这里是$_FILES
中的那些参数:
$_FILES这个变量用与上传的文件参数设置,是一个多维数组
数组的用法就是 $_FILES['key']['key2'];
$_FILES['upfile']是你表单上传的文件信息数组,upfile是文件上传字段,在上传时由服务器根据上传字段设定。
$_FILES['upfile']包含了以下内容:
$_FILES['upfile']['name'] 客户端文件的原名称。
$_FILES['upfile']['type'] 文件的 MIME 类型,需要浏览器提供该信息的支持,例如"image/gif"。
$_FILES['upfile']['size'] 已上传文件的大小,单位为字节。
$_FILES['upfile']['tmp_name'] 文件被上传后在服务端储存的临时文件名。
$_FILES['upfile']['error'] 和该文件上传相关的错误代码。
除去上面那一段代码,这里还有三段无关紧要的代码
if($msg != null){
echo "提示:".$msg;
}
?>
//如果有$msg不为空,则提示
-------------------------------------------------------------------------------
if($is_upload){
echo '.$img_path.'" width="250px" />';
}
?>
//如果上传成功,则返回存储路基
-------------------------------------------------------------------------------
if($_GET['action'] == "show_code"){
include 'show_code.php';
}
?>
//如果提交的action参数为“show_code”,则包含 show_code.php 文件
下面这段js代码定义了检查上传文件的函数
<script type="text/javascript">
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("."));
//提取上传文件的类型。
//通过lastIndexOf取到“.”的索引,再使用substring函数截取 .后缀名
if (allow_ext.indexOf(ext_name) == -1) {
//如果 allow_ext 中没有 ext_name字符串,则返回-1
var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
alert(errMsg);
return false;
}
//判断上传文件类型是否允许上传
}
</script>
这关,我们直接使用上一关的第二种方法就可以进行绕过,但是第一种方法不行,因为这一关的考点和上一关是不一样的,我们抓个包来看看。第一关是在前端验证后缀名,所以,第一关如果我们不禁用js,直接选择选择一个.php
的文件的话,即使开了burp也是不会抓到包的,但是这关不一样
我们选择一个 2.php
文件,开启burpsuite,然后点击上传,成功的抓取到数据包。我们既然直接用上一关的第二种方法可以直接上传成功,那我们不妨来对比一下两个数据包。
这个是我们这一关的数据包
这是上一关的数据包
通过对比,不难发现,两个数据包最大的不同之处就是Content-Type
字段,Content-Type字段,见名知意,内容类型,在http协议中,使用该字段来描述了具体请求中的媒体内容信息。我们来修改该字段尝试是否可上传成功,既然题目要求我们上传图片类型,那我们将其改为图片型的即可。
修改后放行数据包,上传成功
再来看一下源代码
$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.'文件夹不存在,请手工创建!';
}
}
可以看到,其实并没有什么特别之处,就是使用了$_FILES['upload_file']['type']
获取到Content-Type
字段,并进行比较,如果是那三种类型就继续向下执行,否则进行提示。
来到第三关,我们先按照上一关的做法来试一下,但是失败了
不妨点击一下右上角,查看一下提示,提示禁止上传这几个后缀名的文件,那这样看来,服务器端应该是对文件后缀名进行了限制,设置了黑名单。
那我们就要在后缀名上面做文章。目前最容易想到的有两种方法
那就来尝试第二种方法,如下是常见的可执行脚本后缀名,及其替代后缀名,但是需要注意的是,除了基础的后缀名,其他后缀名可能需要在特殊的情况下才能被解析。
|asp/aspx|asp,aspx,asa,asax,ascx,ashx,asmx,cer,aSp,aSpx,aSa,aSax,aScx,aShx,aSmx,cEr|
|php|php,php5,php4,php3,php2,pHp,pHp5,pHp4,pHp3,pHp2,html,htm,phtml,pht,Html,Htm,pHtml|
|jsp|jsp,jspa,jspx,jsw,jsv,jspf,jtml,jSp,jSpx,jSpa,jSw,jSv,jSpf,jHtml|
修改后缀名为pHtml
,上传成功
但是我尝试连接却失败了,看来确实是不能被服务器正常解析。那我们再试试其他的后缀名,尝试了很多后缀名,都是上传成功了,但却没有被正确解析。网上搜了一下,原来需要对apache配置文件做修改,在phpstudy中点击“其他选项菜单”打开Apache配置文件httpd-conf
。
找到下图中的这条数据,将其注释符去掉,表示:执行以.php
和.phtml
为后缀名的文件中的php代码,当然你也可以自己添加其他后缀名。修改之后保存并重启服务。
这时再来连接,成功连接上,做完之后,我们再来看一下源代码
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array('.asp','.aspx','.php','.jsp');
$file_name = trim($_FILES['upload_file']['name']);
//trim去除字符创两侧的的特殊字符
$file_name = deldot($file_name);//删除文件名末尾的点
//deldot是一个自己创建的函数
$file_ext = strrchr($file_name, '.');
//取出后缀名 如: .txt
$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 . '文件夹不存在,请手工创建!';
}
}
下面这个是deldot函数的函数体,这里又看到了substr
函数(如果有看过我sql注入的话,应该看到过),但是这是php中的substr函数,与mysql中的有区别,mysql中substr('abcde',1,1)
截取的是a
,而php中的substr截取的是b
。
function deldot($s){
for($i = strlen($s)-1;$i>0;$i--){
// hello. $i=5
$c = substr($s,$i,1);
// .
if($i == strlen($s)-1 and $c != '.'){
return $s;
}
if($c != '.'){
return substr($s,0,$i+1);
}
}
先按照上一关的方法,但是提示上传不成功,再来看一下提示
发现,还是黑名单方式,但是却过滤了几乎所有的可执行脚本后缀名,但是我们可以联想到Apache解析的特点。从右往左检测后缀名,比如一个文件是a.php.aaa.bbb
,Apache首先检查.bbb
后缀名,发现不认识,接着继续往左检查.aaa
,还是不认识,那就会检查.php
,发现认识,于是就进行了解析。这里为了效果明显,所以拿phpinfo()
来做示范,成功被解析。
除了这种方法以外,还有其他方法,利用.htaccess
文件,.htaccess文件
是Apache服务器下的一个配置文件,将改文件放在一个文件夹下,作用于该文件目录及其子目录,启用.htaccess
,需要修改httpd.conf
,启用AllowOverride
,并可以用AllowOverride限制特定命令的使用。
如果需要使用.htaccess以外的其他文件名,可以用AccessFileName指令来改变。例如,需要使用.config ,则可以在服务器配置文件中按以下方法配置:AccessFileName .config
。
因为关卡采用的是黑名单方式,所以,我们可以上传.htaccess
文件,我们先上传一个.htaccess
文件,文件内容如下,表示执行.png
文件中的php代码。
AddType application/x-httpd-php .png
然后再来上传一个phpinfo.png
图片,上传成功,并且在.htaccess
文件的作用下,能够被解析,若是将该配置文件删除,则不能正常解析。
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",".ini");
$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.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
源代码与前一关并无大的差距,就是黑名单里的后缀名更多了而已
以上一关的第一种方式,可以直接通过,不过第一种方式或许并非是这两关的考点。我们尝试一下第二种方式时,尝试失败,上传.htaccess
文件,直接失败。点击提示却出现这样一个不太看得懂的提示。不过我们可以试一下先读取一下这个readme.php
我们先随便上传一个文件,复制其路径,再把文件名改为readme.php
访问一下,看有什么内容,发现就是一段文字,并没有给我们更多的信息。
于是,还是去求助万能的度娘,找到了这一关的过关姿势,以及一篇很不错的文章,戳这里看
那我来简单说一下,php中有一个默认配置文件php.ini
,其中包括了很多php的配置,而自 PHP 5.3.0 起,PHP 支持基于每个目录的 INI 文件配置。
此类文件 仅被 CGI/FastCGI SAPI 处理。此功能使得 PECL 的 htscanner 扩展作废。如果你的 PHP 以模块化运行在 Apache 里,则用 .htaccess 文件有同样效果。
.user.ini
就是一个支持用户“自定义”的配置文件,作用于所在的文件目录,及其子目录。在php工作时,会自下而上的检查配置文件,直到网站的根目录。
在它支持自定义的配置模式中,有两个比较有意思的配置选项,auto_append_file
和auto_prepend_file
,这两个选项起到了文件包含的作用,类似于调用了一个require
函数。auto_prepend_file
指定在主文件前自动解析文件的名称;auto_append_file
指定在主文件后自动解析文件的名称,如果主文件以exit()
结尾的话,则不会进行自动追加。
因此我们可以通过上传.user.ini
配置文件,使其上传文件夹中的php文件能够自动包含某个文件。
我们先上传一个.user.ini
文件,内容如下,表示该目录及其子目录下所有的php文件在执行时,自动在头部包含一个5.jpg
文件。
auto_prepend_file=5.jpg
然后我们再上传一个文件内容为一句话木马的5.jpg
文件。这时,我们去访问readme.php
文件,并在参数中写入一个phpinfo();
,却失败了。于是我更换了php版本,我使用的是phpstudy环境,我将版本切换为php-5.4.45-nts后,成功的查看到了phpinfo的信息。
我之前用的是php-5.4.45,在我查看其phpinfo后找到了原因,因为使用的API不是FastCGI。我把phpstudy上的几个版本都尝试了一下,带nts的能成功,不带nts的则失败。
先使用上一关的方法,但是上传失败了。看一下提示,看到这个提示,又是黑名单模式了。
但是这个提示是有误的,没有提示说 .ini
的文件禁止上传,然而我去看了一下源码,看到 .ini文件也是被加入了黑名单的。
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",".ini");
$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 . '文件夹不存在,请手工创建!';
}
}
通过源代码我们可以发现,黑名单里虽然过滤的很全面,但是在下面的后缀名处理之中却出现了纰漏,没有将后缀名转换为小写。
$file_ext = strtolower($file_ext); //转换为小写
这样的话,我们就可以进行大小写绕过了。首先来说,直接上传后缀名为.pHP
的文件成功。
多的不说,直接看提示和源代码吧
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",".ini");
$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)) {
$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 . '文件夹不存在,请手工创建!';
}
}
还是拿来和第四关的代码对比,可以看到,在对后缀名进行处理时,又少了一条语句
$file_ext = trim($file_ext); //首尾去空
那我们就给后缀名首尾加空格吧,在前面加显然是不行的,因为这是加在文件名里面了,后缀名照样过不了检查。
在后面加空格,就顺利的通过了php的检查,而在windows系统中存储文件时,系统又自动忽略了文件后缀名末尾的空格,从而绕过。
还是一样的,先来查看提示和源码,这个提示不明白,但先不管,直接看源码
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",".ini");
$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)) {
$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 . '文件夹不存在,请手工创建!';
}
}
还是拿来对别第四关,依旧是少了一条语句
$file_name = deldot($file_name);//删除文件名末尾的点
没有删除文件名末尾的点,那我们就来加个点
然后上传成功,进行访问,,,结果,居然访问成功了。
这反而让我有点措手不及了,因为后缀名变为了.php.
,按理来说应该是会访问失败的,并且我还上传了一句话木马核实一遍,也成功了。然后我查看了一下上传文件夹,原来,Windows系统在存储文件时会自动去掉文件名末尾的点和空格。这里url末尾的点,对资源的访问没啥影响,我试了一下,多加几个点或者不加点都能访问到,但是换成其他字符不行。
还是继续查看提示
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",".ini");
$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)) {
$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 . '文件夹不存在,请手工创建!';
}
}
还是依旧和第四关进行对比,发现还是少了一句
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
那这个::$DATA
是个啥玩意儿呢?
在php
+Windows
的环境下,如果文件名+::$DATA
会把::$DATA
之后的数据当成文件流处理,不会检测后缀名,且保持::$DATA
之前的文件名
那就上传文件,然后来burpsuite中进行修改吧,在文件名末尾加上一个::$DATA
。
复制返回图片的地址进行访问,发现访问失败.
将url末尾的::$DATA
去掉后进行访问,访问成功
还是老规矩,先来查看一下提示,看到这个提示会感觉像是白名单
接着来查看源码,发现还是黑名单,只不过黑名单比较完善,而且似乎对后缀名的处理也很完善
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",".ini");
$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.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
但是查了查网上的攻略找到了方法。我们上传一个.php
文件,然后修改后缀名为.php. .
,于是上传成功了,并且我们复制图像地址进行访问也是成功了。
查看一下文件上传的存储目录,可以看到,最终存储的文件是.php
结尾的
在知道这么一个方法之后,其实是不难理解最终出现这样的结果的
$file_name = deldot($file_name);//删除文件名末尾的点
// "phpinfo.php. ."-----> "phpinfo.php. "
$file_ext = strrchr($file_name, '.');
//取出".php. "
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空
// ".php. "---->".php."
经过脚本一系列的处理之后原本.php. .
的后缀名变成了.php.
,而由于Windows的特性,又将文件末尾的点给去除了,最终就存的时候.php
的文件。同理也可以上传.htaccess. .
等文件。。。(就算没有经过脚本的处理,.php. .
在windows中也是会被存储为.php
)
看提示,会从文件名中去除这些字符,那可以猜一波双写绕过
不过还是先来试一下上传一个phpinfo.php
文件,结果上传成功了,但是复制图片地址时,图片地址中 的文件名变成了info.
,那看来确实是把php
给过滤为空了,尝试一下双写,上传成功,复制图片地址进行访问也成功。
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","ini");
$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 . '文件夹不存在,请手工创建!';
}
}
还是先查看提示
看到这个提示,首先会想到的就是%00
截断
那我们来看一下是怎么个情况,先来试试上传一个正常的.php
文件,提示上传失败
我们可以看到在get提交的参数中有一个参数看起来就是上传路径,我们在这里进行修改,加上自己想修改的文件名,然后再加个%00
,放行数据包。一开始我使用的是php5.3.29,上传失败,即使我确认了magic_quotes_gpc为关闭状态,也依旧上传失败,改为php5.2.17后上传成功
上传成功后复制图片地址进行访问,但是可以看到,这个地址比较奇怪,没有访问成功,把文件末尾多出来的东西删除掉之后,访问成功。
这道题限制条件较大,需要满足两个限制条件:
00
会被转义 原理:php的一些函数的底层是C语言,而move_uploaded_file
就是其中之一,遇到0x00会截断,0x表示16进制,URL中%00解码成16进制就是0x00
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类型文件!";
}
}
提示和前一关是一样的
抓取数据包可以看到,文件路径的参数是以post方式提交的,我们在参数后面加个点
点击hex
,打开16进制编辑器。找到点那个位置,将它修改为00
,因为不是在url中的参数,所以不能用%00
,会无法解析
来到新的关卡,出现新的提示,提示让我们上传图片马到服务器,能够配合文件包含漏洞使用。
我们先写一个有文件包含漏洞的简单页面放到网站目录下面(关卡里面自带有include文件,大家可以不用自己写,之前没发现)
if(isset($_GET['path']))
{
include $_GET['path'];
}
else
{
echo "please input the 'path' as parameter whit your file";
}
?>
先来直接尝试上传一个文件内容为,文件名为
phpinfo.php
的文件。
查看一下提示,提示会检查文件开头前两个字节,因为每种类型的图片文件开头两个字节都是特定的,用于标识文件类型的数据
gif的文件头是最简单的,可以直接在文件头部写入字符 “GIF89a”
再次上传,直接上传成功,复制下图片的地址,然后访问文件包含的页面,使用../
的方式返回到上级目录,包含到目标文件。在不知道网站目录结构的情况,可以使用../
的方式不断返回上级目录,再通过相对路径包含到上传的图片文件。
接着是jpg和png格式的图片,我们可以找图片再使用16进制编辑器打开,复制文件头并转换为ascii码形式粘贴在前面。这里讲另一种方式,使用现成的图片文件,将一句话木马合成到图片中,/b
表示以二进制方式处理,/a
表示以ascii码方式处理。
copy 111.jpg/b + 1.php/a 14.jpg
合成之后,使用editplus打开合成的文件,使用16进制查看,可以看到在文件的末尾,成功写入了一句话木马。
http://192.168.2.134/upload-labs-master/upload/2220201101204536.jpg
再进到存在文件包含漏洞的页面,包含文件,但是这个地方很容易出错,有些图片,里面存在特殊字符,使用文件包含的时候可能会出错,遇到报错的话,多找几张图片来试一试
http://192.168.2.134/include/include.php?path=../upload-labs-master/upload/2220201101204536.jpg
如果访问到时一堆乱码,那其实是成功了,否则应该是几句报错信息
使用蚁剑连接成功
甚至可以在图片的属性信息中添加一句话木马,如果你运气比较好的话,也是有可能成功的。
反正我是失败了,里面被加了点,也就懒得去试了
然后来测试一下png图片的,直接用phpinfo来进行测试,和jpg是一样一样的
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 = "上传出错!";
}
}
}
这一关上传和前一关一样的图片马即可成功
function isImage($filename){
$types = '.jpeg|.png|.gif';
if(file_exists($filename)){
$info = getimagesize($filename);
//获取图像大小及相关信息
$ext = image_type_to_extension($info[2]);
//根据指定的图像类型返回对应的后缀名
if(stripos($types,$ext)>=0){
return $ext;
}else{
return false;
}
}else{
return false;
}
}
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$res = isImage($temp_file);
if(!$res){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").$res;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}
和上一关其实是差不多的,上一关是读取文件前两个字节的数据,用于判断文件类型,这一关时候使用了php中的getimagesize函数,获取到图片的信息,再取出其文件后缀名进行对比。相当于是把上一关所写的函数封装起来了。
提示如下:
先来尝试直接上传之前的文件,页面直接变黑了。源代码中提示需要开启php_exif模块
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;
}
}
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$res = isImage($temp_file);
if(!$res){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$res;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}
这里有个php_exif开启步骤的文章,开启之后,上传上一关的文件即可。
这一关 有点奇怪,用5.2.17会上传出错,于是切换版本到5.3.29,上传内容为phpinfo的图片,上传成功,第十五关还能用的图片,到这儿就给你报错了。(唉,这个文件上传真是做得让人头皮发麻,切换个版本,原本能用的图片马又不能用了,要给你报错。
)
又换了一下图片,上传成功了,但是进行访问时并没有查看到phpinfo的信息,我们将返回给我们的图片保存下来,到editplus里面查看。
发现,原本写在图片末尾phpinfo
消失不见了,整个图片中都搜索不到了,这是因为服务器对图片进行了二次渲染。
二次渲染:就是根据用户上传的图片,新生成一个图片,将原始图片删除,将新图片添加到数据库中。比如一些网站根据用户上传的头像生成大中小不同尺寸的图像。
这一关我选择使用gif图片,因为gif更加稳定,容易成功一些,我们来打开我们的burpsuite,把第一张,带有phpinfo的图片上传,进行抓包,将两个图片都发送到Compare模块进行比较,通过比较,我们可以看到图片中有很大一片区域是没有被改动过的,我们在这片区域里面插入我们的php代码即可。
通过对比,能看到,有这么一块儿地方,全是00,这种地方修改应该会更加稳妥,就在这儿修改吧
点击数据包的hex,找到这个位置,来一个一个的修改就好了
gif图片的应该是最容易成功的,至于jpg和png的我没有去深究,看到一篇很详细的文章,大家可以去看一下,戳这里
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);
//basename() 函数返回路径中的文件名部分。
$fileext= substr(strrchr($filename,"."),1);// 获得上传文件的扩展名
//判断文件后缀与类型,合法才进行上传操作
if(($fileext == "jpg") && ($filetype=="image/jpeg")){
if(move_uploaded_file($tmpname,$target_path)){
$im = imagecreatefromjpeg($target_path);
//使用上传的图片生成新的图片
/*imagecreatefromjpeg,由文件或 URL 创建一个新图象,
成功则返回图像资源,失败返回false*/
if($im == false){
$msg = "该文件不是jpg格式的图片!";
@unlink($target_path);
//ulink:删除文件
}else{
//给新图片指定文件名
srand(time());
//根据系统时间生成一个随机数
$newfilename = strval(rand()).".jpg";
//strval — 获取变量的字符串值
$img_path = UPLOAD_PATH.'/'.$newfilename;
//生成新图片的存储路径
imagejpeg($im,$img_path);
//imagejpeg — 输出图象到浏览器或文件。
@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的图片文件!";
}
}
奇怪,我直接上传就成功了,原来是我糊涂了,人家说的是上传webshell,没有说让我上传图片马。
但是这一关提示我们进行代码审计,看上去似乎没有什么问题
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 = '上传出错!';
}
}
通过分析源代码,可以发现存在逻辑漏洞
由此也就产生了漏洞,会有一个短暂的时间将我们上传的webshell存储在目录下,且以我们上传的文件名的形式
但是这个时间相当相当短暂,以至于,你打开上传目录,点击上传文件,你连影子都看不到就已经没了,所以这个时候我们可以使用burpsuite,我们先抓包,然后发送到intruder模块。
点击clear去除所有参数,然后payload选择无,并且选择持续发包。
然后开启爆破,再打开浏览器刷新界面就行了,最终成功访问到phpinfo。
但是文件在被访问后依旧会被删除,所以可以利用php代码创建一个新的木马文件,在成功访问到页面时即会生成木马文件
fputs(fopen("info.php", "w"), ''); ?>
这里有链接
依旧提示我们需要代码审计,那就再来看一下代码
上传一个合法文件时,生成的文件路径是这样的,估计是写函数的时候失误了,将文件目录与文件名进行拼接的时候少写了一个/
。
http://192.168.2.135/upload-labs-master/upload1604394444.gif
主文件:
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);
//生成一个MyUpload类的对象
$status_code = $u->upload(UPLOAD_PATH);
//调用upload函数,传递的参数为默认存储目录
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_upload_dir = ""; // 上传目录
var $cls_filename = ""; // 上传文件名
var $cls_tmp_filename = ""; // 临时文件名
var $cls_max_filesize = 33554432; // 文件最大长度
var $cls_filesize =""; // Actual file size.
var $cls_arr_ext_accepted = array(
".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
".html", ".xml", ".tiff", ".jpeg", ".png" );//允许的后缀名类型
var $cls_file_exists = 0; // 设置为1检查文件是否已存在
var $cls_rename_file = 1; // 设置为1,将上传后的文件重命名
var $cls_file_rename_to = ''; // 重命名后的新文件名
var $cls_verbal = 0; // 设置为1返回字符串而不是错误码
function MyUpload( $file_name, $tmp_file_name, $file_size, $file_rename_to = '' ){
//构造函数
$this->cls_filename = $file_name;
$this->cls_tmp_filename = $tmp_file_name;
$this->cls_filesize = $file_size;
$this->cls_file_rename_to = $file_rename_to;//文件重命名为
}
function isUploadedFile(){
if( is_uploaded_file( $this->cls_tmp_filename ) != true ){
//is_uploaded_file — 判断文件是否是通过 HTTP POST 上传的
return "IS_UPLOADED_FILE_FAILURE";
} else {
return 1;
}
}
function setDir( $dir ){
if( !is_writable( $dir ) ){
//is_writable — 判断给定的文件(夹)名是否可写
return "DIRECTORY_FAILURE";
} else {
$this->cls_upload_dir = $dir;
return 1;
}
}
function checkExtension(){
if( !in_array( strtolower( strrchr( $this->cls_filename, "." )), $this->cls_arr_ext_accepted )){
//判断上传文件的后缀名是否是允许上传的类型
// strtolower( strrchr( $this->cls_filename, "." ))-获取后缀名并转换为小写
return "EXTENSION_FAILURE";
} else {
return 1;
}
}
function checkSize(){
if( $this->cls_filesize > $this->cls_max_filesize ){
//判断上传文件的大小是否小于最大长度
return "FILE_SIZE_FAILURE";
} else {
return 1;
}
}
function move(){
if( move_uploaded_file( $this->cls_tmp_filename, $this->cls_upload_dir . $this->cls_filename ) == false ){
//判断移动文件是否成功
return "MOVE_UPLOADED_FILE_FAILURE";
} else {
return 1;
}
}
function checkFileExists(){
if( file_exists( $this->cls_upload_dir . $this->cls_filename ) ){
//检查文件移动的目标文件名是否已存在
//file_exists — 检查文件或目录是否存在
return "FILE_EXISTS_FAILURE";
} else {
return 1;
}
}
function renameFile(){
if( $this->cls_file_rename_to == '' ){
$allchar = "abcdefghijklnmopqrstuvwxyz" ;
$this->cls_file_rename_to = "" ;
mt_srand (( double) microtime() * 1000000 );
for ( $i = 0; $i<8 ; $i++ ){
$this->cls_file_rename_to .= substr( $allchar, mt_rand (0,25), 1 ) ;
//随机截取八个字符,作为文件名
}
}
$extension = strrchr( $this->cls_filename, "." );
$this->cls_file_rename_to .= $extension;
//给文件名添加上扩展名形成完整文件名
if( !rename( $this->cls_upload_dir . $this->cls_filename, $this->cls_upload_dir . $this->cls_file_rename_to )){
//判断是否命名成功
return "RENAME_FAILURE";
} else {
return 1;
}
}
function upload( $dir ){
$ret = $this->isUploadedFile();
//调用isUploadedFile函数判断上传文件的文件名是否是用过http post方式上传的
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 ){
//如果设置了“cls_file_exists = 1 ”,那么调用checkFileExists检查目标路径是否已存在
$ret = $this->checkFileExists();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}
$ret = $this->move();
//调用move函数,移动文件
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
if( $this->cls_rename_file == 1 ){
//检查cls_rename_file是否设置为1,如果设置了,调用renameFile将文件重命名
$ret = $this->renameFile();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}
return $this->resultUpload( "SUCCESS" );
}
function resultUpload( $flag ){
switch( $flag ){
case "IS_UPLOADED_FILE_FAILURE" : if( $this->cls_verbal == 0 ) return -1; else return "The file could not be uploaded to the tmp directory of the web server.";
break;
case "DIRECTORY_FAILURE" : if( $this->cls_verbal == 0 ) return -2; else return "The file could not be uploaded, the directory is not writable.";
break;
case "EXTENSION_FAILURE" : if( $this->cls_verbal == 0 ) return -3; else return "The file could not be uploaded, this type of file is not accepted.";
break;
case "FILE_SIZE_FAILURE" : if( $this->cls_verbal == 0 ) return -4; else return "The file could not be uploaded, this file is too big.";
break;
case "FILE_EXISTS_FAILURE" : if( $this->cls_verbal == 0 ) return -5; else return "The file could not be uploaded, a file with the same name already exists.";
break;
case "MOVE_UPLOADED_FILE_FAILURE" : if( $this->cls_verbal == 0 ) return -6; else return "The file could not be uploaded, the file could not be copied to destination directory.";
break;
case "RENAME_FAILURE" : if( $this->cls_verbal == 0 ) return 2; else return "The file was uploaded but could not be renamed.";
break;
case "SUCCESS" : if( $this->cls_verbal == 0 ) return 1; else return "Upload was successful!";
break;
default : echo "OUPS!! We do not know what happen, you should fire the programmer ;)";
break;
}
}
}; // end class
?>
程序执行流程我已经写在上面的代码里面了,网上都说是条件竞争,但是讲真,这个条件竞争漏洞还不太好利用起来。
首先,这个程序先检查了后缀名,后缀名合法的才能通过,光是这一点就使得必须要搭配文件解析漏洞或者文件包含漏洞才能利用了。
那我们所能利用的就只有文件未被重命名这一点,那这一点能帮助我们干嘛呢?
如果上传后会返回我们文件路径的话,我们可以直接搭配文件包含和文件解析使用,要是不返回路径的话,难道就依靠我们去目录爆破,找到上传文件存储目录来使用原文件名进行利用吗?似乎也只有这个解释了。
方法和上一关是一样的,不过需要搭配文件包含和解析漏洞。
这一关比较特别,这一关需要选择上传的文件,同时还需要填写保存的文件名。那我们可以测试一下,这一关的校验机制到底是校验上传的文件,还是校验保存的文件名,还是二者都有。
上传.php
文件,保存为.jpg
文件,上传成功;上传.jpg
文件,保存为.php
文件,上传失败。这样看来校验的应该是保存的文件名,那么又需要看是白名单校验还是黑名单校验,还是上传.php
文件,随便输入一个保存的文件名,随便输入一个后缀名,或者是不写后缀名,保存成功。说明是黑名单验证。那黑名单验证就有太多的绕过方式了。
/.
,move_uploaded_file会忽略掉文件末尾的/.
(和windows存储特性不同,这个是函数的特性)。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 . '文件夹不存在,请手工创建!';
}
}
直接查看源码
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$is_upload = false;
$msg = null;
if(!empty($_FILES['upload_file'])){
//mime check
$allow_type = array('image/jpeg','image/png','image/gif');
//检查Content-Type
if(!in_array($_FILES['upload_file']['type'],$allow_type)){
$msg = "禁止上传该类型文件!";
}else{
//check filename
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
//如果通过post提交'save_name'参数,将上传的文件名赋值给$file,否则将'save_name'参数值赋值给$file
if (!is_array($file)) {
$file = explode('.', strtolower($file));
//$file如果不是数组,以'.'为分隔符,将字符串打散为数组
/*
phpinfo.jpg执行结果会是
Array
(
[0]=>phpinfo
[1]=>jpg
)
*/
}
$ext = end($file);
//end()输出数组中最后一个元素
$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";
}else{
$file_name = reset($file) . '.' . $file[count($file) - 1]; //$file[count($file) - 1]--->$file[1]
//reset() 函数将内部指针指向数组中的第一个元素,并输出。
//count() 函数返回数组中元素的数目。
//如果后缀名合法则生成文件名phpinfo.jpg
$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 = "请选择要上传的文件!";
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
分析源码中的主要流程
save_name
参数或者是文件名赋值给$file
,如果不是数组的话,以.
为分隔符,将文件名拆散为数组array[0]
与array[count($file)-1]
拼接起来生成文件名 上面四个流程,表面上看上去是没有什么漏洞的,程序会校验数组的最后一个元素,经过校验之后,会将array[count($file)-1]
作为后缀名拼接到array[0]
之后。如果$file
的值是一个文件名,那么程序执行是正常的。如果是一个连续的数组,执行也依旧正常,但是当出现下图中的情况时,就可被绕过。