A very common and critical vulnerability in PHP applications is PHP Object Injection. This blog post explains how they work and how they can lead to a full site takeover by remote attackers.
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 is serialized
The above example creates a new object and then produces the following serialized string representation of this object:
The resulting serialized string:
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
8 separated 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
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:
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.
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 is unserialized again:
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.
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:
Here, user input is directly passed to
unserialize(). The next section will detail how this can be exploited.
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(), he could still exploit the vulnerability with the following serialized payload:
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.
Although passing user input to unserialize() is highly discouraged, such attacks still happen all the time. To name a few examples, we detected the following critical PHP Object Injections:
- Pydio unauthenticated Remote Code Execution
- WooCommerce Privilege Escalation
- PrestaShop Remote Code Execution
- Pydio 8.2.1 Unauthenticated Remote Code Execution
- Shopware 5.3.3: PHP Object Instantiation to Blind XXE
- A Salesmans Code Execution: PrestaShop 184.108.40.206
- CTF Writeup: Complex Drupal POP Chain
- Breaking Into Your Company's Internal Network - SuiteCRM 7.11.4
- phpBB 3.2.3: Phar Deserialization to RCE
- What is Phar Deserialization
- How security flaws in PHP's core can affect your application
- Why mail() is dangerous in PHP