Control Web Panel - Fingerprinting Open-Source Software using a Consolidation Algorithm approach

Control Web Panel code

Introduction

At Bitsight, part of the core work of the Vulnerability Research team is to analyze new high-profile vulnerabilities and ensure we come up with ways to detect, at an internet-wide scale, who is affected by these. Sometimes - more often than not - the direct exploitation of these vulnerabilities is significantly intrusive, and thus we can not load a direct port of the publicly available Proofs-of-Concept onto our internet scanning infrastructure. Instead, we need to find unique ways of inferring exposure without causing any impact.

This blog post details one of these very unique cases: `CVE-2022-44877`, an unauthenticated Command Injection issue, flagged by CISA as a Known Exploited Vulnerability (CISA KEV), affecting Control Web Panel, an open-source control panel for servers and VPS management. Initially, the team could not find a way to straightforwardly fingerprint the software’s version, nor another way to detect it without intrusive exploitation - thus we used a novelty technique: an algorithm that retrieves the web application’s static web content files and consolidates them to pin-point the software’s version.

Problem statement

NVD describes `CVE-2022-44877` as a vulnerability in “CWP (aka Control Web Panel or CentOS Web Panel) 7 before `0.9.8.1147` allows remote attackers to execute arbitrary OS commands via shell metacharacters in the login parameter.”

Upon further inspection, and analyzing the first published PoCs, such as CVE-2022-44877 - nuclei template, we quickly understand that this is achieved by intercepting the HTTP POST request used in the login flow and injecting a simple command injection payload in the `login` URL parameter.

The technique in the above and most PoCs actively exploits the issue to execute a `ping` command on the vulnerable target and will confirm its success by waiting for the callback of that `ping` command.

By examining the differences in the code and the differences in the behavior between the patched and unpatched versions of the product, we could not find any differences that we would be able to detect from an external standpoint without actually executing code in the vulnerable host.

When this is the case, we usually move on to exploring fingerprinting techniques that would allow us to infer which version of the software the target instance is running - and then compare that against the versions we know to be vulnerable from the advisories.

Again, CWP proved tricky in this area as well - while examining the code of the software we only found one endpoint in which the version information was dynamically available: `version.php` - however this endpoint was only accessible with authentication.

Our last resort when the above happens is to enumerate all the applications’ unauthenticated content we can find (both endpoints and static files), to attempt to find one in which the content or behavior is different between patched and unpatched versions of the software - even if this content is unrelated with the vulnerability in question.

Static Files Analysis

As CWP is an open-source product, getting a hold of several different vulnerable and not-vulnerable versions was quite trivial - as we could just download them all from the GitHub releases page. Then, we sorted the files by last-modified date to see if any static files or endpoints had suffered changes across these.

In terms of endpoints, several `.php` files were changed between the versions - but unfortunately, none of these were accessible without authentication. For static files, however, we found our first potential lead in a Javascript one called `filemanager.js`.

Control Web Panel code1

In the below screenshot, we created our first initial helper script which computed the hash of that file for each of the downloaded versions.

Control Web Panel code

We can see that from version `0.9.8.1146` (Vulnerable) to `0.9.8.1150` (Not Vulnerable) - the following file (which is externally accessible) - `https:///admin/design/js/filemanager.js` changed in version `0.9.8.1146` (one version before the vulnerability fix in `0.9.8.1147`). This meant that we could reliably flag all vulnerable versions except for `0.9.8.1146` (which will be a False-Negative).

Our initial logic would be:

  1. Download all the software versions and compute the hash of `admin/design/js/filemanager.js`, building a list of hashes in which the version was vulnerable (except for 1146)
  2. Then, in our scanner, when we find a CWP instance, we download the `js` file and compute its hash at runtime
  3. If the hash is on the list of vulnerable hashes, we would flag it as vulnerable

After starting with this approach, we faced our first issue: In versions `0.9.8.980` and below, this file does not exist. The obvious idea would be to check if the scanner found the file and it was in the list of vulnerable hashes OR the scanner didn't find the file, but this would make the check very prone to False-Positives if the file was ever also removed in future versions.

To circumvent this problem, we attempted to find another file that has been present in all available versions (the used one was `main.css`), and use its hash in conjunction with the previous one, with the following logic:

  • The `filemanager.js` file exists and is within the list of vulnerable hashes (this will include versions `0.9.8.1145` to `0.9.8.980`)

OR

  • The `filemanager.js` file does not exist AND the `main.css` file is within the list of all currently available versions

The Consolidation Algorithm

When reviewing this new strategy of consolidating the result of two different files, we thought: Why don’t we expand this further to use even more files?

We can have each of the pre-computed hashes associated with the file and version they belong to - and once we scan a CWP target and extract hashes for all the files we have indexed, we can use a cross-check them to narrow down as much as possible the versions the instance could be running.

So we did. We changed our helper script to find all files with a certain extension (we used .css, .js, .html, .png) and from these, extract the top 6 files with more modifications across all the versions. Taking these files, it then computed the hash for each of them to build a dictionary with the following format, containing all the different hashes, their respective files, and the versions of the software in which they were present:

{
 "md5hash":
   {"file":"admin/design/js/filemanager.js", "versions":["cwp-el7-0.9.8.300", "cwp-el7-0.9.8.301"]},
 "md5hash2":
   {"file":"admin/design/js/main.css", "versions":["cwp-el7-0.9.8.300", "cwp-el7-0.9.8.3001"]},
}

Finally, in the live scan, we fetch these files from a live endpoint, compute their md5 hash, and perform the intersection of these against our constants dictionary to narrow down as much as possible the versions that the software could be running - in what we ended up nicknaming our Consolidation Algorithm.
 

@staticmethod

    def _get_narrowed_versions(webpanel_data):

        """

        We cross-check the files-hashes across the 4 requested files - and compare it to the indexed file:hash:versions dictionary in constants.py to narrow down as much as possible to a version

        Since the versions associated with each hash are sequential - we can use a range check to infer this information

        """

        min_version = None

        max_version = None

        for hash_value, _ in webpanel_data.files_hash:

            if hash_value in versions_fingerprints:

                version_list, parsed_version_list = versions_fingerprints[hash_value]['version'], []

                for version in version_list:

                    parsed_version_list.append(parse_version(version))

                if not min_version or parsed_version_list[0] > min_version:

                    min_version = parsed_version_list[0]

                if not max_version or parsed_version_list[-1] < max_version:

                    max_version = parsed_version_list[-1]

 

        if min_version and max_version:

            webpanel_data.min_version, webpanel_data.max_version = (

                str(min_version),

                str(max_version),

            )

            return f'{min_version} to {max_version}'

        return ''

By using this, we were able to scan the Internet and fingerprint the CWP product with an acceptable granularity that wouldn’t have been possible any other way without authentication.

The below graphs detail the version distribution results, dating back to when this technique was first implemented: April 2023. At the present time, the amount of vulnerable instances we observe is negligible.

Other version ranges

Using these ranges of versions, we developed our detection capability for CVE-2022-44877, but the advantage of this method is that if any new CVEs for the same product were to surface, these ranges could be re-used.

Conclusion

With this blog post, we aim to demonstrate the importance of creative thinking and out-of-the-box strategies when fingerprinting products when it’s imperative that our solution is not intrusive and does not cause any impact on the scanned systems.

We also decided to share this case because we believe that consolidating fingerprints of exposed files is a technique that can be re-utilized in a lot of other cases - especially other open-source products - where we can obtain and compute a list of these to use for comparison beforehand.