From Single / Double Quote Confusion To RCE (CVE-2022-24637)

Open Web Analytics (OWA) is an open-source alternative to Google Analytics. OWA is written in PHP and can be hosted on an own server. Version 1.7.3 suffers from two vulnerabilities, which can be exploited by an unauthenticated attacker to gain RCE, when chained together.

The cause of the first vulnerability (CVE-2022-24637) is a single quote / double quote confusion, which leads to an information disclosure. The header of an automatically generated PHP cache file containing sensitive information is defined as '<?php\n…' instead of "<?php\n…". This leads to a literal backslash and n character being written instead of a newline character resulting in a broken PHP tag. Because of this the file is not interpreted as PHP code, but delivered in plain leaking sensitive cache information. This information can be leveraged to set a new password for the admin user.

The second vulnerability is a PHP file write, which requires admin privileges. The internal settings for the logfile path as well as the log level can be changed by manually crafting a POST request. This way the logfile can be set to a PHP file. By also increasing the log level and generating an event with attacker controlled data, PHP code can be injected into this logfile. This results in the possibility to execute arbitrary PHP code.

I would like to thank Peter Adams, the creator and maintainer of OWA who released a patch for the issue only one day after my initial notification. I was really amazed by the quick and professional reaction. There are security issues in each and every software, but the difference is how these are dealt with.

Introduction
Single / Double Quote Confusion
PHP file write
Demonstration
Patch and Mitigations
Conclusion


Introduction

Open Web Analytics (OWA) is an open-source web analytics software written in PHP and using a MySQL database. The source code is freely available on GitHub. OWA offers profound tracking capabilities as well as a wide variety of detailed reports. In contrast to Google Analytics, OWA can be hosted on an own server.

In this article we will take a look at two vulnerabilities in OWA, which enable an unauthenticated attacker to gain RCE on the underlying webserver when combined. After determining the root cause of these vulnerabilities, we will point out how these can be mitigated and highlight a few general aspects regarding secure coding.

Single / Double Quote Confusion

A basic entity in OWA is represented by the owa_entity class. Examples for such entities inheriting from owa_entity are documents (owa_document), sites (owa_site) or users (owa_user). These entities are persisted in the MySQL database. In order to increase performance, OWA version 1.7.3 and prior uses a file based caching mechanism by default. When an entity is loaded from the database the first time, it is cached by writing its serialized data to a cache file.

The creation of these cache files is implemented in the putItemToCacheStore method within the owa_fileCache class (modules/base/classes/fileCache.php):

...

    function putItemToCacheStore($collection, $id) {
    
            ...
        
            $data = $this->cache_file_header.base64_encode(serialize($this->cache[$collection][$id])).$this->cache_file_footer;

            ...
            
            $tcf_handle = @fopen($temp_cache_file, 'w');

            ...
            
                fputs($tcf_handle, $data);
                
                ...

As we can see, the serialized entity is base64 encoded and written to a temporary cache file. The name of this temporary file is later changed to consist of a unique id with a .php extension:

            ...
            
            $cache_file = $collection_dir.$id.'.php';
    
            ...
            
                if (!@ rename($temp_cache_file, $cache_file)) {
                
                    ...

In the first code snippet we can also see that the base64 encoded data is prefixed by cache_file_header and suffixed by cache_file_footer. Let’s see how these are defined:

...

class owa_fileCache extends owa_cache {

    ...
    
    var $cache_file_header = '<?php\n/*';
    var $cache_file_footer = '*/\n?>';
    
    ...

Accordingly a cache file is supposed to look like this:

<?php
/*BASE64-ENCODED DATA*/
?>

When directly accessing such a PHP file, the response is supposed to be empty, because the file only contains a comment. Though the above strings are surrounded by single quotes instead of double quotes, which makes a severe difference when it comes to escape sequences:

php > echo '<?php\n/*'; // <-- single quotes
<?php\n/*
php > echo "<?php\n/*"; // <-- double quotes
<?php
/*

As the above example shows, when using single quotes, escape sequences such as \n are not evaluated.

What does this mean for the generated PHP file? The PHP tag <?php is supposed to be followed by a line feed (0x0a) like this:

user@b0x:~$ cat sample1.php 
<?php
echo 1;
?>

user@b0x:~$ php -f sample1.php
1

Valid alternatives for the line feed are tab (0x09), carriage return (0x0d) or space (0x20). Though, when the PHP tag is directly followed by a backslash character, it is no a valid PHP tag anymore and the PHP code is not interpreted, but rather displayed in plain:

user@b0x:/tmp$ cat sample2.php
<?php\necho 1;\n?>

user@b0x:/tmp$ php -f sample2.php
<?php\necho 1;\n?>

Since the PHP cache files are publicly accessible (owa-data/caches/), we can retrieve the base64 encoded serialized data. In order to do this, we need to know the name of the cache file. Though it turned out, that the filename is predictable. If the admin user at least logged in once, the cache file exists. But even if the user never logged in, we can trigger the creation by trying to login with this user. The failed login attempt does also create the cache file.

After calculating the filename, we can easily retrieve the cache file:

user@b0x:~$ curl http://localhost/owa_web/owa-data/caches/1/owa_user/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.php

<?php\n/*Tzo4OiJvd2FfdXNlciI6NTp7czo0OiJ ... YWNoZSI7Tjt9*/\n?>

The base64 encoded data contains all properties of the user within the MySQL database including the username, hashed password, temp_passkey and API key:

user@b0x:~$ echo Tzo4OiJvd2FfdXNlciI6NTp7czo0OiJ ... YWNoZSI7Tjt9 | base64 -d
O:8:"owa_user":5:{s:4:"name";s:9:"base.user";s:10:"properties"; ...
...
... s:4:"name";N;s:5:"value";s:5:"admin"; ...
... s:8:"password";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:60:"$2y$10$vT89Rpbp/kz92SdR2j03luLVFOy7ldaqdyOIpTmFtmp2WGGOmFIwS"; ...
... s:12:"temp_passkey";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:32:"d7d8c0d6c8da08adea071fa80220b121"; ...
... s:7:"api_key";s:5:"value";s:32:"2f37b9889afb326b2ae21a5eb45933bd"; ...
...
}s:12:"wasPersisted";b:1;s:5:"cache";N;}

The password is hashed and thus needs to be cracked before being valuable. Though the temp_passkey can directly be used to set a new password for the user via the base.usersChangePassword action. After changing the password, an attacker can successfully login and now has full admin privileges.

PHP file write

The second vulnerability is a PHP file write, which requires admin privileges.

By browsing to the Settings tab in the admin interface, different options can be set:

When updating the settings (action base.optionsUpdate) the controller owa_optionsUpdateController is triggered, which is responsible for persisting the new settings:

class owa_optionsUpdateController extends owa_adminController {

    ...

    function action() {

        $c = owa_coreAPI::configSingleton();

        $config_values = $this->get('config');

        if (!empty($config_values)) {

            foreach ($config_values as $k => $v) {

                list($module, $name) = explode('.', $k);

                if ( $module && $name ) {
                    $c->persistSetting($module, $name, $v);
                }
            }

            $c->save();
            owa_coreAPI::notice("Configuration changes saved to database.");
            $this->setStatusCode(2500);
        }

        $this->setRedirectAction('base.optionsGeneral');
    }

    ...

The selected settings are sent via the config parameter ($config_values). As we can see there is no restriction on the settings we can define. Although only a few settings are displayed in the GUI on the Settings tab, we can change all other settings by crafting a corresponding POST request.

Two of these settings are used within the owa_error class, which is responsible for logging. When the application is running in production mode, the method createProductionHandler is called to initialize the logfile by calling make_file_logger:

...
class owa_error {

    ...

    function createProductionHandler() {

        ...
        
        $this->make_file_logger();

        ...
    }
    
    ...
    
    function make_file_logger() {

        $path = owa_coreAPI::getSetting('base', 'error_log_file');
        //instantiate a a log file
        $conf = array('name' => 'debug_log', 'file_path' => $path);
        $this->loggers['file'] = owa_coreAPI::supportClassFactory( 'base', 'logFile', $conf );
    }

As we can see the file path of the logfile is determined by calling the owa_coreAPI::getSetting method. The setting being used is base.error_log_file. We can control this value and thus control the file path, where the log is being written. This means that we can also set the file path to be a PHP file. In order to execute arbitrary PHP code, we only need to control some data, which is written to the logfile.

By default the application is only logging events with the log level OWA_LOG_NOTICE. These messages are mainly static and do not contain any data, which can be controlled by an attacker. Thus let’s have a look at the logging mechanism.

When a message is supposed to be logged, the method logMsg is called with the corresponding priority (log level):

    function logMsg( $msg, $priority ) {

        ...
        
        // check error priority before logging.
        if ( owa_coreAPI::getSetting('base', 'error_log_level') <= $priority ) {

            $dt = date("H:i:s Y-m-d");
            $pid = getmypid();
            foreach ( $this->loggers as $logger ) {

                $message = sprintf("%s %s [%s] %s \n", $dt, $pid, $logger->name, $msg);
                $logger->append( $message );
                ...

The code snippet above shows that before the message is actually logged, the value of base.error_log_level is checked. If the $priority value is greater or equal than the current base.error_log_level, the message is logged. We can also control the value of base.error_log_level. By setting this value to OWA_LOG_DEBUG = 2, the messages being logged are very verbose and even include each POST parameter send to the application. This way an attacker can easily inject arbitrary PHP code into the logfile. By accessing this logfile, which was set to a PHP file, the injected code is executed resulting in RCE.

Demonstration

The combination of both vulnerabilities can be leveraged by an unauthenticated attacker to gain RCE as the following demonstration shows:

Patch and Mitigations

Within this section we want to determine how both of these vulnerabilities can be mitigated and point out a few general aspects regarding secure coding.

Only shortly after my notification Peter provided a patch for the file cache vulnerability (CVE-2022-24637). This patch tackles the vulnerability on multiple layers. At first single quotes for the cache_file_header and cache_file_footer are replaced by double quotes preventing the leak, which was the actual cause of the vulnerability. Though the patch contains further hardening. The secret OWA_AUTH_KEY value is included into the cache filename calculation. Without knowing this value, it is far harder to determine the name of the cache file. Also the owa_user entity was removed from the cache at all. Thus there is no cache file for user entities. Not part of the above commit, but also a crucial mitigation is to prevent files from being accessed via the webserver, which are not supposed to be accessed directly. One way to achieve this is to move all files out of the webroot, which are not supposed to be accessed via the webserver. The problem with this solution is that the website administrator needs file system access beyond the webroot. Depending on the hosting or security model this might not be intended. Another way is to use a webserver specific restriction mechanism like an .htaccess file, when an apache webserver is in place.

The second vulnerability is quite common for PHP. File writes should really be handled carefully. If an attacker can control what is being written to a file, RCE is not far away. Even if the file does not reside within the webroot or does not have a .php extension, an additional local file inclusion (LFI) vulnerability, which might be useless on its own, turns this into easy RCE. In this case a restriction should enforce that the logfile can only be a .txt file. Also this file should not reside within the webroot, if viable. This restriction must be enforced, if there is the necessity of making the file path configurable. Generally the settings, which are configurable via the webinterface, should be very limited. A more adequate way to make sensitive settings configurable is by defining constant values in a config file. These can only be changed by actually altering the config file (which in the best case should only be possible to the legitimate website administrator).

Summing up, the key takeaways are:

  • Mind the difference between single and double quotes in PHP
  • When hashing values, add a secret pepper / salt value
  • Do not store any files in the webroot, which are not supposed to be accessed via the webserver, or employ restrictions to prevent direct access to these files
  • Carefully restrict file writes (content, file path and extension)
  • Restrict which settings are configurable via the webinterface

Conclusion

The article described the chaining of two vulnerabilities in OWA, which can be exploited by an unauthenticated attacker to gain RCE on the underlying webserver. Also we took a look at how these vulnerabilities can be mitigated and pointed out a few general aspects regarding secure coding.

Thanks again to Peter for professionally handling these issues and quickly providing a patch.

Timeline
01 February 2022 – Vendor Notification
01 February 2022 – Vendor Acknowledgement
02 February 2022 – Vendor Patch
18 March 2022 – Public Disclosure