PHP Serialization Recap

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:

A PHP object being ‘serialized’

1
2
3
4
<?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:

The resulting ‘serialized’ string

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

The serialized string being ‘unserialized’ again

1
2
3
<?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.

Dangers of Unserialize

Although this feature is very effective and easy to use, it does introduce potential security issues, to be exact when user input is passed to unserialize(). This can in fact lead to Remote Code Execution. For one, the PHP interpreter had many low-level security issues in this built-in function that could be exploited. But also, depending on the code base of the affected application, there are other ways for attackers. Let’s have a look at the following PHP code:

The serialized string being ‘unserialized’ again

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php
class LoggingClass {

    function __construct($filename, $content) {
        // add .log to the filename so we are really creating a log file!!
        $this->filename = $filename . ".log";
        $this->content = $content;
    }
    
    // This method is executed for each object at the end of the PHP execution
    function __destruct() {
        // flush the logs
        file_put_contents($this->filename, $this->content);
    }

}

$data = unserialize($_GET['data']);

Here, user input is directly passed to unserialize(). The next section will detail how this can be exploited.

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:

A serialized payload

1
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 and we documented examples for Drupal and ExpressionEngine

Object Injections in Real World

Although passing user input to unserialize() is highly discouraged, such attacks still happen all the time.

To name a few examples, RIPS detected the following critical PHP Object Injections: