ZenTao CMS | Privasec

ZenTao CMS – A Monkey’s journey to Priv Esc & Remote Code Execution

By Angus Strom, Privasec’s Managing Consultant

This blog post explores the ZenTao CMS application. The post is split into two parts firstly understanding how Zentao performs request routing, and secondly detailing vulnerabilities identified within the application that led to an attack chain that achieves Remote Code Execution.

Understanding ZenTao Routing

When approaching an application of this size and complexity one of the first things I like to do is to figure out how the application is routing requests, doing so allows us to easily match up the source code with the application functionality during testing.

At a high level ZenTao provides two methods of routing:

  • GET Method, which has paths that appear like:
  • PATH_INFO Method, which has paths that appear like:

We can find a sample configuration file at ZenTao/config/config.php that shows the possible routing configurations:

This alone already provides us some interesting information in understanding how the routing of ZenTao operates, we can tell that the dash acts as a divider of some sort for PATH_INFO routing, and that the Var variables act as some type of routing information for the GET method.

The actual in use configuration file is the ZenTao/config/my.php which by default, for a fresh ZenTao installation, sets the requestType as PATH_INFO:

Since PATH_INFO is the default routing method, lets first dive into understanding how the application routes this requestType. Looking into the www/index.php file of Zentao we can see this file sets up the application and calls the parseRequest method in the app object as shown:

This should automatically grab our interest as this file is responsible for setting up the application, and the term parseRequest() closely relates to the type of functionality we are looking to investigate. Searching the codebase for the parseRequest() function leads us to the framework/base/router.class.php file as shown below:

Judging from the comments and code we can see we are on the right track as this function will chose to parse the request based on which requestType was setup in the my.php configuration file. For now, we will follow the PATH_INFO IF branch, coming back to the GET IF branch later.

At a high level, the routing is done in two steps, firstly the parsePathInfo() function on line 1019 is called which parses the request and handles it in such a way that the setRoutebyPathInfo function on line 1020 is able to parse the path and route the request.

The setRouteByPathInfo() function goes into a complex chain of various functions that operate to route the request to the correct module. For the purposes of this blog post we do not need to fully investigate this chain in order to infer how routing operates within the application (this can be an excise for the curious).

By investigating the setRouteByPathInfo() function, as shown in the image below, we can see that the requestFix (which is a dash by default as outlined in the config above) is used to explode the path into multiple items.

The first item from this exploded array is set as the module name, and the second item set as the method name. If we look into the source code of ZenTao we can see that the module folder contains a number of named modules corresponding to various features of ZenTao, and inside each of these modules is a control.php file that has a number of named methods.

From inference of reading this code and using the application we can begin to piece together how the PATH_INFO routing is operating:

The image above shows how we can match up the path with the source code. The first parameter ‘doc’ points to the doc module, the second parameter ‘browse’ points to the browse method, and the remaining functions match up linearly with the additional optional parameters which will override their default settings if set. Great, with this we understand how the PATH_INFO routing operates, now we just need to investigate and understand the GET routing method.

Fortunately, understanding the GET routing method is easy as it follows a more traditional approach to routing. Looking back on the parseRequest() function we know that the GET requestType IF branch leads us to two functions, parseGET() and setRouteByGET():

The praseGET() function parses the request and gets the URI and viewType from the request before parsing it on to the setRouteByGET() function which is where the main part of routing occurs.

We can see that the moduleName and methodName are both set by getting the respective parameters based off the original configuration file, we can see this in action below.

Awesome, we now understand how both routing methods work, personally I find the GET method to be more familiar and easier to understand and for this reason I have set my research instance to use the GET requestType, however (with some work) most vulnerabilities discovered using this requestType should transfer over to the PATH_INFO requestType as well.

Gaining Remote Code Execution

To start off I always spend my time exploring the application from both a regular user perspective and administrative perspective. I’ll spend an hour or two just exploring the application and messing around with functionality to see if I can start to get any ideas on what to explore in more depth. Something that caught my eye right away was the System tab under the Admin menu.

This catches my attention as the tab is called Cron, which anyone familiar with Linux knows is a time based job scheduler used to run shell commands. To understand the function, I search for documentation with a quick google search, however this brings up an interesting result:

