文件上传练习靶场–upload-labs通关记录以及对文件上传漏洞的总结

Pass-01

直接上传php文件,出现弹框提示上传失败

尝试抓包,但是因为弹框未抓到上传文件的包,所以猜测是前端JS代码对文件进行了检测,直接查看网页源代码,发现检测JS代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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("."));
//判断上传文件类型是否允许上传
if (allow_ext.indexOf(ext_name) == -1) {
var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
alert(errMsg);
return false;
}
}
</script>

代码大致流程是对比文件名的最后一个后缀是否是jpg,png,gif,如果不是则前端拦截文件,上传失败。

既然是前端进行,我们只要绕过前端,再利用抓包修改文件名后缀,即可成功上传PHP文件。我们先上传一个后缀名为JPG,内容为PHP代码的文件1cmd.jpg,再通过抓包修改文件名为1cmd.php,过程如下图所示

Pass-02

直接上传PHP文件,提示文件类型错误,猜测后台代码对文件类型进行了检测,抓包修改文件类型为image/jpeg,如下图所示

本关检测代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.'文件夹不存在,请手工创建!';
}
}

Pass-03

上传PHP文件,提示禁止不允许上传.asp,.aspx,.php,.jsp后缀文件 ,尝试修改文件名为.jpg.php,修改文件类型,大写PHP,都失败,猜测后台代码将文件名的最后一个”.”后作为检测目标。后台过滤代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 . '文件夹不存在,请手工创建!';
}
}

过滤了后缀名为.asp,.aspx,.php,.jsp的文件,但是没有过滤phtml文件

上传成功http://127.0.0.1/upload-labs/upload/201903031949124726.phtml

另外修改后缀名为php3也可以

Pass-04

上传php,phtml,php3等文件都失败,过滤代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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");
$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文件,我们上传一个.htaccess文件,内容为:

1
SetHandler application/x-httpd-php

上传之后,该路径下所有文件都会被解析成PHP格式文件,我们再上传包含PHP代码的图片文件

访问http://127.0.0.1/upload-labs/upload/4cmd.jpg

Pass-05

跟上一关区别的是黑名单又增加了.htaccess文件,过滤代码如下:

1
2
3
4
5
6
7
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 = 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); //首尾去空

但是仔细观察发现这关并没有将上传文件的后缀名通过strtolower进行大小写转化的处理,所以很简单,上传一个.PHP文件即可

Pass-06

1
2
3
4
5
6
7
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

黑名单一样,并对文件名进行小写转化处理,但是未对文件名通过trim函数进行去空处理,所以对后缀名进行加空,即可上传成功

Pass-07

1
2
3
4
5
6
7
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 = 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); //首尾去空

黑名单相同,对文件名进行去空和小写转换处理,但是没有通过自定义的deldot函数进行末尾去点处理,所以上传后缀名为.php.文件,windows特性上传后会自动将后缀名的点去掉

Pass-08

1
2
3
4
5
6
7
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 = 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); //首尾去空

这关没有通过str_ireplace函数去除字符串::$DATA,在文件名后缀加上::$DATA即可绕过

Pass-9

1
2
3
4
5
6
7
8
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 = 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); //首尾去空

相对于前面几关而言,这关过滤的较为完善,可以看到,过滤的流程为:(1)文件名去空(2)文件名去点(3)截取最后一个点后的字符串(4)将截取的文件后缀转换为小写(4)将截取的文件名后缀去除字符串::$DATA(5)将截取的文件名后缀去空

我们可以看一下deldot函数的具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
function deldot($s){
for($i = strlen($s)-1;$i>0;$i--){
$c = substr($s,$i,1);
if($i == strlen($s)-1 and $c != '.'){
return $s;
}

if($c != '.'){
return substr($s,0,$i+1);
}
}
}

可以发现,检测流程是从文件名的最后一位开始检测,是点就去掉末位,继续向前检测,只要检测到文件名最后一位不是点,就返回过滤后的文件名,而且去点只有一次

针对上述过滤流程,我们可以构造后缀名为.php. .(点+空格+点),经过去点过滤后的文件名为.php. (点+空格),之后截取文件名后缀自然就绕过检测,上传的文件名最后后缀为.php.(点)

Pass-10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 . '文件夹不存在,请手工创建!';
}
}

这关是将上传文件的文件名通过str_ireplace函数去除黑名单中的文件后缀,但是这个函数的缺点是只能去除一次,所以双写就能绕过,上传文件名后缀为.pphphp

Pass-11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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类型文件!";
}
}

这关开始采用了白名单的形式,要求上传文件名后缀名必须为jpg,png,gif,但是我们可以发现上传文件的路径是通过GET方式传递的参数save_path进行拼接的,所以在save_path末尾利用%00截断绕过

Pass-12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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类型文件!";
}
}

