PHP Serialization
PHP provides a mechanism for storing and loading data with PHP types across multiple HTTP requests. This mechanism boils down to two functions: serialize() and unserialize(). This may sound complicated but let’s look at the following easy example:
<?php $object = new stdClass(); $object->data = "Some data!"; $cached = serialize($object);
The above example creates a new object and then produces the following serialized string representation of this object:
O:8:"stdClass":1:{s:4:"data";s:10:"Some data!";}
The syntax of the serialized string is relatively easy to understand: The O stands for the type of the serialized string. In this case the O maps to a PHP object. The 8 seperated by the colons represents the length of the name of the class the object is an instance of. In this case, the serialized object is an instance of the PHP built in stdClass.
The following 1 represents the number of properties the serialized object contains which are stored within curly brackets. Each property is stored as a serialized string representing the property name, a semicolon and a serialized string representing the value.
While the property name is always a serialized PHP string, the value can be of any type: arrays, integers, strings, objects and NULL are the most common ones.
In this example, there is only one property. It has the name data and has the value Some data!, which is a PHP string (s) of length 10.
PHP Deserialization
The reason I touched on PHP serialization syntax is to make it easier to understand what happens when you call unserialize() on a serialized string.
<?php $object = unserialize('O:8:"stdClass":1:{s:4:"data";s:10:"Some data!";}'); echo $object->data;
PHP will parse the serialized string with the logic depicted above and create an instance of an stdClass with the properties given in the serialized string.
The reason developers do this is to easily and effectively store PHP data across requests, e.g. in caches or databases. A user session might be implemented as an instance of a class UserSession. This session object can easily be stored as a serialized string in the $_SESSION superglobal of PHP and be unserialized when needed.
Magic Methods and Object Injections
The LoggingClass declared in the example above takes two parameters in the constructor: A filename to write to and the file contents. The magic method __destruct() then actually flushes the log and writes it to the filename passed to the constructor. Note that the __destruct() method is called automatically for each PHP object of the LoggingClass at the end of the PHP code execution.
Even if an attacker would be able to control the arguments passed to the constructor, he probably would not be able to exploit the vulnerability for the simple reason that a .log is appended to the filename. If this would not happen, the attacker could simply set the filename to shell.php and set the content to some arbitrary PHP code.
However, if an attacker supplied the following serialized string to the call to unserialize() on line 18, he could still exploit the vulnerability:
O:12:"LoggingClass":2:{s:8:"filename";s:9:"shell.php";s:7:"content";s:20:"<?php evilCode(); ?>";}
Here, the filename property is set to shell.php. The constructor of the class is not called during deserialization, the object was already instantiated and is available in serialized form. However, the destructor is going to be called at the end of execution and it’s using the object’s properties. Namely, the destructor will call file_put_contents() on the filename and content property that can be edited by the attacker by modifying the serialized string. This allows an attacker to inject an object into memory with the filename property set to shell.php which will then create a PHP backdoor on the server.
There are also further ways for exploitation. For example, the altered properties could be used to call another method of an object. An attacker could then control the class of that method call and defer the control flow. Such payloads are called property-oriented programming.