PHP object injection vulnerability in QRadar Forensics web application

Abstract

A PHP object injection vulnerability was found in the QRadar Forensics web application. The vulnerability can be triggered via a specially crafted cookie and can be used by an authenticated attacker to execute arbitrary commands. The commands will be executed with the privileges of the Apache system user.

See also

CVE-2020-4271 6189651 - IBM QRadar SIEM is vulnerable to PHP object injection (CVE-2020-4271)

Tested versions

This issue was successfully verified on QRadar Community Edition version 7.3.1.6 (7.3.1 Build 20180723171558).

Fix

IBM has released the following versions of QRader in which this issue has been resolved:

Introduction

QRadar is IBM's enterprise SIEM solution. A free version of QRadar is available that is known as QRadar Community Edition. This version is limited to 50 events per second and 5,000 network flows a minute, supports apps, but is based on a smaller footprint for non-enterprise use.

A PHP object injection vulnerability was found in the QRadar Forensics web application. The vulnerability exists in the DataSetModel class and can be triggered via a specially crafted cookie. By exploiting this issue it is possible for authenticated users to instantiate arbitrary PHP objects. It has been confirmed that a POP chain exists that can be used to execute arbitrary commands. The commands will be executed with the privileges of the Apache system user (generally the nobody user).

Details

The Forensics web application contains functionally to save graph data in cookies. When a graph is viewed that was previously saved, the data will be restored from the cookie value(s). Saving and restoring data is done using PHP object serialization. The serialized data is compressed and encoded with base64 before it is returned as cookie to the user. Deserialization of graph cookies is done in the restore() method of the DataSetModel as is shown in the code fragment below.

/opt/ibm/forensics/html/DejaVu/Reports/DataSetModel.php:

public function restore($dataKeys, $dsize) {
	if ($dsize == 0)
	// No data
		return null;
	
	$cookieData = '';
	foreach ($dataKeys as $dataKey) {
		if (array_key_exists($dataKey, $_COOKIE)) {
			**$cookieData .= $_COOKIE[$dataKey];**
			// All done, so delete the data cookie.
			setcookie($dataKey, "", time() - 3600);
		} else {
			error_log("MISSING COOKIE '$dataKey'");
			return null;
		}
	}
	
	$sz = strlen($cookieData);
	if ($sz != $dsize) {
		error_log("ERROR: Graph data size incorrect: expected $dsize, got $sz");
		return null;
	}
	
	try {
		**$dataset = unserialize(gzuncompress(base64_decode($cookieData)));**
		return $dataset;
	} catch (Exception $e) {
		error_log("Error deserializing session data: " . $e->getMessage());
		$dataset = null;
	}
	return null;
}

The restore() method is called in the constructor of various chart classes, which all inherit from the BaseChart class. These chart classes are exposed in the /forensics/graphs.php page of the Forensics web application.

/opt/ibm/forensics/html/DejaVu/Charts.php:

abstract class BaseChart extends ParameterizedObject {
[...]
	public function __construct($params=null) {
[...]
        
        $dm = empty($dmodel) ? new DataSetModeler(null) : new $dmodel(null);
        if(array_key_exists('sid',$_GET))
            $dm->setSessID($_GET['sid']);
            
        $dataset = $dm->restore($dataKeys,$dsize);
[...]

It has been confirmed that this vulnerabilty can be used to execute arbitrary commands by sending a specially crafted cookie to the affected web page.

Proof of concept

Execution of arbitrary commands is possible by abusing the License class. The get_license_info() method runs a system command (via the load_license_xml() private method) based on property values that are set in the License object. Using serialization it is possible to set these values to arbitrary values such that when get_license_info() is called it will run the command that was set when the object was deserialized.

/opt/ibm/forensics/html/includes/license.inc.php:

class License {
    private $license_util;
    private $license_file;
	
[...]
	
    private function load_license_xml($use_cache = true, $use_license_file = "") {
[...]
	
        if (is_file($lic_file)) {
            **$run_cmd = $this->license_util . ' ' . $lic_file;**
            $license_cmd_out = **$this->run_command($run_cmd, $retval, $output)**;

However, there is no direct path from a PHP magic method to the get_license_info() method. Therefore the License object needs to be wrapped in another object.

It was found that the simple_html_dom_node can be abused to call call_user_func_array() on user-supplied (deserialized) data. This PHP function can be passed an object and method of that object, which needs to be called. If we would pass it a License object and instruct it to call the get_license_info() method, it is possible to execute arbitrary commands. The call_user_func_array() is called from the outertext() method of the simple_html_dom_node class, which is amongst other locations called from the __toString() magic method.

By wrapping the simple_html_dom_node in a fake dataset class we can cause any of the chart classes to call the __toString() method, which will set of the POP chain. The full POP chain and proof of concept code are provided below.

Questions or feedback?