这关拼接的参数save_path是通过POST方式传递的,同样抓包修改save_path,但是因为POST不像GET能URL解码%00,所以我们需要在二进制中修改

Pass-13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}

通过读取文件的前两个字节来判断文件类型,本关的目的是上传图片马,所以利用copy命令将图片文件和php文件进行合并成图片马文件,命令如下:

1
copy 1.jpg/b + 13cmd.php/a 13cmd.jpg

最后通过带有文件包含漏洞的文件检测图片马

Pass-14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}

利用getimagesize函数获取文件类型是否是图片文件,跟上一关一样,可以用copy命令生成图片马,也可以在文件内容的开头加入GIF89A伪装成GIF文件

Pass-15

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}
}

同样利用copy命令生成图片马或者在文件内容开头加入GIF89A即可上传图片马

Pass-16

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 = "上传出错!";

这关规定了文件的后缀名必须是jpg,png或gif,文件类型Content-Type必须为image/jpeg,image/png或image/gif,而且上传后还经过imagecreatefromgif函数进行图片二次渲染的过程,我们可以先试着上传一个利用copy命令生成的图片马

可以看到成功上传,接下来访问上传的图片马

可以看出上传的图片马末尾的PHP代码经过二次渲染后发生了变化

二次渲染后的图片是会有部分内容不会发生变化的,我们可以试着上传一张完整的GIF图片,对比上传后的图片与原来的图片

我们可以发现开头部分内容前后是没有变化的,那么我们就在开头部分直接添加PHP代码再上传

成功上传,再利用文件包含漏洞访问一下上传的图片马

这个方法只适合gif图片,如果是png和jpg方法较为麻烦,具体可以参考https://xz.aliyun.com/t/2657

Pass-17

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 = '上传出错!';
}
}

这关先经过move_uploaded_file函数进行文件上传,再利用白名单过滤文件,如果不是图片文件再通过unlink函数将文件删除,我们可以利用条件竞争的原理,利用多线程不断上传php文件,再后台还未来得及通过unlink函数删除php文件时,访问到webshell

发包的同时在浏览器不断访问17cmd.php文件

Pass-18

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;
}
}

这关同样使用了白名单的形式规定了合法的后缀名,上传后再通过rename函数重命名。我们可以观察这关的白名单中存在压缩包的后缀名

1
2
3
var $cls_arr_ext_accepted = array(
".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
".html", ".xml", ".tiff", ".jpeg", ".png" );

那么跟上一关一样,我们可以利用条件竞争,通过多线程发送上传后缀名为.php.7z的文件的包,当服务器还未来得及将文件改名时访问上传的webshell

可以看到有的响应包的提示是文件还来不及被重命名

在浏览器中访问18cmd.php.7z

成功访问webshell

Pass-19

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 . '文件夹不存在,请手工创建!';
}
}

这关以一个POST方式传递的参数save_name作为上传文件保存的文件名,同时通过pathinfo函数对文件名的后缀名进行黑名单检测,但是我们可以发现,并没有对该参数进行一系列过滤处理(去点,去空,去::$DATA字符串,大小写转化)

我们先测试一下pathinfo函数:

1
2
3
4
5
echo pathinfo("cmd.php",PATHINFO_EXTENSION); #php
echo pathinfo("cmd.php.",PATHINFO_EXTENSION); #
echo pathinfo('cmd.php::$DATA',PATHINFO_EXTENSION); #::$DATA
echo pathinfo("cmd.php ",PATHINFO_EXTENSION);echo "<br>"; #php
echo pathinfo("cmd.PHP",PATHINFO_EXTENSION);echo "<br>"; #PHP

通过测试说明,一系列之前关卡的绕过方法都是可以的

Pass-20

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if(!empty($_FILES['upload_file'])){
//mime check
$allow_type = array('image/jpeg','image/png','image/gif');
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'];
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 = "文件上传失败!";
}
}
}
}

这关首先检查了上传的文件类型,然后将POST方式传递的参数save_name(如果为空,则上传的文件名)作为文件名变量$file,对$file进行了是否是数组的判断,如果不是数组则以“.”为分界符打散成数组,并取出数组最后一个元素(通过end函数)作为文件名后缀进行白名单的检测,通过检测的话就取出数组的第一个元素(通过reset函数)与$file[count($file) - 1]拼接成最终的文件名上传

如果变量$file作为字符串,则我们只能上传图片马,但如果作为数组,则不需要经过explode函数的处理,那么我们就考虑对$file数组赋值如下:

1
2
3
4
$file = array();
$file[1] = "20cmd";
$file[2] = "php";
$file[3] = "jpg";

那么被检测的后缀名变量$ext和最后上传的文件名变量$file_name的值如下:

1
2
$ext = end($file) == "jpg"
$file_name = reset($file) . '.' . $file[count($file) - 1] == "20cmd.php"

上传的payload如下图所示

