Intro to PHP Object Injection Vulnerabilities


logo Logo by Colin Viebrock on php.net

Serialization is when an object in a programming language (say, a Java or PHP object) is converted into a format that can be stored or transferred. Whereas deserialization refers to the opposite: it’s when the serialized object is read from a file or the network and converted back into an object.

Insecure deserialization vulnerabilities happen when applications deserialize objects without proper sanitization. An attacker can then manipulate serialized objects to change the program’s flow.

Today, let’s talk about PHP object injections. They are insecure deserialization vulnerabilities that happen when developers deserialize PHP objects recklessly.

Serialization in PHP

To understand PHP object injections, you have to first understand how PHP serializes and deserializes objects.

Serializing

When you need to store a PHP object or transfer it over the network, you use serialize() to pack it up.

serialize(): PHP object -> plain old string that represents the obj

When you need to use that data, use unserialize() to unpack and get the underlying object.

unserialize(): string containing object data -> original object

For example, this code snippet will serialize the object “user”.

<?php
class User {
     public $username; 
     public $status;
 }

$user = new User;
$user->username = 'vickie';
$user->status = 'not admin';

echo serialize($user);
?>

Run the code snippet, and you will get the serialized string that represents the “user” object.

O:4:"User":2:{s:8:"username";s:6:"vickie";s:6:"status";s:9:"not admin";}

Serialized String Structure

Let’s break this serialized string down! First, “O” represents an object.

O:LENTH_OF_NAME:"CLASS_NAME":NUMBER_OF_PROPERTIES:{PROPERTIES}

The object is a kind of “exception”. The following is the basic structure of a PHP serialized string.

For example, “s” represents a string.

s:LENTH_OF_STRING:"ACTUAL_STRING";

“b” represents a boolean.

b:THE_BOOLEAN;

“i” represents an integer.

i:THE_INTEGER;

“d” represents a float.

d:THE_FLOAT;

“a” represents an array.

a:NUMBER_OF_ELEMENTS:{ELEMENTS}

So we can see our serialized string here represents an object of the class “User”. It has two properties. The first property’s name is “username” and has the value “vickie”. The second property has the name “status” and the value “not admin”.

O:4:"User":2:{s:8:"username";s:6:"vickie";s:6:"status";s:9:"not admin";}

If the property “username” is private, the name of the class “User” is added to the variable name.

O:4:"User":2:{s:14:"Userusername";s:6:"vickie";s:6:"status";s:9:"not admin";}

Also, if “username” is protected, “*” is added to the variable name.

O:4:"User":2:{s:11:"*username";s:6:"vickie";s:6:"status";s:9:"not admin";}

Deserializing

When you are ready to operate on the object again, you can deserialize the string with unserialize().

<?php
class User {
     public $username; 
     public $status;
}

$user = new User;
$user->username = 'vickie';
$user->status = 'not admin';

$serialized_string = serialize($user);
$unserialized_data = unserialize($serialized_string);
var_dump($unserialized_data);
var_dump($unserialized_data["status"]);
?>

Unserialize() Under the Hood

So how does unserialize() work under the hood? And why does it lead to vulnerabilities?

What Are PHP Magic Methods?

PHP magic methods are function names in PHP that have “magical” properties. Learn more about them here. For this article, we deal with __wakeup() and __destruct(). __wakeup() will be executed automatically when unserialize() is called on an object. On the other hand, __destruct() is called when there are no more references to an object or when we force its deletion.

Step 1: Object Instantiation

Instantiation is when the program creates an instance of a class in memory. That is what unserialize() does. It takes the serialized string, which specifies the class and the properties of that object. With that data, unserialize() creates a copy of the originally serialized object.

It will then search for a function named __wakeup(), and execute code in that function. __wakeup() reconstructs any resources that the object may have. It is used to re-establish any database connections that have been lost during serialization and perform other reinitialization tasks.

Step 2: Program Uses the Object

The program operates on the object and uses it to perform other actions.

Step 3: Object Destruction

Finally, when no reference to the deserialized object instance exists, __destruct() is called to clean up the object.

Exploiting PHP Deserialization

When you control a serialized object that is passed into unserialize(), you control the properties of the created object. You might also be able to hijack the flow of the application by controlling the values passed into automatically executed methods like __wakeup() or __destruct().

This is called a PHP object injection. PHP object injection can lead to variable manipulation, code execution, SQL injection, path traversal, or DoS.

Controlling Variable Values

One possible way of exploiting a PHP object injection vulnerability is variable manipulation. For example, you can mess with the values encoded in the serialized string.

a:2:{s:8:"username";s:6:"vickie";s:6:"status";s:9:"not admin";}

In this serialized string, you can try to change the value of “status” to “admin”, and see if the application grants you admin privileges.

a:2:{s:8:"username";s:6:"vickie";s:6:"status";s:5:"admin";}

Getting to RCE

It’s even possible to achieve RCE using PHP object injection! For example, consider this vulnerable code snippet: (taken from https://www.owasp.org/index.php/PHP_Object_Injection)

class Example2 {
 private $hook;  

 function __construct(){
     // some PHP code...
 }  

 function __wakeup(){
     if (isset($this->hook)) eval($this->hook);
 }
}

// some PHP code...

$user_data = unserialize($_COOKIE['data']);

// some PHP code…

You can achieve RCE using this deserialization flaw because a user-provided object is passed into unserialize. And the class Example2 has a magic function that runs eval() on user-provided input.

To exploit this RCE, you simply have to set your data cookie to a serialized Example2 object with the hook property set to whatever PHP code you want. You can generate the serialized object using the following code snippet:

class Example2 {
  private $hook = "phpinfo();";
}

print urlencode(serialize(new Example2));

// We need to use URL encoding since we are injecting the object via a URL.

Passing the above-generated string into the data cookie will cause the code phpinfo(); to be executed. Once you pass the serialized object into the program, the following is what will happen in detail:

  1. You pass a serialized Example2 object into the program as the data cookie.
  2. The program calls unserialize() on the data cookie.
  3. Because the data cookie is a serialized Example2 object, unserialize() instantiates a new Example2 object.
  4. unserialize() sees that the Example2 class has __wakeup() implemented, so __wakeup() is called.
  5. __wakeup() looks for the $hook property of the object, and if it is not NULL, it runs eval($hook).
  6. $hook is not NULL, and is set to phpinfo();, so eval("phpinfo();") is run.
  7. RCE is achieved.

Conclusion

If you can control the serialized string that is passed into unserialize(), you can control the parts of the object that are generated. If the object is not sanitized before deserialization, you might be looking at a PHP object injection vulnerability. You can use PHP object injections to achieve variable manipulation, SQL injection, path traversal, DoS, or even RCE!

Vickie Li