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:
- You pass a serialized
Example2
object into the program as the data cookie. - The program calls
unserialize()
on the data cookie. - Because the data cookie is a serialized
Example2
object,unserialize()
instantiates a newExample2
object. -
unserialize()
sees that theExample2
class has__wakeup()
implemented, so__wakeup()
is called. -
__wakeup()
looks for the$hook
property of the object, and if it is not NULL, it runseval($hook)
. -
$hook
is not NULL, and is set tophpinfo();
, soeval("phpinfo();")
is run. - 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!