Upload-labs

项目地址:https://github.com/c0ny1/upload-labs

上传漏洞类型

image-20221001005305992

靶机包含漏洞类型分类

image-20221001005001448

文件上传漏洞测试流程

  1. 对文件上传的地方按照要求上传文件,查看返回结果(路径,提示等)
  2. 尝试上传不同类型的“恶意文件”,比如xx.php文件,分析结果
  3. 查看html源码,看是否通过js在前端做了上传限制,可以绕过
  4. 尝试使用不同方式进行绕过:黑名单绕过/MIME类型绕过/目录0x00截断绕过等
  5. 猜测或者结合其他漏洞(比如敏感信息泄露等)得到木马路径,连接测试

参考文章:

upload-labs通关记录_mb5ff2f1c4b5e55的技术博客_51CTO博客

Pass-01

前端校验,抓包改后缀即可。提示方式是使用alert弹窗

原理:js脚本在前端对文件类型进行了校验。但是总所周知外来数据都不可信,但是服务器信了,前端的数据我们想改就改。

可以看到提交时会返回检查函数

image-20220930184329261

检查函数

image-20220930184351536

这里修改前端代码让他上传无效,应该是缓存问题。无奈直接抓包了。

Pass-02

这里不是前端校验了,后端进行了校验,我们就思考后端使用了什么方法校验。

最简单的肯定是http的type类型校验,上传一个正常的图片可以看到其正常Content-Type:

image-20220930184658367

于是我们上传一句话,修改type为这个即可。

Pass-03

这里type不行了,所以我们考虑其他校验方法。猜测是黑名单校验,那么我们考虑没在黑名单但是可以执行php的后缀。可以被执行的后缀有这些:

image-20220930194146162

于是我们上传一个phtml试试,发现成功。

Pass-04

这里查看源代码发现几乎所有后缀都算在黑名单了:

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
$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",".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.'/'.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 . '文件夹不存在,请手工创建!';
}
}

同时后缀做了去除空格、去除dot、大小写处理,那么我们还有什么办法呢?有个很有用的东西叫.htaccess,而且它没有在黑名单内,于是我们可以上传一个这个文件,内容:

1
SetHandler application/x-httpd-php

这行代表所有格式文件都可以用来当作php执行。然后我们再随便上传一个改了后缀的一句话,这张图片就可以执行。

Pass-05

这里其他的方法都杜绝了,但是大小写处理的语句被删了,所以改下大小写就可以绕过黑名单。

Pass-06

这里我们发现trim函数被去掉了,所以上传时数据包内的文件名以空格结尾就不会被修剪,所以在文件名后加一个空格,就可以绕过黑名单的检查!

Pass-07

这里没有对结尾的点做处理,那么文件提交到服务器后,windows会自动删除文件名结尾的dot,于是我们加一个点到文件名结尾,这样的话,最后进行黑名单检查的时候其实就只有一个点了,自然就可以绕过了!

Pass-08

这里缺少对::DATA的处理!

利用的是Windows下NTFS文件系统的一个特性,即NTFS文件系统的存储数据流的一个属性DATA。当我们访问 a.asp::DATA 时,就是请求 a.asp 本身的数据,如果a.asp 还包含了其他的数据流,比如 a.asp:lake2.asp,请求 a.asp:lake2.asp::$DATA,则是请求a.asp中的流数据lake2.asp的流数据内容。

于是我们上传时在结尾时加上::DATA,这样就可以绕过黑名单检查。

image-20220930195555792

可以看到成功上传

image-20220930195558842

Pass-09

这一关,保护全开,但是我们仔细审计代码:

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
$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 = 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 . '文件夹不存在,请手工创建!';
}
}

之前我们知道windows会自动删除文件结尾的.和空格。那么我们这样实验一下:

image-20221001010342190

写很多个空格和点,确认修改。然后刷新一下:

image-20221001010424488

结尾所有的点和空格都消失了!那么我们再回头看看源码,服务端先去除空格,再去除最后的点,然后取最后点开始的部分。那么我们构造一个yjh.php. .,让他去除最后的点后还有空格不就行了吗?这样deldot函数只能去除最后一个点,相当于后缀是一个空格。然后继续修剪掉空格,就只剩点了,就饶过了。php后跟着的点不能省,不然第二次修剪后还是.php后缀,我们要让他strrchr断在php之后。

image-20221001010951782

上传成功

Pass-10

源码:

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

这里可以看到检测方式从黑名单改为了替换!但是它只替换了一次并且再无检测,我们直接双写!

image-20221001011550756

成功绕过

Pass-11

检测方式变为了白名单,但是我们看到GET传了一个路径进去:

image-20221001101126086

同时,图片路径使用直接拼接的方式:

image-20221001101223127

于是我们可以改变文件复制的路径,也就可以修改文件复制后的名称了!由于是直接拼接,我们可以使用%00截断:

image-20221001101736822

0x00是字符串的结束标识符,利用手动添加字符串标识符的方式来将后面的内容进行截断,而后面的内容又可以帮助我们绕过检测。于是路径拼接后到%00就断了,最后复制的路径名是:../upload/1.php

利用条件:

1、php版本小于5.3.4。php<5.3.4 版本中,存储文件时处理文件名的函数认为0x00是终止符

2、php的magic_quotes_gpc为OFF状态

image-20221001102224160

image-20221001102132621

Pass-12

