CiviCRM 5.22.0 - Code Execution Vulnerability Chain Explained

by dennis brinkrolf|

CiviCRM Code Execution Vulnerability Chain Explained

During our vulnerability research on the largest CMS systems we came across CiviCRM last year. It’s an open source CRM plugin for the most popular CMS systems like Wordpress, Joomla, Drupal, and Backdrop. CiviCRM is specifically designed for the needs of non-profit, non-governmental, and advocacy groups, and serves as an association management system. According to CiviCRM, it has been used by more than 11,000 organizations, processed more than 116 million donations, and managed 189 million contacts which makes it an attractive target for cyber criminals.

In our analysis we discovered several critical code vulnerabilities in CiviCRM version 5.22.0. A combination of these vulnerabilities allowed remote attackers to execute arbitrary system commands on any CiviCRM instance running on WordPress and to fully compromise the server and its data. In this blog post we analyze the technical root cause of two different security issues and demonstrate how attackers could exploit these. We reported all issues responsibly to the affected vendor who released multiple security patches to protect all users against attacks.

Impact

During the analysis of CiviCRM 5.22.0, we found a CSRF vulnerability (CVE-2020-36389) that led to a Stored XSS vulnerability. Both vulnerabilities were fixed in CiviCRM version 5.28.1 and 5.27.5 ESR. Additionally, we discovered a Phar Deserialization vulnerability leading to PHP code execution (CVE-2020-36388). The issue was fixed in CiviCRM version 5.24.3 and 5.21.3.

A combination of all vulnerabilities could allow a remote attacker to execute arbitrary system commands on any CiviCRM instance. As a result, the underlying CMS such as WordPress is compromised too. In order to successfully execute the attack, an authenticated administrator is lured to a malicious page that embeds a form that automatically sends a request to the website, including the administrator's cookies, which allows gaining Remote Code Execution capabilities.

For demonstration purposes we’ve created a short video that shows how quick and easy a server is compromised.

Technical Details

In the following, we look at the root cause of two vulnerabilities in the source code of CiviCRM. First we introduce the stored XSS that can be exploited via CSRF. In the next step, we analyse the root cause of a Phar Deserialization vulnerability.

The Attacker’s Entry Point (CVE-2020-36389)

The administration interface of CiviCRM uses the CKEditor, a rich text editor that enables direct editing and writing of configuration files. Here, a run() method is used (see code below) that is called each time the editor is accessed. In line 56, the filename of the current config that will be saved is received via $_REQUEST['present']. In line 63, the save() method is called with attacker-controlled POST parameters. No CSRF tokens are verified, which results in a CSRF vulnerability (CVE-2020-36389). You can find more details about how CSRF attacks work in our LocalStack blog post.

civicrm/CRM/Admin/Page/CKEditorConfig.php

55    public function run() {
56        $this->preset = CRM_Utils_Array::value( 'preset', $_REQUEST, 'default' );
...
62        elseif ( ! empty( $_POST['config'] ) ) {
63            $this->save( $_POST );
64        }

The save() method constructs a config file from the POST parameters passed and saves the resulting config into a JavaScript file. This allows an attacker to write JavaScript files on the target host via CSRF. 

Let’s have a look at how the content of this file is created. In line 110 in the code below, a default file header is prepended to the configuration file and in lines 113 - 129 the content of the configuration file is composed. Thereby each POST parameter name is processed that starts with config_  in line 115. The name and value of the POST parameters are concatenated in line 126 and added to the $config in line 127. In line 130, the saveConfigFile() method is called with the attacker-controlled variable ($config) and partially controlled file name ($this->present).

civicrm/CRM/Admin/Page/CKEditorConfig.php

107    public function save( $params ) {
108        $config = self::fileHeader()
109        // Standardize line-endings
110            . preg_replace( '~\R~u', "\n", $params['config'] );
112        // Use all params starting with config_
113        foreach ( $params as $key => $val ) {
115            if ( strpos( $key, 'config_' ) === 0 && strlen( $val ) ) {
117                $val = json_encode( $val, JSON_UNESCAPED_SLASHES );
124                $pos = strrpos( $config, '};' );
125                $key = preg_replace( '/^config_/', 'config.', $key );
126                $setting = "\n\t{$key} = {$val};\n";
127                $config  = substr_replace( $config, $setting, $pos, 0 );
128            }
129        }
130        self::saveConfigFile( $this->preset, $config );
134    }

An attacker can skip lines 115 - 129 by sending a single POST field consisting of the key config with an arbitrary value, since the key does not start with config_. This allows the attacker to insert any content after the file header. It is important to note that the file header itself consists of a multi-line comment and thus does not cause JavaScript syntax errors which could terminate the execution of the JavaScript.

Let’s have a look at the name of the file to which this content is written. In line 238 of the following code the filename of the configuration file is composed and in line 239 the content of the file is written. The partial attacker-controlled file name has the following format:

crm-ckeditor-$preset.js

The only attacker controlled information in this filename is $preset. A path traversal attack is not feasible since a folder crm-ckeditor- would have to be located above the current directory. Also, the filename would always have the .js extension which is another limitation.

civicrm/CRM/Admin/Page/CKEditorConfig.php

237    public static function saveConfigFile( $preset, $contents ) {
238        $file = Civi::paths()->getPath( self::CONFIG_FILEPATH . $preset . '.js' );
239        file_put_contents( $file, $contents );
240    }

At first glance, this vulnerability does not seem particularly critical. But from an attacker's point of view, any possibility, no matter how small, is sufficient to carry out an attack. In total, the attacker needs two CSRF requests, where the first CSRF request creates an XSS payload within a config file. The second CSRF request permanently loads the previously dropped XSS config file by overwriting the CKEditor default config.

Using the first CSRF request, the attacker creates a configuration file named crm-ckeditor-xss.js. In order to execute the JavaScript code without syntax errors the attacker skips lines 115 - 129 of the save() method as already mentioned above.

In the CKEditor config it is allowed to include another custom configuration file via the customConfig directive. The JavaScript code of the included configuration file is executed directly (CIVI-SA-2020-12). Therefore, the second CSRF request overwrites the default CKEditor configuration and includes the previously created crm-ckeditor-xss.js via a directive which includes the XSS payload. Combining both CSRF requests causes a stored XSS in the entire backend every time the CKEditor is invoked.

Code Execution via Phar Deserialization (CVE-2020-36388)

In this step we explain how an attacker can take further steps to compromise a CiviCRM instance. Through the stored XSS, the attacker is now able to execute JavaScript in the browser of the administrator, which can perform arbitrary actions within the web application.

However, an administrator does not always have the access privileges to features that allow to control the server, it depends on the configuration of the CMS and the server. Therefore in most cases, attackers try to extend their capabilities, e.g. to execute code on the server. In the following, we introduce another vulnerability that can be used by an attacker to escalate their privileges.

We found this code vulnerability in the Badge component (see code below). In line 22 the user controlled variable $img from line 21 is passed to the getImageProperties() method without any further checks.

civicrm/CRM/Badge/Page/AJAX.php

20    public static function getImageProp() {
21        $img = $_GET['img'];
22        list($w, $h) = CRM_Badge_BAO_Badge::getImageProperties($img);
24    }

In line 399 (see code below) the user controlled variable $img is passed directly to the PHP internal function getimagesize(), which is vulnerable to Phar Deserialization. Because an attacker can control the entire string, the attacker is able to deserialize objects via the phar:// wrapper.

civicrm/CRM/Badge/BAO/Badge.php

398    public static function getImageProperties($img, $imgRes = 300, $w = NULL, $h = NULL) {
399        $imgsize = getimagesize($img);
404    }

In order to exploit the Phar Deserialization vulnerability as an attacker, an image must be uploaded to the file system that contains Phar metadata. Then the getImageProp() method can be called via an Ajax request, whereby the img parameter points to the path of the previously uploaded image. We found gadgets in the CiviCRM core that allowed us to execute code. This is a strong primitive for an attacker since some CMS systems do not contain any known gadgets. However the vulnerability can only be exploited as an administrator, which is possible via the stored XSS described above.

All in all, the Phar Deserialization leads to code execution in WordPress, other CMS systems like Joomla prevent deserialization of Phar metadata. But from an attacker's point of view, it would be possible to find a universal approach via the stored XSS that would compromise any underlying CMS.

Patches

CVE-2020-36389 (CSRF on CKEditor Configuration Form):

This security issue is a classic CSRF vulnerability that can be prevented by adding cryptographically secure tokens that are validated for every sensitive request.

CVE-2020-36388 (PHP Code Execution via Phar Deserialization):

A potential solution is to use a custom Phar wrapper which prevents the untrusted deserialization of Phar metadata. A well-known project for this would be the custom Phar wrapper of Typo3. Moreover, with PHP 8.0 the automatic deserialization from the Phar metadata was disabled. In general, it is advised to always check which user can perform which action within a web application. It is also important to pay attention to third-party features.

Timeline

DateAction
2020-02-18We report all issues to vendor
2020-02-22Vendor confirmed the issues
2020-04-15Vendor released patch for Phar Deserialization in version 5.24.3 and 5.21.3
2020-08-19Vendor released patch for CSRF and XSS in version 5.28.1 and 5.27.5 ESR

Summary

In this blog post we analyzed two code vulnerabilities found in CiviCRM (5.22.0), a widely used open source solution for customer relationship management written in PHP. The combination of these two vulnerabilities can lead to a complete takeover of a CiviCRM instance. We’ve evaluated the root causes in the PHP code base and described how to fix them. We reported these vulnerabilities to the vendor who confirmed and fixed the vulnerabilities quickly. We would like to thank the CiviCRM team who quickly released multiple patches after our report. If you are hosting a CiviCRM instance and have not yet updated your installation, we highly recommend to do so now.

Related Blog Posts