It would seem that this function has had previous issues with Remote Code Execution, on reading the article, it appears that the function previously ran direct shell commands using the System command type. As we can see in the previous picture, this appears to be no longer the case, and that the Type drop down field no longer supports System. Some form of mitigation had taken place to run the commands as ‘modules’ instead of direct shell commands, however the article doesn’t state if this fix was investigated.

I investigate the Cron module, which leads us to /zentao/module/cron/control.php in which we find the code responsible for the Cron tab. Reviewing the following code block catches my eye:

It appears that if we are able to set the type of the Cron job to ‘system’ then it will execute the command field! This is likely some form of left over code that wasn’t removed during the changes to resolve the previous vulnerability. We already know we can’t set the type to System via the drop down menu in the application, however what happens if we intercept the request and set the parameter to System?

It seems our request was accepted without error, looking in the GUI we don’t see any change to the Cron job, however if we look in the backend database we can see quite clearly that our type is now System.

As it turns out, the code responsible for handling updating and creation of Cron jobs will accept any of the zt_cron table parameters and update them as long as they exist within the table. Thus updating this to System achieves Remote Code Execution as we now satisfy the code block above and reach the exec() statement. This vulnerability has been reported and assigned CVE-2021-27556.

With this, we have remote code execution from the Admin account, however admin accounts are often hard to come by. Luckily, there is a way for us to achieve this RCE with just a normal user account.

RCE with XSS and CSRF

If we look back on the previous Burp interception screenshot above, we can see that the request seems to lack any form of Cross Site Request Forgery (CSRF) protection, we can confirm this by crafting a CSRF payload (aka using the Burp CSRF generator because I’m lazy) and observing that it does allow for a XHR payload to update the Cron job tab.

If you are unfamiliar with CSRF I’d recommend checking the PortSwigger Academy page on CSRF, as this post won’t go into detail on explaining CSRF.

So, we now know that if we can lure a user signed into an admin account to an attacker-controlled webserver, then we can force them to update the Cron job tab to include a command of our choice that will execute as a ‘system’ command. Rather then executing a complex command, we can abuse this vulnerability in order to reset the Admin password and give us more direct access to the Remote Code Execution vulnerability. When we go to the Forgot Password function of Zentao, we are given the option to create a file on the host that will cause the administrator accounts password to be reset as shown:

Thus, if we use the touch command to create the above file through our CSRF payload then we are able to reset the password of the Administrator account and gain direct access to the Cron job tab, and other admin functions as shown:

With that, we have reset the admin password and obtained admin access. However, the attack chain requires us to trick an admin user into clicking a URL that isn’t trusted, we can improve this exploit chain one step further by abusing Reflected Cross Site Scripting in order to include the XHR payload into a Zentao link, making the URL provided to the admin appear to be trusted.

There are a number of areas for reflected cross site scripting throughout the application, any basic fuzzing tools will give back a long list of possible injection points. For our purposes we’ll look at the following injection point:

We can inject into the data-link-creator of the response and break out of this in order to execute our own scripts. This comes with a number of caveats; we can’t use spaces and we also can’t use dots. Using either of these will be filtered into an underscore breaking the script execution. To bypass this, we can use a forward slash instead of a space to bypass the space filter, and we can also use a homograph attack to bypass the dot filter. This is shown below whereby we can script include our CSRF payload, note this does not include the dot homograph bypass:

Wonderful, with this we have completed our attack chain and can escalate from reflected cross site scripting, to an admin account takeover, and finally to remote code execution. The XSS and CSRF vulnerabilities are identified under CVE-2021-27558 & CVE-2021-27557 respectively.


This post has dived into exploring Zentao, understanding how its routing works, and identifying several vulnerabilities that lead to an attack chain that an attacker can execute in order to achieve remote code execution. This is by no means an all-inclusive look at the Zentao platform and I highly recommend that if this post interested you that you perform your own research into the platform as there are likely to be many more vulnerabilities waiting to be discovered.

Thank you for taking the time to read my blog post, for your time please enjoy this TLDR summary in monkey form:

Disclosure Timeline

22 / 02 / 2021 – Initial Email
25 / 02 / 2021 – Follow up Email
03 / 03 / 2021 – Response received, Report sent to R&D, 90 day Disclosure timeline given
15 / 04 / 2021 – Follow up Email to check in on remediation
30 / 04 / 2021 – Second Follow up email
07 / 07 / 2021 – Blog Post Release

Scroll to Top