JunAMS v1.2.1.20190403代码审计笔记

前言

CNVD-2020-24741

过程

JunAMS是以ThinkPHP为框架的开源内容管理系统,本地搭建受影响版本JunAMS v1.2.1.20190403

前台没有上传功能,进入后台。发现在系统设置->版本管理->添加表单中,发现文件上传功能

先传张正常的图片,抓个包看一下:

上传功能没问题,根据POST路径追踪对应功能代码,在common方法中的add_images函数,代码如下:

public function add_images() {
        $file = request()->file('file');
        $path = ROOT_PATH . 'public' . DS . 'edit' . DS;

        if($file){
            $info = $file->validate([])->move($path); 
            if($info){
                $name = $info->getSaveName();
                $path = $path.$name;
                # 是否需要压缩
                if (\qiniu\Qiniu::get_zip() == 1) {
                    \qiniu\Qiniu::image_png_size_add($path, $path);
                }
                # 关闭七牛云上传
                if (\qiniu\Qiniu::get_status() == 0) {
                    $url = str_replace(['\\', '//'],'/', dirname($_SERVER['SCRIPT_NAME']) .DS. 'public' .DS. 'edit' .DS. $name);
                } else {
                    # 要先释放TP5的实例,否则无法删除图片
                    unset($info);
                    $url  = \qiniu\Qiniu::put($path, $name);
                }
                # 成功上传后 获取上传信息
                if ($url) {
                    echo json_encode([
                        'code' => 0,
                        'msg'  => '上传成功',
                        'data' => [
                            'src' => $url
                        ],
                    ], JSON_UNESCAPED_UNICODE);exit;
                }
                
            }
            # 上传失败获取错误信息
            echo json_encode([
                'code' => '01',
                'msg'  => '上传失败:'.$file->getError(),
                'data' => '',
            ], JSON_UNESCAPED_UNICODE);exit;
        }
    }

$info获取文件详情,并经过move函数处理,追踪下move()

public function move($path, $savename = true, $replace = true)
    {
        // 文件上传失败,捕获错误代码
        if (!empty($this->info['error'])) {
            $this->error($this->info['error']);
            return false;
        }

        // 检测合法性
        if (!$this->isValid()) {
            $this->error = 'upload illegal files';
            return false;
        }

        // 验证上传
        if (!$this->check()) {
            return false;
        }

        $path = rtrim($path, DS) . DS;
        // 文件保存命名规则
        $saveName = $this->buildSaveName($savename);
        $filename = $path . $saveName;

        // 检测目录
        if (false === $this->checkPath(dirname($filename))) {
            return false;
        }

        // 不覆盖同名文件
        if (!$replace && is_file($filename)) {
            $this->error = ['has the same filename: {:filename}', ['filename' => $filename]];
            return false;
        }

        /* 移动文件 */
        if ($this->isTest) {
            rename($this->filename, $filename);
        } elseif (!move_uploaded_file($this->filename, $filename)) {
            $this->error = 'upload write error';
            return false;
        }

        // 返回 File 对象实例
        $file = new self($filename);
        $file->setSaveName($saveName)->setUploadInfo($this->info);

        return $file;
    }

这里我们需要着重关注验证上传部分,看下check函数如何定义

public function check($rule = [])
    {
        $rule = $rule ?: $this->validate;

        /* 检查文件大小 */
        if (isset($rule['size']) && !$this->checkSize($rule['size'])) {
            $this->error = 'filesize not match';
            return false;
        }

        /* 检查文件 Mime 类型 */
        if (isset($rule['type']) && !$this->checkMime($rule['type'])) {
            $this->error = 'mimetype to upload is not allowed';
            return false;
        }

        /* 检查文件后缀 */
        if (isset($rule['ext']) && !$this->checkExt($rule['ext'])) {
            $this->error = 'extensions to upload is not allowed';
            return false;
        }

        /* 检查图像文件 */
        if (!$this->checkImg()) {
            $this->error = 'illegal image files';
            return false;
        }

        return true;
    }

check函数中检查了文件类型、后缀图像文件,依次看下代码,首先来看checkMime()

public function checkMime($mime)
    {
        $mime = is_string($mime) ? explode(',', $mime) : $mime;

        return in_array(strtolower($this->getMime()), $mime);
    }

直接将传入的类型与获取到的类型做对比,未做过滤,继续看checkExt()

public function checkExt($ext)
    {
        if (is_string($ext)) {
            $ext = explode(',', $ext);
        }

        $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION));

        return in_array($extension, $ext);
    }

后缀类型也没有限制,看下checkImg()

public function checkImg()
    {
        $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION));

        // 如果上传的不是图片,或者是图片而且后缀确实符合图片类型则返回 true
        return !in_array($extension, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf']) || in_array($this->getImageType($this->filename), [1, 2, 3, 4, 6, 13]);
    }

    /**
     * 判断图像类型
     * @access protected
     * @param  string $image 图片名称
     * @return bool|int
     */
    protected function getImageType($image)
    {
        if (function_exists('exif_imagetype')) {
            return exif_imagetype($image);
        }

        try {
            $info = getimagesize($image);
            return $info ? $info[2] : false;
        } catch (\Exception $e) {
            return false;
        }
    }

这里限制了当上传的后缀为'gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf'时,文件内容必须为图片,换言之,当不是图片后缀时,内容没有限制

整个上传流程为:获取图片文件后缀、Mime类型、内容,当后缀为图片文件时,检测内容是否为图片,当后缀不为图片文件时,定义上传目录与文件名,上传成功,返回文件路径。并且common方法没有受后台权限验证基类Backend限制,任意用户可访问,产生前台任意文件上传漏洞。

利用

本地构造上传表单

<form enctype="multipart/form-data" action="http://localhost//admin.php/common/add_images.html" method="post">  
<input type="file" name="file" size="50"><br>  
<input type="submit" value="Upload">  
</form>

任意文件上传GETSHELL:

最后

上传文件位置不止一处,其他的还需一一验证。

posted @ 2021-01-11 16:55  cHr1s_h  阅读(522)  评论(0编辑  收藏  举报