还可以考虑通过利用%00截断函数move_uploaded_file,对$file数组赋值如下:

1
2
3
$file = array();
$file[0] = "20cmd.php "; //将最后一个空格字符" "在burp的提交包中的十六进制中替换成0x00
$file[1] = "jpg";

上传的payload如下图所示

文件上传漏洞总结

文件上传的检查主要分为两大部分:客户端检查和服务器端检查

一.客户端检查

客户端主要是通过前端的JS代码进行检查,如果只是单纯的前端检查,我们只需要按照前端的检查标准发送请求包,再通过抓包修改请求包的内容,如第一关,抓包修改一下文件名后缀再提交即可成功上传webshell

二.服务器端检查

服务器端则是通过后台脚本代码(本靶场为PHP)进行检查,检查主要分为三部分:检查Content-type,检查后缀名,检查文件内容

1.检查Content-type

抓包修改Content-type字段为合法内容即可

2.检查后缀名

检查后缀名分为黑名单检测和白名单检测

2.1黑名单检测

列举出一系列禁止上传的文件后缀名进行过滤,常用的绕过方法有以下几种:

(1)上传特殊可解析后缀:如phtmlphp3php5pht

(2)上传.htaccess文件:内容为SetHandler application/x-httpd-php ,上传的所有文件都会被当做php文件进行解析,前提是需要服务器相关配置开启

(3)大小写绕过:在Linux没有特殊配置的情况下,这种情况只有win可以,因为win会忽略大小写,例如Pass-05中未使用strtolower函数进行小写转化处理,那么将后缀名改成PHP即可上传成功

(4)空格,点绕过:Win下xx.php[空格] 或xx.php.这两类文件都是不允许存在的,若这样命名,windows会默认除去空格或点 ,例如Pass-06和Pass-07未使用trim函数或者自定义的deldot函数进行去空和去点处理,就可以利用该方法进行绕过上传

(5)::$DATA绕过:NTFS文件系统包括对备用数据流的支持。这不是众所周知的功能,主要包括提供与Macintosh文件系统中的文件的兼容性。备用数据流允许文件包含多个数据流。每个文件至少有一个数据流。在Windows中,此默认数据流称为:$ DATA,例如Pass-08中,未使用str_ireplace函数去除::$DATA,那么上传后缀名为.php::$DATA即可上传

(6)双写后缀名绕过:当服务器利用函数(如Pass-10中使用str_ireplace函数)将敏感的后缀名替换为空时,双写后缀名,如.pphphp即可绕过

(7)上传.7z压缩包绕过:.7z是一种压缩包文件的格式,我们上传cmd.php.7z文件,再访问该文件时能够正常访问到php页面,这属于Apache解析漏洞,Apache解析文件时,如果后缀名不认识,则会继续想前解析,会解析到php,这就是Apache的解析漏洞

2.2白名单检测

列举出只允许上传的文件后缀名,过滤掉不属于白名单中的文件,常用的绕过方法有以下几种:

(1)MIME绕过:检查http包的Content-type字段来判断文件类型,直接修改该字段值即可

(2)%00截断:利用%00截断move_uploaded_file函数,只解析%00前的字符,%00后的字符不解析,通常运用在GET方式,因为GET方式传入能自动进行URL解码,如Pass-11

(3)0x00截断:原理同%00截断,只不过是通过POST方式传递参数,需要通过Burp在十六进制形式中修改

3.检查文件内容

通过一些检查文件内容的函数进行判断是否是图片格式的文件,可以大致分为对文件头检查,getimagesize函数检查,exif_imagetype函数检查和二次渲染,通常我们只能够上传图片马,常用的绕过方法有以下几种:
(1)利用copy命令生成图片马:命令具体为copy 1.jpg/b + cmd.php/a shell.jpg,生成图片马后上传成功,但是同时还得存在文件包含漏洞才能执行图片马

(2)利用GIF89A伪造成GIF文件:在PHP文件开头内容加入GIF89A,服务器通过getimagesize会认为这是GIF文件

(3)绕过二次渲染:上传PNG和JPG图片马方法较为复杂,但是GIF图片马只需要找到上传前后两个文件经过二次渲染未改变内容的地方,并在其中添加PHP代码即可

三.代码逻辑

这一类属于比较特别的,根据服务器端代码执行的逻辑通过条件竞争上传黑名单文件,条件竞争漏洞是一种服务器端的漏洞,由于服务器端在处理不同用户的请求时是并发进行的,因此,如果并发处理不当或相关操作逻辑顺序设计的不合理时,将会导致此类问题的发生 。

以Pass-17为例,程序先进行文件上传后再判断文件是否合法,不合法再进行删除,如果利用多线程持续发送上传PHP文件的请求包,并不断访问上传的文件,服务器会来不及将不合法文件删除,我们也能因此而成功执行PHP文件代码

最后,再附上一张别人的总结图