这里和上一题一样,但是提交变为了POST。POST不会对URL进行解码,因此我们需要直接写入一个00字节

image-20221001103037135

使用解码或者直接在hex里插入一个00字节,即可绕过!

Pass-13

这里源码使用了一个函数来检测文件’真正’的类型。方法是检测前两个字节(magic number)

image-20221001104241451

于是我们可以使用图片马(前面有图片的文件头,但是后面跟着php的代码)我们构造一个这样的文件上传上去

image-20221001104308159

上传成功,同时结合文件包含验证

1
http://127.0.0.1/include.php?file=./upload/rand.gif

Pass-14

同13,但是使用另一个函数进行判断

image-20221001104836055

image-20221001104851799

因此上传图片马同样可以绕过此函数。

Pass-15

这里也是使用图片马绕过,但是使用exif_imagetype()函数检测文件类型

image-20221001105113832

Pass-16

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
$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的图片文件!";
}
}

这里同样使用图片码+filetype绕过,但是上传后并不生效。将上传成功的图片与原图片马进行对比,可以看到区别很大:

image-20221001110317128

回头再看源码,发现服务器对图片进行了二次渲染,导致图片中包含语句的部分丢失了(可以检查文件的哈希)。

于是我们将渲染后的gif再拿出来,在不被渲染的部分覆盖成我们的一句话:

image-20221002140042722

上传后,图片可以正常访问,可以成功连接

image-20221002140109609

Pass-17

代码审计:

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

可以看到,后端校验逻辑是:先获取文件后缀名,然后将文件移动到upload目录下,然后判断类型是否为白名单。如果在白名单内,则将文件名格式化。否则删除文件。

通过这个过程我们思考绕过方法。首先是文件后缀绕过,这里使用了字符串截断和白名单方式,因此不可行。其次通过文件包含图片马的方式,可以预料的是如果存在文件包含那么只要修改后缀就可以利用。

如果只能文件上传呢?我们可以推测,在php执行完文件移动操作后,且删除操作之前是有一小段时间的。那么我们可以利用这段极短的时间对webshell进行访问。因此可以构想:无限重复发包webshell,就有概率在webshell还没被删除之前成功访问。

image-20221002141409923

条件竞争成功,成功返回页面

image-20221002141528216

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//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) {
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 1

if( $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 file

if( $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" );

}
......
......
......
};

可以发现,检测相关函数被封装到了一个类里,其中对临时文件进行检查、移动、重命名的操作在upload函数内。函数会对上传的文件是否已上传、目标文件夹设置、扩展名(白名单)、尺寸、文件是否存在进行检查,全部通过后才对文件进行移动、重命名。因此我们选择直接上传图片马。

Pass-19

这一题采用的上传方式是用户自定义保存名称。我们试试图片马能否保存为php:

image-20221002143341339

发现禁止上传。为了排除保存名称的前端校验,我们直接测试接口:

image-20221002143539416

发现服务端存在校验。上传webshell并保存为正常后缀,发现能正常上传:

image-20221002143136177

那么可以推测图片马的上传是没什么问题。那么我还是想做到传php文件上去。想想有什么方法。我们弄一个随机的后缀试试:

image-20221002143944624

我擦,传上去了,说明服务端使用了黑名单而不是白名单。那么我们传些别的有用的后缀试试。发现均被包入黑名单。那么我们结合前面使用到的方法,结尾添加空白,发现成功绕过黑名单:

image-20221002144331891

试试点:

image-20221002144523728

同样成功(%00截断同样适用)。我们查看下源代码:

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
$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 . '文件夹不存在,请手工创建!';
}
}

发现服务端使用了pathinfo函数获取文件后缀并进行校验。

pathinfo($file_name,PATHINFO_EXTENSION)

获取获取上传文件后缀名

pathinfo(string $path [,int $options = PATHINFO_DIRNAME | PATHINFO_BASENAME | PATHINFO_EXTENSION | PATHINFO_FILENAME])

返回一个关联数组包含有path的信息。返回关联数组还是字符串取决于options

PATHINFO_DIRNAME:文件所在目录

PATHINFO_BASENAME:文件+后缀名

PATHINFO_EXTENSION:后缀名

PATHINFO_FILENAME:文件名

image-20221002144854039

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
30
31
32
33
$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类型白名单
  • 第二步,检查文件名,若不是数组则打散为数组(同时文件名全部转为小写。explode类似python中的split)
  • 第三步,将使用.分割打散后的数组的最后一位取出,并使用end函数进行白名单校验。
  • 以上全部通过的话,则移动文件为upload目录下的 file数组首位和数组长度-1为索引(count函数)的元素拼接而成的文件名。(reset重置数组内部指针到首位并返回元素)

可以发现,关键位置就在数组和后缀的白名单校验。因此我们可以想象,如果传入的本来就是一个数组,那么我们可以构造一个这样的数组:数组通过end函数取出的最后一位在白名单内,且首位为***.php,同时要做到数组长度-1为索引的元素并不是end()取出的元素。例如:

1
2
3
4
Array {
[0]=> "info.php"
[2]=> "png"
}

这样,end取出的png在白名单内,同时首位元素为info.php,数组长度-1为1,即空。于是通过白名单的是png,但是构造出的文件名却是”info.php” . null => “info.php”。于是我们构造如下数据包:

image-20221002152014028

成功上传

image-20221002152046153

⬆︎TOP