Path Traversal Vulnerabilities in Icinga Web

by thomas chauchefoin|

Path Traversal Vulnerabilities in Icinga Web

Icinga is a modern, open-source IT monitoring system with a web interface. Thanks to its specialized scripting language, it is highly configurable and can run checks on virtually any IT equipment. It also offers useful built-in plugins to query the state of services running on monitored hosts, such as running services, network traffic, or available disk space.

We recently discovered two code vulnerabilities in Icinga Web that allow attackers to compromise the server on which it is running by running arbitrary PHP code. As part of our research, we unveiled an unpatched bug in the PHP engine itself that enables the exploitation of one of the findings. This article presents the technical details of both vulnerabilities and how the maintainers fixed them. 

It’s not common to discuss both PHP and C code in the same blog post; we will do our best to keep it fun. Let’s dive into it!

Impact

The most common way to deploy Icinga is to use the administration interface Icinga Web that communicates with the Icinga monitoring server.

We discovered a Path Traversal vulnerability (CVE-2022-24716) that can be abused to disclose any file on the server. It can be exploited without authentication and without prior knowledge of a user account. We also discovered CVE-2022-24715, which leads to the execution of arbitrary PHP code from the administration interface. 

They can be easily chained to compromise the server from an unauthenticated position if the attacker can reach the database by first disclosing configuration files and modifying the administrator's password. 

We strongly recommend updating your icingaweb2 instances to either 2.8.6, 2.9.6, or 2.10, even if they are not directly exposed to the Internet. 

Although we won't be releasing a proof-of-concept, exploiting these findings is straightforward. We also recommend assuming that any secret present in the Icinga Web configuration (e.g. database credentials) could have been compromised; they should be rotated as a precautionary measure. 

Technical Details

We assume that Icinga Web 2 was deployed using the upstream packages in version ​​2.9.5-1.hirsute and following the official documentation. As you will later see in the section CVE-2022-24715 - Remote Code Execution, this setup makes the exploitation slightly more complex for attackers and more interesting for us security researchers!

Arbitrary File Disclosure (CVE-2022-24716)

Context

The Apache HTTP server is configured to dispatch all the incoming requests to index.php using its module mod_rewrite; this setup is very common for modern PHP applications to provide only one entry point:

.htaccess

<IfModule mod_rewrite.c>    
  RewriteEngine on    
  RewriteBase /icingaweb2/    
  RewriteCond %{REQUEST_FILENAME} -s [OR]    
  RewriteCond %{REQUEST_FILENAME} -l [OR]    
  RewriteCond %{REQUEST_FILENAME} -d    
  RewriteRule ^.*$ - [NC,L]    
  RewriteRule ^.*$ index.php [NC,L] 
</IfModule>

This first script loads webrouter.php and then tries to dispatch the request to the right software component based on the requested path:

  • Important static resources (css/icinga.css, css/icinga.min.css, etc.) are processed first, with support for the ETag header, minification and server-side cache;
  • Dynamically-generated images (svg/chart.php, png/chart.php) based on request parameters;
  • Requests to paths starting with lib/ are handled by StaticController;
  • Everything else is handed to controllers.

Dynamic routers are always interesting components to review: they have to construct paths based on user-controlled data and are thus very prone to path traversal vulnerabilities; that’s what happens here!

Identifying the code vulnerability

The important code of StaticController is shown below: it first iterates over existing libraries to find one matching the request URL and then concatenates the associated asset path to the a value provided by the client:

Icinga/Web/Controller/StaticController.php

$assetPath = ltrim(substr($request->getRequestUri(), strlen($request->getBaseUrl()) + 4), '/');

$library = null;
foreach ($app->getLibraries() as $candidate) {
    if (substr($assetPath, 0, strlen($candidate->getName())) === $candidate->getName()) {
        $library = $candidate;
        $assetPath = ltrim(substr($assetPath, strlen($candidate->getName())), '/');
        break;
    }
}
// [...]
$assetRoot = $library->getStaticAssetPath();
$filePath = $assetRoot . DIRECTORY_SEPARATOR . $assetPath;
[...]    
$app->getResponse()
[...]
        ->setBody(file_get_contents($filePath));
}

