Writing Exploits For Exotic Bug Classes: unserialize()


Tired of all the same re-hashed articles on exploiting the SQL injections, XSS, XSRF, File Inclusion, basic overflows, and more? I am, so I started writing more interesting material. This started with “Exploiting Modern Userland Linux Applications”. A tale on bypassing modern exploit mitigations and real adventures in x64 world without turning off ASLR. Now I’m here to present a series called “Writing Exploits For Exotic Bug Classes.” This will be adventures in exploiting common bug classes that aren’t talked about as often. They will be co-presented with exploits for modern applications (2012-2013), possibly 0day assuming legal possibilities with a certain vendor, an easier to consume slide deck and, as always, a weaponized Python exploit. Enjoy!

Part 1.) Unserialize

The first part of this series will be on writing exploits for unserialize() vulnerabilities in PHP. This bug class is really Stefan Esser’s baby and a lot of groundwork has been covered since 2009 by him and his “Shocking News in PHP Exploitation” work. Without further ado, let’s get into it.

What is Serialize and Unserialize?

Serialize() generates a string representation for PHP values. It is used to store or pass around these values to be used later. Serialize handles all PHP values except for resources (references to external resources). On the other hand, unserialize() takes this string and restores the PHP values.
The Issue and the Warning

Unserialize() experiences typical user-controlled injection issues much like SQLi and XSS. On the unserialize page, we see the warning:

“Do not pass untrusted user input to unserialize(). Unserialization can result in code being loaded and executed due to object instantiation and autoloading, and a malicious user may be able to exploit this. … ”

Learning Exploitation The Hard Way: Abusing Real Applications

In order to appropriately guide someone through exploitation, we’ll need a real vulnerability under a realistic scenario. To do this, we’ll be taking a look at Invision Power Board 3.3.4. According to CVE-2012-5692, Invision Power Board 3.1.x to 3.3.4 inclusive is vulnerable to a user-controlled unserialize() string. The description about the vulnerability on http://secunia.com/advisories/51104 is:
“The vulnerability is caused due to the admin/sources/base/core.php script using the “unserialize()” function with user controlled input. This can be exploited to e.g. create a cache file with arbitrary PHP code using the “__destruct()” method of the “dbMain()” class via a specially crafted serialized object sent in a “Cookie” header.

Successful exploitation may allow execution of arbitrary code, but requires the “short_open_tag” to be enabled.”

By the time this is done, you should get a real example of why you shouldn’t take ‘restrictions’ imposed to heart.

The Vulnerability

We will find the vulnerability in admin/sources/base/core.php starting in class IPSCookie, public function get(), starting on line 4012:

static public function get($name) [1]
	/* Check internal data first */
	if ( isset( self::$_cookiesSet[ $name ] ) )
		return self::$_cookiesSet[ $name ];
else if ( isset( $_COOKIE[ipsRegistry::$settings['cookie_id'].$name] ) ) [2]
		$_value = $_COOKIE[ ipsRegistry::$settings['cookie_id'].$name ]; [3]

	if ( substr( $_value, 0, 2 ) == 'a:' ) [4]
			return unserialize( stripslashes( urldecode( $_value ) ) ); [5]
			return IPSText::parseCleanValue( urldecode( $_value ) );
	return FALSE;

The function get($name) at [1] gets a cookie value from given $name. It is checked at [2] to see if the cookie exists, then set as $_value at [3]. If the cookie starts with a: (serialized array) at [4] it will urldecode(), stripslashes(), and unserialize the $_value at [5].
The Vulnerable Path

Now we need to know what is required to reach the vulnerable path and trigger the vulnerability. To do this we’ll need to search the code to find what reaches IPSCookie::get. Simple grep comes to the rescue here:

grep IPSCookie::get -r ./

There’s quiet a few entries here with one mentioning directly controlled cookie values:

$cookie['session_id']   = IPSCookie::get('session_id');
$cookie['member_id']    = IPSCookie::get('member_id');
$cookie['pass_hash']    = IPSCookie::get('pass_hash');

If you read through this file (and given the file name), we find this runs in the __construct class and is triggered when building any user and guest session. This means that login is not required and construction of a guest session will trigger the path to our vulnerability.

Understanding The Serialized String

When initially looking at a PHP serialized string, or any serialized forms, things look completely foreign. However, it is a very easy format when you understand just what is going on. When we send our payload, it looks like:


After the URL decoding process:


So this begs the question of what exactly is going on here? When looking again at php.net/serializethe first comment reveals what some of these values mean:

User: egingell at sisna dot com
Anatomy of a serialize()'ed value:



b:value; (does not store "true" or "false", does store '1' or '0')


a:size:{key definition;value definition;(repeated per element)}

O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}

String values are always in double quotes
Array keys are always integers or strings
"null => 'value'" equates to 's:0:"";s:5:"value";',
"true => 'value'" equates to 'i:1;s:5:"value";',
"false => 'value'" equates to 'i:0;s:5:"value";',
"array(whatever the contents) => 'value'" equates to an "illegal offset type" warning because you can't use an
array as a key; however, if you use a variable containing an array as a key, it will equate to 's:5:"Array";s:5:"value";',
attempting to use an object as a key will result in the same behavior as using an array will.

With this understanding, let’s break apart the payload for the exploit:

a:1:{                      | Array with one element
O:15:"db_driver_mysql":1:{ | Object, 15 characters long, name "db_driver_mysql", takes one property
s:3:"obj";                 | String, 3 characters long, value "obj"
a:2:{			   | Array with two elements
s:13:"use_debug_log";	   | String, 13 characters long, value "use_debug_log"
i:1;                       | Integer value of 1
s:9:"debug_log";           | String, 9 characters long, value "debug_log"
s:12:"initdata.php";}}}    | String, 12 characters long, value "initdata.php", end two arrays and one object

This should be enough to understand the values in the serialized string.

Deeper Look Into PHP Unserialize Process

Is there anything not mentioned? Taking a look at the php source code at ext/standard/var_unserializer.c in the php_var_unserialize() function starting at line 476:

