CVE-2021-45467: CWP CentOS Web Panel – preauth RCE

CentOS Web Panel or commonly known as CWP is a popular web hosting management software, used by over 200,000 unique servers, that can be found on Shodan or Census. The vulnerability chain that we used to exploit a full preauth remote command execution as root uses file inclusion (CVE-2021-45467) and file write (CVE-2021-45466) vulnerabilities. In this post we hope to cover our vulnerability research journey, and how we approached this particular target. 

Mapping out attack surface

After hosting CWP on a local environment it quickly became evident that most features require administrative or user accounts to perform. Since we are interested only in vulnerabilities that can be exploited without user authentication or interaction, we will avoid all the restricted sections and focus our research on parts of the panel that are exposed without authentication in the webroot. Turns out, not a lot is exposed. 

Some of the interesting pages that can be accessed without authentication are /user/loader.php and /user/index.php: had this interesting file inclusion protection method (/user/loader.php):

if(!empty($_GET["api"])) {
        if (!empty($_GET["scripts"])) {
            $_GET["scripts"] = GETSecurity($_GET["scripts"]);
            include "../../resources/admin/scripts/" . $_GET["scripts"] . ".php";

Where GETSecurity() is defined as the following: 

function GETSecurity($variable)
    if (stristr($variable, ".." ) {
        exit("hacking attempt");

if the parameter “scripts” contains “..” (two dots) then the application will not process the input and will instead exit with “hacking attempt” displayed to the user. 

stristr() is basically the same function as strstr() in PHP except it isn’t case sensitive. Few ideas I had after looking at the code:

Potential methods of bypassing stristr(): 

1. Trick CWP to treat other characters as dot (.) 

2. Find unique characters the language C processes as a dot (.) when lower cased. 

3. Trick CWP into thinking there are no consecutive dots (..)

1. Trick CWP to treat other characters as a dot:

For the first method, after fuzzing a basic unicode range, we quickly realized CWP doesn’t have any characters that it normalizes to dots. So this path wasn’t as promising as we thought it would be.

2. Find unique characters the language C processes as a dot when lower cased.

The idea behind this came after wondering how stristr() does a case sensitivity check different from strstr(). PHP is written in C, so sometimes it helps to look at how the underlying functions work. And in this case, looking at php-src/ext/standard/string.c in PHP proved that stristr() is basically strstr():

PHPAPI char *php_stristr(char *s, char *t, size_t s_len, size_t t_len)
    zend_str_tolower(s, s_len);
    zend_str_tolower(t, t_len);
    return (char*)php_memnstr(s, t, t_len, s + s_len);

After looking at the code, you realize that the function takes any input and converts it to lowercase before it does the comparison. So, immediately we started fuzzing for unique unicode characters that when lowercased, might be converted to a dot. In javascript for example, the character “ſ” gets converted to “S” when capitalized: “ſ”.toUpperCase() => “S”. Unfortunately that didn’t yield any useful results but we did find some weird quirky behaviors worthy of future posts. 

3. Trick CWP into thinking there are no consecutive dots (..)

This requires straight up fuzzing. After brainstorming a few ideas, we got this code: 

Running the above fuzzer actually got us a bypass: which is /.%00./


During our tests some functions in CWP (including the require() and include() functions) seem to process /.%00./ as /../ – Similarly, while their stristr() ignores the null bytes, it still counts its size so it bypasses the check. We aren’t exactly sure why this happens but it could be caused by a regression to their library using something like striptags()

Now using this trick it means we can include any local file on the server. We have a full file inclusion vulnerability, and if we find a way to write to a file, we can get preauth RCE. However, this next step wasn’t as straightforward as we’d like it to be, since CWP ships with interesting unix file r/w locking settings called CWP-Secure-Kernel. The server doesn’t give us easy file write or edit permissions to logs or anywhere obviously useful. 

But with our file inclusion bug, it means we can reach the restricted API section, which requires API key to access and is unreachable because it’s not exposed in the webroot. But by using our file inclusion, sending a request like the following will result in the server registering any API key we want.

GET https://CWP/user/loader.php?api=1&scripts= .%00./.%00./api/account_new_create&acc=guadaapi&ip= 

Now we have added the api key “OCTAGON” requesting from to have access to the full API like the following: 

GET https://CWP/api/?key=OCTAGON&api=add_server is now a valid API request. When we reported this specific the vulnerability to CWP, their fix was adding the following code:

function GETSecurity($variable)

if (stristr($variable, "..") || stristr($variable, ".%00.")) {        
exit("hacking attempt");


.%00%00%00./.%00%00%00./api/account_new_create lets us reach the same file, just add as many nullbytes as you like.


Time to find a basic file write vulnerability. This wasn’t very easy, but we found we can exploit a file write bug in the API section that lets us add to a .TXT file. For example, using our maliciously added key

https://CWP/api/?key=OCTAGON&api=add_server&DHCP=<?=phpinfo()?>&fileFinal=/ will now write to a file called authorized_keys located in the /resources/ folder. Next we use our first file inclusion bug to include our malicious authorized_keys file to get full RCE.  The file inclusion vulnerability was reported via the ZDI program and has been patched in CWP, but we saw some managed to reverse the patch and exploit some servers.

So to reiterate the steps:

  1. Send a null byte powered file inclusion payload to add malicious API key 
  2. Use API key to write to a file (CVE-2021-45466)
  3. Use step #1 to include the file we just wrote into (CVE-2021-45467)
  4. Woot! 

We will release a full PoC for red teams that achieves preauth RCE once enough servers migrate to the latest version.