The code of StaticController has two security issues:

  • Libraries can declare an empty asset path, in which case the path to the file is constructed using only the user input; for instance, icinga/icinga-php-thirdparty.
  • The user input can contain directory traversal sequences (../), resulting in a final path outside the intended directory; for instance, icinga/icinga-php-library.

Impact

As a result, attackers can disclose any file of the local filesystem. We could confirm this vulnerability against the official demonstration instance, for instance by obtaining the contents of the file /etc/hosts:

$ curl https://icinga.com/demo/lib/icinga/icinga-php-thirdparty/etc/hosts -v
[...]
127.0.0.1   localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.1  demo-icinga2
172.17.0.3  2a2f396a3e13

Attackers can also target incingaweb2 configuration files. Among other things, they contain database credentials used by the web interface.

If attackers can reach the database service, they can use these credentials to change the password of an existing account and gain authenticated access to the instance. We pursued this scenario and later found a way to execute arbitrary code on the instance thanks to this access (see below).

On non-default deployment, Icinga can also be told to use SSH private keys present on the local filesystem. They could be read using this technique and later pivot to other systems with the identity of the monitoring agent.

Remote Code Execution (CVE-2022-24715)

Initial finding

Authenticated users can edit resources to later reference them from other configuration files. One of the resource types is SSH keys, which require to be written to the local filesystem to be used.

We identified that no validation is performed on the parameter user of the SshResourceForm at [1]. It allows attackers to use directory traversal sequences (e.g. ../) to write the SSH key outside of the intended directory at [2]:

application/forms/Config/Resource/SshResourceForm.php

public static function beforeAdd(ResourceConfigForm $form)
{
    $configDir = Icinga::app()->getConfigDir();
    $user = $form->getElement('user')->getValue();
    $filePath = $configDir . '/ssh/' . $user; // [1]
    if (! file_exists($filePath)) {
        $file = File::create($filePath, 0600);
    // [...]
    $file->fwrite($form->getElement('private_key')->getValue()); // [2]

Our first assumption was to consider this bug useless since SSH keys are validated with openssl_pkey_get_private(); it doesn't sound easy to craft a PHP script that would also be a valid PEM certificate. 

This function call being the only obstacle, it is worth investigating a bit deeper and taking the time to study its implementation. As mentioned in the documentation, this function is part of PHP's Cryptography Extensions; its code is located in php-src/ext/openssl

We need to go deeper!

While looking at this implementation in the PHP engine source code, one can notice a quirk specific to the OpenSSL module in PHP. Such libraries usually offer one way to load data, either based on the file's name that it will open and read or the data itself (in which case it's up to the user to handle any I/O operation). 

Here, both methods are automatically supported: if the parameter $private_key is prefixed with file://, it reads the file for the user. Otherwise, this parameter is considered to be the value of the certificate. 

This leads to some rather uncommon control flow in its implementation:

php-src/ext/openssl/openssl.c

static EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *passphrase, size_t passphrase_len)
{
   EVP_PKEY *key = NULL;
   X509 *cert = NULL;
   bool free_cert = 0;
   char * filename = NULL;
   // [...]
   } else {
       // [...]       
       if (Z_STRLEN_P(val) > 7 && memcmp(Z_STRVAL_P(val), "file://", sizeof("file://") - 1) == 0) {
           filename = Z_STRVAL_P(val) + (sizeof("file://") - 1);
           if (php_openssl_open_base_dir_chk(filename)) {
               TMP_CLEAN;
           }
       }
           // [...]
           if (filename) {
               in = BIO_new_file(filename, PHP_OPENSSL_BIO_MODE_R(PKCS7_BINARY));
           } else {
               in = BIO_new_mem_buf(Z_STRVAL_P(val), (int)Z_STRLEN_P(val));
           }

In the code snippet above, zval *val is the internal representation of the private key submitted via the form. val is binary-safe, which means that the PHP engine can work with the complete string even if it contains NULL bytes by keeping track of its length in bytes alongside the data. However, the libssl API (BIO_*) only works with NULL-terminated char arrays, which are inherently not binary-safe: processing will stop at the first NULL byte. 

Attackers can use this quirk to circumvent the validation performed by openssl_pkey_get_private() while keeping the ability to put arbitrary data in the resource file: PHP stops at the first NULL byte while searching for the certificate on the disk, but the full data will be written to the destination file!

Attackers could then craft a payload in 4 parts:

  • The mandatory prefix to enter the vulnerable code path, file://;
  • Path to a valid PEM certificate on the server, e.g., /usr/lib/python3/dist-packages/twisted/test/server.pem in our test virtual machine;
  • A NULL byte;
  • The contents of the file to write, here a small PHP script executing an external command.

Example of a payload to exploit CVE-2022-24715

One last thing

When installed using the official Linux packages, the PHP scripts of Icinga Web 2 are deployed under /usr/share/icingaweb2. They are owned by the root user and hence can't be modified with the identity of www-data under which the HTTP server is running.

While this would prevent straightforward exploitation based on planting a PHP file under this directory and accessing them, we found another technique that attackers could use to obtain the execution of arbitrary code. 

Icinga has a notion of modules, self-contained third-party code that extends the interface's capabilities (e.g., to add Grafana support). These modules are stored under /usr/share/icingaweb2/modules by default, but administrators can also change this path directly from the interface.

The setting global_module_path expects colon-separated paths from where modules are located. Changing this value to a path where the previously demonstrated vulnerability can write, say /dev/shm/, setting  global_module_path to /dev/, and enabling the new module named shm allows executing arbitrary PHP code.

Patches

Both vulnerabilities are related to a similar vulnerable code pattern and were addressed by introducing a new validation step after constructing the destination path (067ec0f, b7c31eb):

  • The path is constructed;
  • realpath() is called: directory traversal sequences, symbolic links are resolved and ensure that the destination file exists;
  • It made sure that the path resulting from the realpath() call is still under the expected directory. 

Further format validation is also performed on the value of the SSH resources before writing them to the disk to prevent the use of file://.

We also reached out to the PHP maintainers to address the NULL byte injection in the functions of the OpenSSL core extension. Because there isn’t any other function designed to validate the format of certificates, other software is likely using the same vulnerable functions. 

We provided patches and test cases to ease their adoption by the maintainers; the bug ticket is still open as of the time of writing this article. Nevertheless, we chose to publicly document this bug as the security risk is deemed low, and an additional fix has been present Icinga Web 2 for several weeks. 

Timeline

DateAction
2022-02-15We report the first path traversal vulnerability to Icinga.
2022-02-21We report the second path traversal vulnerability to Icinga.
2022-02-23Icinga acknowledges the vulnerabilities, GitHub advisories are created.
2022-03-10The PHP bug is reported on the upstream bug tracker in #81713.
2022-03-14Icinga releases icingaweb2 2.8.6, 2.9.6 and 2.10.

Summary

In this publication, we covered the technical details behind two very similar vulnerabilities in Icinga Web 2, an IT monitoring solution. Both vulnerabilities can be combined within an attack in order to fully compromise the Icinga server. During the research of these vulnerabilities, we also discovered a bug in the PHP interpreter itself. We had a nice reminder that unintended quirks may be found in the implementation of a language’s built-in functions which can allow the exploitation of bugs that would be safe otherwise.

We strongly recommend not exposing such systems to Internet as-is: they should only be reachable by trusted source IP addresses (e.g., a VPN endpoint) or put behind a centralized authentication system. 

We would like to thank the maintainers of Icinga and PHP for their prompt replies and help in addressing our findings. 

Related Blog Posts