switch (yych) {
case 'C':
case 'O':       goto yy13;
case 'N':       goto yy5;
case 'R':       goto yy2;
case 'S':       goto yy10;
case 'a':       goto yy11;
case 'b':       goto yy6;
case 'd':       goto yy8;
case 'i':       goto yy7;
case 'o':       goto yy12;
case 'r':       goto yy4;
case 's':       goto yy9;
case '}':       goto yy14;
default:        goto yy16;

Comparing this to the above php.net comment, there’s a few that were not mentioned and a couple of extra casings. To spare you from the goto maze (ends at yy98), you can look at the php.js projects version of unserialize for some of these extra values. While it doesn’t have everything, it certainly is easier to follow some of the more confusing logic in the php source. http://phpjs.org/functions/unserialize/
So what do we have that wasn’t mentioned on the php.net site?

case 'O':	| O: can be followed by a + with O:+
case 'R':	| R:#; used as a reference number (fun with self-referencing)
case 'd':	| double/float and/or convert string to int then double/float
ONRS each have upper and lower case forms

New In PHP: Automated To Attack

With a solid understanding of the PHP serialized format, let’s see that warning again:

“Unserialization can result in code being loaded and executed due to object instantiation and autoloading”

So now to define some things. Instantiation is when a class becomes an object by creating an instance of the class in memory. So when you actually call new class(), class() is now an instantiated object.

Autoloading is where the real beast comes into play. This allows a developer to call instantiated objects from any location. This was born from OOP style of PHP. By allowing this autoload format, the developer is not required to require(_once)/include(_once) multiple libraries on every page they might be needed. Instead, you’d strap a page to autoload these objects and use them at will. There are functions that allow you to manually autoload and those that will do it automatically. Note that this can lead to vulnerabilities in themselves.

As a developer, you can use the magic function __autoload or use spl_autoload_register to manually autoload libraries. For more information on these functions and how to use them please visit http://php-autoloader.malkusch.de/en/.
A bit more interesting is the amount of functions that will initiate the autoloader for you. These functions are:


As said, these can introduce vulnerabilities in and of themselves. For more information on this research and where this list was pulled, please visit http://hakre.wordpress.com/2013/02/10/php-autoload-invalid-classname-injection/.
Taking Advantage of “Magic”

Alright wizards, you should now understand the bulk of the issue. However, the unserialization process just turns a serialized string into PHP values. One can’t necessarily call functions to use these values, unless unserialize_callback_func is set. They’re still just values, so how do we take advantage of these values? We abuse something called magic functions.

Magic functions are functions that are automatically executed when certain events/actions happen. This allows us to go into a function with our PHP values present. Taking a look at http://php.net/manual/en/language.oop5.magic.phpyou should see a nice list of magic functions. These are __construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __toString(), __invoke(), __set_state() and __clone(). So when do these get called?

Gets called when a new object is created.
Called when there are no more references to an object or when an object is destroyed manually. Also, when the script ends, all destructors are called. This is called even if exit()/exit was given, unless it's given inside the destructor.
Happens when attempting to invoke an inacessible method in an object context.
	Ex: $obj = newMethod; $obj->inaccessible();
Same as __call, but in a static context.
	Ex: Method::inaccessible();
Triggered when writing data to inaccessible properties.
Triggered when reading data to inaccessible properties.
When isset() or empty() is called on inaccessible properties.
When unset() is used on inaccessible properties.
This function is executed prior to serialization to give extra time to finish possibly pending values to be serialized. Usually just returns an array of variable names for the object.
Unserialize() triggers this to allow reconstruction of resources to be used in conjunction with or needed for the unserialized PHP values.
This triggers when you treat a class like a string. Prior to PHP 5.2.0 only echo or print would trigger this. Now it can happen in any string context like printf.
	Ex: echo $class;
Hit when trying to call an object like a function.
	Ex: $class('hi');
When a class is exported by var_export, this is executed. This will only contain an array of the exported class properties.
The function ran when cloning an object.

As seen, most of these are called on invalid access issues and are made to be used to fix the access issues. Given that, we do have some generic useful magic to check out first. That is __wakeup and __destruct.

The __wakeup() magic function is, again, called when unserialize() is called. It can be used to set and call initiation functions for the PHP values. According to http://www.php.net/manual/en/language.oop5.magic.php#object.wakeup:
“The intended use of __wakeup() is to reestablish any database connections that may have been lost during serialization and perform other reinitialization tasks.”

public function __wakeup()
private function connect()
$this->link = mysql_connect($this->server, $this->username, $this->password);
mysql_select_db($this->db, $this->link);

This style could give us some options, but what about __destruct? The __destruct() magic function can be used to fix and log interesting data (like exceptions caught and issuing a destructor to log the bad data). This is also likely to cleanup saved data like removing log files (unset($file)).

Aside from the generically useful magic functions, there could be vulnerability specific related issues that can extend the amount of useable magic functions. A case study with a presentation format for such an issue can be seen at http://prezi.com/5hif_vurb56p/php-object-injection-revisited/.
Finding The Right Magic

With our knowledge of magically delicious functions, now to find one to abuse. This requires knowing the attack surface. Aside from the product source code, what frameworks are used by the web server, any extra plugins/addons used, conjunction software, and etc. We will just be sticking with the Invision Power Board source code for right now, but it’s good to know what else can be (ab)used.

With IPB code handy and grep readily available, what magic functions are available to us? A simple grep __magic -r ./ will work for now. Obviously, better grep one-liners can be made to remove useless entries, comments, regex for all magic functions, and more. So what do we have with IPB code?

xml_parser_free( $this->parser );
if( is_file( $this->temp_file ) ) { @unlink( $this->temp_file );
	*With this, we have the ability to remove an arbitrary file within appropriate permission context.
if( $this->mail_method == 'smtp' ) { $this->_smtpDisconnect();
@ftp_close( $this->stream );
if ( get_class( $this ) != 'output' ) { return
	*This will return the name of the class.
if( $this->connection_id ) { ldap_unbind( $this->connection_id );
public function __destruct()
		$this->return_die = true;
		if ( count( $this->obj['shutdown_queries'] ) )
			foreach( $this->obj['shutdown_queries'] as $q )
				$this->query( $q );
		$this->writeDebugLog( '{end}', '', '' );
		$this->obj['shutdown_queries'] = array();

Aside from just being able to remove an arbitrary file, ips_kernel/classDb.php has an interesting __destruct. Depending on shutdown stage, the query() function with controlled $q is available. WriteDebugLog sounds like an interesting function, let’s look at some of that relevant code:

public function writeDebugLog( $query, $data, $endtime, $fileToWrite='', $backTrace=FALSE )
	$fileToWrite = ( $fileToWrite ) ? $fileToWrite : $this->obj['debug_log'];
else if ( $query == '{end}' AND ( $this->obj['use_debug_log'] AND $this->obj['debug_log'] ) )
			$_string  = "\n==============================================================================";
			$_string .= "\n=========================        END       ===================================";
			$_string .= "\n========================= " . $_SERVER['PHP_SELF'] . '?' . $_SERVER['QUERY_STRING'] . " ===================================";
			$_string .= "\n==============================================================================";
if ( $_string AND $FH = @fopen( $fileToWrite, 'a' ) )
			@fwrite( $FH, $_string );
			@fclose( $FH );

This is looking better and better! With our ability to control PHP values, cause a magic __destruct function, we seem to have a controlled filename and a semi-controlled write ($_SERVER[‘QUERY_STRING’]).

Putting The Pieces Together

So, is this autoloaded and can we reach the destructor? Let’s follow the flow:

require_once( IPS_ROOT_PATH . 'sources/base/ipsRegistry.php' );
	require_once ( IPS_KERNEL_PATH . 'classDb' . ucwords($db_driver) . '.php' );
	$classname = "db_driver_" . $db_driver;
	self::$dbObjects[ $key ] = new $classname;
		*At this point, db_driver_mysql is now instantiated and an available object.
		require_once( dirname( __FILE__ ) . '/classDb.php' );
			*Now classDb classes are available by the prior instantiated class.

Let’s Get Serial!

Now that we know the vulnerability, how to reach the vulnerable path to trigger the vulnerability, what to abuse, and how it all works together, we just need to build the serialized payload. Let’s do that now.

First, the IPB source code has a requirement that must be filled. If you remember the vulnerability code, there is a check for the string to start with an array:

if ( substr( $_value, 0, 2 ) == ‘a:’ )

Simple enough starting point, a:1:{.

db_driver_mysql is the instantiated object that eventually reaches to dbMain for its __destruct. Let’s fill in that object value. a:1:{O:15:”db_driver_mysql”:1:{.

In order to fullfill our controlled filename and semi-controlled write, we need $obj and an array for two elements. One to boolean enable use_debug_log and another to set debug_log filename:

$fileToWrite = ( $fileToWrite ) ? $fileToWrite : $this->obj['debug_log'];
( $this->obj['use_debug_log'] AND $this->obj['debug_log'] )


Patching Blues

A patch was released by IPB later to include an extra regex check on the controlled serialized object. This check was:

else if ( ! preg_match(‘/(^|;|{|})O:[0-9]+:”/’, $serialized ) )

If you remember right, when looking deeper into the PHP unserialize process, O: doesn’t have to be followed just by a number. It can be followed by a +. The new exploit to bypass this patch is a one character change:


Show Stopper Craziness

With our payload ready, what do we write and what do we write to? The combination of $_SERVER[‘QUERY_STRING’] with the surrounding equal signs in the semi-controlled string presents an annoying show stopper for a lot of control. $_SERVER[‘QUERY_STRING’] dies on whitespace characters. These characters, according to trim() page, are:

" " (ASCII 32 (0x20)), an ordinary space.
"\t" (ASCII 9 (0x09)), a tab.
"\n" (ASCII 10 (0x0A)), a new line (line feed).
"\r" (ASCII 13 (0x0D)), a carriage return.
"\0" (ASCII 0 (0x00)), the NUL-byte.
"\x0B" (ASCII 11 (0x0B)), a vertical tab.

The equal signs are the next stage to destroy what could’ve been a beautiful beast. With a controlled filename for fopen(), one could potentially abuse stream filters to decode encoded data and get around the space requirement. Take a look at conversion filters at http://php.net/manual/en/filters.convert.php for more information. Of course, both of these die with wild equal signs. convert.base64-decode will die when an equal sign is in the middle of the string. Next, convert.quoted-printable-decode will die when an equal sign is not followed by a hex representation. So == will cause it to die. While line-break-chars might be the savior of the injection, I was not able to get it to function with the given streams.
Sticking It To The Show Stopper

Regardless of the aforementioned issues, there is still a lot of room to maneuver. As the advisory stated, short tag injections are possible. So this will work:


While this is enabled by default in php.ini given the php source code, my test XAMPP Windows server had it disabled. Plus, it’s always good to find a more universal solution in case the time comes.

I tried multiple injections with comments. For example starting a comment like <?php/* and ending it before next controlled injection. While online interpreters were freely working with this, real environment was not so happy and forced me to use a space. Of which is broken by QUERY_STRING whitespace restrictions. No luck there.

We could do operating system dependant injections. The write can be given a full path and append data outside of webroot and we could append or create shell/batch scripts contextually to server permission level. Write to a startup/restart script and stomp the box with a bad append. These scripts will ignore the characters as invalid commands and we could just wrap the commands like $(ls) in linux. However, again this is not universally friendly and we want a more universal solution.

After brainstorming with DiabloHorn and doing all sorts of stupid things, we’ve come up with a solution! As of PHP 5.4.0, released March 2012 (after vulnerability discovered late 2012), something exciting was released. According to the changelog at http://php.net/ChangeLog-5.php, there’s this savior line:
“<?= is now always available regardless of the short_open_tag setting.”

So even if short_tag_open was not enabled in php.ini, we can still create an injection that doesn’t require spaces! For example:


Other Injection Possibilities?

As it must be mentioned, there is still the possibility of the server using the Zend Framework, or at least an important piece of it. This is the most universally known de-facto __destruct style payload everyone copy+paste’s. This includes Metasploit, Vupen, Immunity, and many unserialize exploits in the wild that don’t just remove arbitrary files. It takes advantage of Zend Framework’s Zend_Pdf_ElementFactory_Proxy object and is certainly powerful and awesome. There’s no reason to regurgitate how it works, even if I could somehow explain better than Esser’s presentation. By now you should be able to follow through with no problem. Please read the following if you are interested:

In order to use this exploit, a full path is required for where to create the file. This means a full path disclosure is needed. Normally this is not an issue, and with our current unserialize control, is not an issue here.

I found an FPD by just toying with arrays (several functions don’t expect an array) and within a minute out popped an FPD. The following is one way to get the full path:

index.php?app[]=members&module=profile&section=dname&id=1 Warning: trim() expects parameter 1 to be string, array given in C:\xampp-portable\htdocs\invision\admin\sources\base\ipsRegistry.php on line 1774

Even without a custom FPD (or it gets signatured later on when this is released), between our controlled $_SERVER[‘QUERY_STRING’] and the equal sign is a sole space. By injecting just a <php, we will trigger a T_IS_IDENTICAL error that also reveals a full path. So we have a universal FPD even if set_error_reporting(0); is done.

When All Else Fails

So, let’s say you need something universal that isn’t OS specific, Zend Framework is not used, no other attack surface increase with frameworks/plugins are used, short tags not enabled, and PHP < 5.4.0. Now what can we do?

Can’t use XML External Entities because of show stopper. Well, there’s still the possibility of html injection to steal tokens, perform xsrf, and etc. There’s just one problem. That is most of the files are PHP and either don’t end their PHP or call exit(); This either gives an error or doesn’t run. So the goal is to find any kind of useful injection locations. The all powerful grep to the rescue once more!

for i in $(grep "?>" -r ./ -l --exclude=*.png); do if tail -n 5 $i | grep "?>" -q; then echo $i; fi; done

After all of those files, only two are either valid or universally acceptable injection locations. They both point to the same place and are initdata.php and conf_global.php. Both of these are executed upon viewing index.php. Now, the fopen is in append, so the data gets written to the bottom of the PHP pages, which will be shown and injected at the top of the actual page. This means it’s a bit more obvious because of the ASCII show stopper characters. This will also break layout in the IE browser. If you really want to use it and not be obvious about the injection (defacers need not worry here), you’ll need to dynamically grab the page on view, flush the current page content, and use document.write(String.fromCharCode()) to rewrite the page. You could also use some CSS magic and hide/layer over the visible injection area or just simply redirect to a mirror copy on your own server! It’s all limited to your html injection imagination.

Final and Misc. Ideas

Sometimes all of this craziness simply isn’t needed depending on the vulnerability. Based on location and logic flow, you could simply abuse how the unserialized objects are used. For example, unserializing data for authentication. While this is not a likely scenario, some bugs are simply ‘special’ in application.

Because you can easily mess with the flow of PHP interpreter and data types, interpreter issues can become a problem. If you looked at the source code in ext/standard/var_unserializer.c in function php_var_serialize() you’d of noticed the use of a couple reference counters. This can lead to much more interesting and awesome exploits like use after free’s as seen with Stefan Esser’s SPLObjectStorage UAF:

Generally these are much more specific to PHP versions and requires extra hurdles to use, as seen with the SPLObjectStorage UAF.

The Exploit

The exploit provided takes advantage of every style documented here minus the specific UAF. It is written in Python and some tricks are used for generic IDS/IPS bypasses like accepting base64 encoded commands via the Accept HTTP header, using HEAD for the injection, and allowing the option to not use the new FPD.


Unfortunately I do not know everything… If I got something wrong, something needs to be improved, PLEASE let me know! I give this out free to help people, if something is wrong then it’s not helping and I need to know in order to make the material honest and useful. Any questions, comments, hate mail and more to tborland1@gmail.com. Hope you enjoyed!

Contributions By Tyler Borland (TurboBorland)
Exploiting Exotic Vulnerabilities: Unserialize (ODP presentation)

About the Author

Stephen Coty - Chief Security Evangelist at Alert Logic

Stephen Coty

Stephen Coty originally joined Alert Logic as the head of the Threat Research team, where he led the effort to build threat content and deliver threat intelligence. He later became the Chief Security Evangelist for the company. Prior to joining Alert Logic, Coty was the Manager of Cyber Security for Rackspace Hosting, and has held IT positions at multiple companies, including Wells Fargo Bank, Applied Materials, Stanford Medical Center and The Netigy Corporation. He has been in the Information Technology field since 1993. Research has been his primary focus since 2007.

@StephenCoty | Articles: 14