项目管理系统远程命令执行漏洞

admin 2024年11月15日13:47:36评论13 views字数 7314阅读24分22秒阅读模式
项目管理系统远程命令执行漏洞

本文来源自平安银河实验室

作者:李思锐

从 官方 dockerhub 镜像仓库下载 docker 镜像。
这里下载漏洞相关版本进行diff,开源版本18.0.beta1和18.0.beta2。
运行两个版本,使用beyond compare进行diff,比较两个版本的源代码。
项目管理系统远程命令执行漏洞
根据已公开的漏洞描述是鉴权绕过和命令注入漏洞。

漏洞分析

下面是从补丁逆推漏洞的过程
鉴权绕过
因为是鉴权绕过,所以猜测是通用类/公共类/入口逻辑有问题,优先diff api/module 下的变动。发现这里存在不同(module/common/model.php#L2455)checkPriv函数,由 echo -> die
项目管理系统远程命令执行漏洞
看下这个函数的定义,和修改处可能的原因,容易看出这个地方存在较大风险,因为 checkPriv 是 return void 即无返回值,正常来说在调用的时候不会关心其返回值,由checkPriv本身处理掉所有异常。
项目管理系统远程命令执行漏洞
但是这里(module/common/model.php#L2455)checkPriv函数,由 echo -> die,容易看出这里的echo会导致checkPriv函数实际不起作用,实际在不满足条件的时候后续代码还是会继续执行,所以才进行修复,由 echo 改为 die。
那么审计这个函数的逻辑,尝试找出触发条件。
/** * Check the user has permission to access this method, if not, locate to the login page or deny page. * * @access public * @return void */ public function checkPriv(){ try { $module = $this->app->getModuleName(); $method = $this->app->getMethodName(); if($this->app->isFlow) { $module = $this->app->rawModule; $method = $this->app->rawMethod; } $beforeValidMethods = array( 'user' => array('deny', 'logout'), 'my' => array('changepassword'), 'message' => array('ajaxgetmessage'), ); if(!empty($this->app->user->modifyPassword) and (!isset($beforeValidMethods[$module]) or !in_array($method, $beforeValidMethods[$module]))) return print(js::locate(helper::createLink('my', 'changepassword'))); if(!$this->loadModel('user')->isLogon() and $this->server->php_auth_user) $this->user->identifyByPhpAuth(); if(!$this->loadModel('user')->isLogon() and $this->cookie->za) $this->user->identifyByCookie(); if($this->isOpenMethod($module, $method)) return true; if(isset($this->app->user)) { $this->app->user = $this->session->user; if(!commonModel::hasPriv($module, $method)) { if($module == 'story' and !empty($this->app->params['storyType']) and strpos(",story,requirement,", ",{$this->app->params['storyType']},") !== false) $module = $this->app->params['storyType']; $this->deny($module, $method); } } else { $uri = $this->app->getURI(true); if($module == 'message' and $method == 'ajaxgetmessage') { $uri = helper::createLink('my'); } elseif(helper::isAjaxRequest()) { die(json_encode(array('result' => false, 'message' => $this->lang->error->loginTimeout))); // Fix bug #14478. } $referer = helper::safe64Encode($uri); die(js::locate(helper::createLink('user', 'login', "referer=$referer"))); } } catch(EndResponseException $endResponseException) { echo $endResponseException->getContent(); } }
简化为:
{ try { // code } catch(EndResponseException $endResponseException) { echo $endResponseException->getContent(); }}
容易看出触发需要try包裹的代码抛出EndResponseException异常,简单搜索一下,容易发现是framework/helper.class.php的end函数。
项目管理系统远程命令执行漏洞
项目管理系统远程命令执行漏洞
查找一下helper::end()
项目管理系统远程命令执行漏洞
容易得到需要进入这个环节if(!commonModel::hasPriv($module, $method))
public function checkPriv(){ try { $module = $this->app->getModuleName(); $method = $this->app->getMethodName(); if($this->app->isFlow) { $module = $this->app->rawModule; $method = $this->app->rawMethod; } $beforeValidMethods = array( 'user' => array('deny', 'logout'), 'my' => array('changepassword'), 'message' => array('ajaxgetmessage'), ); if(!empty($this->app->user->modifyPassword) and (!isset($beforeValidMethods[$module]) or !in_array($method, $beforeValidMethods[$module]))) return print(js::locate(helper::createLink('my', 'changepassword'))); if(!$this->loadModel('user')->isLogon() and $this->server->php_auth_user) $this->user->identifyByPhpAuth(); if(!$this->loadModel('user')->isLogon() and $this->cookie->za) $this->user->identifyByCookie(); if($this->isOpenMethod($module, $method)) return true; if(isset($this->app->user)) { $this->app->user = $this->session->user; if(!commonModel::hasPriv($module, $method)) { // 需要进入这个逻辑才能触发 if($module == 'story' and !empty($this->app->params['storyType']) and strpos(",story,requirement,", ",{$this->app->params['storyType']},") !== false) $module = $this->app->params['storyType']; $this->deny($module, $method); } } else { $uri = $this->app->getURI(true); if($module == 'message' and $method == 'ajaxgetmessage') { $uri = helper::createLink('my'); } elseif(helper::isAjaxRequest()) { die(json_encode(array('result' => false, 'message' => $this->lang->error->loginTimeout))); // Fix bug #14478. } $referer = helper::safe64Encode($uri); die(js::locate(helper::createLink('user', 'login', "referer=$referer"))); } } catch(EndResponseException $endResponseException) { echo $endResponseException->getContent(); } }
整理一下checkPriv这里触发需要的条件:
  • $this->app->user 即 user 存在
  • !commonModel::hasPriv($module, $method) 没有权限访问指定模块的指定方法
满足这两个条件时,就能让绕过权限校验。
先看条件一, 需要获取到 $this->app->user ,先看下$this如何初始化的
module/common/model.php的构造函数中,user通过setUser方法完成初始化。
项目管理系统远程命令执行漏洞
项目管理系统远程命令执行漏洞
如果 session 中存在user,那么取$this->session->user的值。
如果是访客($this->app->company->guest) 或者 PHP_SAPI == 'cli', 则新构造一个“guest”的user对象,取这个guest user的值。
即需要是下面情况之一:
  • session 中有 user 对象,但是对 user 具体内容没有要求,比如guest
  • 访客模式
  • 命令行模式下调用
后两种情况限制条件较大,那么先尝试第一种,尝试是否有途径可以生成 user,因为checkPriv函数有if($this->isOpenMethod($module, $method)) return true;,则先看是否存在一个open method,可以生成 session user。
public function isOpenMethod($module, $method){ if(in_array("$module.$method", $this->config->openMethods)) return true; if($module == 'block' and $method == 'main' and isset($_GET['hash'])) return true; if($this->loadModel('user')->isLogon() or ($this->app->company->guest and $this->app->user->account == 'guest')) { if(stripos($method, 'ajax') !== false) return true; if($module == 'block') return true; if($module == 'my' and $method == 'guidechangetheme') return true; if($module == 'misc' and $method == 'downloadclient') return true; if($module == 'misc' and $method == 'changelog') return true; if($module == 'tutorial' and $method == 'start') return true; if($module == 'tutorial' and $method == 'index') return true; if($module == 'tutorial' and $method == 'quit') return true; if($module == 'tutorial' and $method == 'wizard') return true; if($module == 'product' and $method == 'showerrornone') return true; } return false; }
三种情况如下,因为后两者的条件显然不满足,所以重点查找config/zentaopms.php定义里是否有可用方法。
  • "$module.$method" 在 config/zentaopms.php定义里面
项目管理系统远程命令执行漏洞
  • $module == 'block' and $method == 'main' and isset($_GET['hash'])
  • $this->loadModel('user')->isLogon() or ($this->app->company->guest and $this->app->user->account == 'guest') 即 登录状态或者访客
发现 $config->openMethods[] = 'misc.captcha'; 即 module/misc/control.php#L223 captcha方法满足条件能够利用,当 $sessionVar 设置为 'user' 的时候,$this->session->set($sessionVar, $captcha->getPhrase()); 即$this->session->set('user', $captcha->getPhrase());, user 对象被生成出来。
/** * Show captcha and save to session. * * @param string $sessionVar * @param string $uuid * @access public * @return void */ public function captcha($sessionVar = 'captcha', $uuid = ''){ $obLevel = ob_get_level(); for($i = 0; $i < $obLevel; $i++) ob_end_clean(); header('Content-Type: image/jpeg'); $captcha = $this->app->loadClass('captcha'); $this->session->set($sessionVar, $captcha->getPhrase()); $captcha->build()->output(); }
所以鉴权绕过的流程如下:
访问验证码接口,构造 $sessionVar = 'user',进入 misc.captcha 方法生成session user ,然后因为session user存在,绕过checkPriv 逻辑,实现能够调用任意模块方法的目的。

命令注入

因为是命令注入,优先查找 exec 关键字/ 组件功能模块,尝试发现diff内容中是否有相关内容,发现 lib/scm/gitrepo.class.php含有相关内容,这里对$client加了一次校验,容易看出这里存在利用的风险,后面补丁做了一次加固。

项目管理系统远程命令执行漏洞
当 $client 可控的时候,直接带入 exec 函数造成 exec("{$this->client} config core.quotepath false"); 命令执行。
那么看GitRepo的上级调用:
项目管理系统远程命令执行漏洞
项目管理系统远程命令执行漏洞
项目管理系统远程命令执行漏洞
有两处地方调用了setEngine方法,如上图,
update的要求先存在一个repo,repo存在检查(module/repo/control.php),当不存在的时候会返回创建链接,即存在前置条件。
项目管理系统远程命令执行漏洞
create的逻辑 module/repo/model.php
项目管理系统远程命令执行漏洞
这里需要SCM == 'Gitlab' 的时候返回true,才能在缺少其他条件/参数的时候通过校验。
项目管理系统远程命令执行漏洞
同时checkConnection函数这里直接拼接client,造成命令注入,导致在update/create 如果能进入checkConnection函数, 便通过client变量已经能够触发命令执行,无须再通过后面的逻辑来完成命令注入。
项目管理系统远程命令执行漏洞
那么总结如下:
create repo 可以通过SCM == Gitlab,创建repo,返回值 locate 字段中有 repo id。
项目管理系统远程命令执行漏洞
update repo 可以直接通过client参数执行命令,但是要求repo先存在。
项目管理系统远程命令执行漏洞
所以需要两者一起使用,流程如下:
1. post /repo-create.html 其中 SCM=Gitlab 创建代码库, 然后获取到repo id
2. post /repo-edit-${id}.html 其中id 为上一步获得的repo id ,通过client变量注入命令,可以通过报错使得接口返回信息

验证过程

访问 /misc-captcha-user.html 获得一个session user (权限绕过)后续都用返回的这个zentaosid。
GET /misc-captcha-user.html HTTP/1.1
项目管理系统远程命令执行漏洞
创建repo, 如果有repo的话这步可跳过 记录下repo id。
项目管理系统远程命令执行漏洞
编辑repo,在这一步触发命令执行。
项目管理系统远程命令执行漏洞
项目管理系统远程命令执行漏洞

验证成功图片

项目管理系统远程命令执行漏洞
项目管理系统远程命令执行漏洞

原文始发于微信公众号(平安集团安全应急响应中心):项目管理系统远程命令执行漏洞

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

发表评论

匿名网友 填写信息