Hack The Box Tenet Walkthrough without Metasploit

Tenet is a medium-rated but comparatively easy box, that required a straightforward PHP deserialization exploit to gain a foothold and exploiting a race condition vulnerability to privesc.


We will begin the reconnaissance phase with an all-port Nmap TCP scan.

  • -T4 : Run faster scan
  • -sC : Specifies Nmap to run default scripts
  • -sV : Specifies Nmap to run service and version detection
  • -Pn : Treat all hosts as online (skip host discovery)
  • -p- : Scan all ports
  • -vv : Verbose output
  • -oA : Output all formats

NMAP returns the following scan result.

We have two ports open on the box

  • Port 22: SSH(OpenSSH 7.6p1 Ubuntu)
  • Port 80: Apache httpd 2.4.29

I also ran a quick UDP scan but it didn’t return anything of interest.

  • -vv: Return Verbose Output(Level 2).
  • — reason: Display why Nmap thinks a particular port is open or not.
  • — top-ports=20: Scan only top 20 UDP ports.
  • — version-all: Display version info of open ports.

UDP scan result:


We now know the open ports. We’ll begin our enumeration with port 22 as it has a narrow range of attack vectors.

Port 22

We have OpenSSH 7.6p1 running on a Ubuntu machine. A simple google search reveals that the Ubuntu version is probably Ubuntu Bionic. The version of SSH is not associated with some major vulnerability so we will leave this port for now.

Port 80

We have Apache httpd 2.4.29 running on port 80. The version of Apache doesn’t have any critical vulnerabilities so let’s check the web interface.

We have a default Apache2 webpage. I always like to check for server banner and programming language info with whatweb, you can also check this with Curl.

Whatweb returns the following data.

Whatweb didn’t returned anything interesting so let’s move on and perform a directory bruteforcing. As we have an Apache webserver running we’ll check for PHP files, which are more common on an Apache webserver.

Gobuster returns the following results.

Great. We have two hits.

The users.txt just displayed a success string so nothing interesting there. WordPress is infamous for many security vulnerabilities over the years.

Navigating the website at , we find that the website code seems to be broken and the website is unable to load css files.

Let’s check the source code and hunt for anything of interest.

We get a hostname tenet.htb. Add the hostname to /etc/hosts file.

Opening the URL http://tenet.htb we get the following page.

Navigating around the website we get more pages. The developers seem to be working on some migration program but we do not have enough info to dig deeper into this.

On one of the pages, we get a comment from Neil user regarding the sator.php file and backup. This is a finding, let’s note it down for the time being.

So, the only thing we have from the website is a file name and a potential username. The website is built on WordPress so let’s fire up WPScan.

WPScan might take some time to run so it would be better if you keep it running in the background while you hunt for anomalies and findings.

WPScan returns the following output:

WPScan returned few interesting things:

The first thing I tried attacking was xmlrpc.php. xmlrpc.php is vulnerable to pingback attacks and can allow us to enumerate usernames or perform an internal port scan in the worst-case scenario.

Refer to the following blog post to learn more about xmlrpc.php.

The rest of the things also lead nowhere, so I decided to move on.

WPScan also returned two usernames

  • protagonist
  • neil

We can try to perform bruteforcing WordPress admin console with Hydra but let’s enumerate more before walking that path.

We know a filename sator.php. Opening the file we get a 404 error, so the file doesn’t exist. But what about subdomains? What if sator is actually a subdomain? I used ffuf to hunt for subdomains and confirm my theory.

Add the subdomain to the /etc/hosts file.

  • sator.tenet.htb

Visiting the new subdomain we get the default Apache2 webpage.

The subdomain also contains the users.txt file we found earlier with gobuster.

On opening the sator.php file, we get the following page.

The webpage seems to grabbing data from the users.txt file and updating some sort of database. We still do not have enough info.

What about the backup file? Let’s try to find the backup file stated by Neil user. Unable to find /backup , /backups. How does Linux store backups? .bak?

We get a file sator.php.bak. Download the file with wget as shown.

Open the file in a text editor of your choice. The code is pretty easy to understand. The developer has created a class named DatabaseExport and assign a file name ‘users.txt’ to a variable user_file. There is an update_db() function that prints the lines we saw earlier in the sator.php file and updates the value of the data variable to ‘Success’(The script is only changing the value of the data variable, No database is being actually updated).

The script then has a magic function __destruct() that uses file_put_contents() function to write the value of data variable(Success) into the users.txt and print the ‘Database updated’ string.

Now we know why the users.txt was displaying ‘Success’ string.

Note: Magic functions in PHP are functions that do not require to be called. Magic function __destruct() is the opposite of __construct() function and is called as soon as there are no other references to a particular object, or in any order during the shutdown sequence.

You can read more about magic methods here:

The next lines in the code create an input variable that accepts a GET request as a ‘repo’ argument. The script then sends the value of this input variable into a unserialize() function and creates an instance(app) of class DatabaseExport. The instance calls the update_db() function which prints the string we got earlier in sator.php.

The lines of interest here are where the script is accepting a GET request and passing the value to an unserialize function. Now, what is unserialize, and how to exploit it?

Serialization is the process of converting some object into a data format(JSON or XML) that can be restored when required. JSON is the most widely used format for serializing data.

Deserialization is the reverse of serialization i.e. converting structured data format into an object. In PHP unserialize() function is used for deserializing data. You can read more about deserialization at the following links.

Why is this bad? A website must never deserialize a user input in any case. If exploited successfully an attacker can manipulate serialized objects in order to pass harmful data into the application code. The impact of insecure deserialization can be very severe as it can act as an entry point to the application source code. It allows an attacker to exploit the existing application code that can lead to remote code execution, privilege escalation, arbitrary file access, and denial-of-service attacks.

How to exploit? Well, we can exploit deserialization in multiple ways such as modifying attribute values, supply unexpected data types, injecting arbitrary objects or memory corruption, etc. In our case, we will create a PHP class called DatabaseExport and write some malicious code into it. We can then serialize the entire class and pass it to the repo argument. This will cause the application to deserialize our malicious payload and give us a reverse shell.


Open an interactive PHP shell with ‘php -a’ command. The interactive shell is also called as REPL(Read Evaluate Print Loop) shell. Create a payload as follows:

What did we do? We created a class with the same name as the application and create a variable user_file and assign it a value of ‘shell.php’. We then created a data variable and pass a simple one-liner BASH reverse shell within a PHP exec() function. In PHP we can use exec(), shell_exec() and passthru() functions to run a system command. We then serialized the entire class with serialize function and creating an instance within. We also urlencoded the entire string with urlencode() function. Make sure to name the variables and class name just as it was in the backup file.

Once you get the string pass it over to sator.php as a get request.

We get the following response. This means our class got deserialized and the application executed our malicious code. Now, if everything went as planned, the application must have created a shell.php file in the same directory, with a BASH reverse shell in it.

Open the shell.php file with curl or in a browser, and create a netcat listener on the mentioned port.

We get a reverse shell as www-data user. Upgrade to a TTY shell as follows.

Hit ctrl+z to background the job.

Type fg (You will not be able to view the characters as you type) and hit enter twice.

Export the TERM variable:

In another window check the number of rows and columns of our terminal:

Set the number of rows and columns in our reverse shell:

We now have a fully interactive shell with tab autocomplete.

Privilege Escalation

Before running any automated tool I like to check the basic privesc vectors like SUID, SUDO, Groups, and cron jobs.

We do not get anything interesting. The next thing I did was to hunt for interesting files in the file system. We had an instance of WordPress on the box, navigate to /var/www/html/wordpress.

WordPress usually stores the database credentials in wp-config.php. Checking the file we get the Username(neil) and Password(Opera2112).

You can try to log in to MYSQL but it is quite common for people to reuse passwords. SSH into the box with the credentials.

Great. We are now Neil user. Let us recheck basic privesc vectors once again.

Checking the sudo permissions, we get that neil can execute a shell script as root without providing a password.

Analyzing the contents of the shell script we find that the script creates a temporary file in /tmp/ssh-xxxxxxx and writes a ssh public key into it. The script then checks if the file exists and if it does, write the content of the file into the authorized_keys file of the root user. It then checks if it was able to write or not and display different output based on that check.

We do not have write permissions to the script but we do have execute permission. How is this vulnerable? This is a perfect example of race condition vulnerability. If we can somehow write into the /tmp/ssh-XXXXXX file before the script we can insert anything into the authorized_keys of the root user.

A race condition occurs when multiple processes or threads try to read and write the same variable i.e. they have access to shared data and try to change it at the same time.

To exploit this vulnerability let us create a ssh public and private key pair with the following command:

Adding the ssh key manually can be time-consuming as we do not know the number of attempts in which we will be able to write into the file(remember this is a race between the script and us).

We will instead create a BASH loop to help us automate the task. Grab the public ssh key we created(id_rsa.pub) and insert it into a loop with neil user as follows:

Here we created an infinite loop that will print the ssh public key and copy it to any file that matches the pattern /tmp/ssh- . We also passed it to /dev/null to suppress any output or errors.

After some time try login as root user with the private key.

Note: If you didn’t create the ssh key with the command I used, you must modify the file permissions of the private key as follows.

We are root..!!!!!! To be honest, this didn’t work on my first attempt and I had to revert the box, so don’t shy away from a revert. We can now grab the flags.

Parting Thoughts

In this box, we learned about Insecure Deserialization and Race Conditions. We also learned how to analyze code and hunt for bugs. The entry point was a bit CTF like but the privesc to Neil and root user was a 100% real-world example. You can learn more about Insecure Deserialization in PHP at the following links:

Deserialization in Java Apps:

Thank You for reading. Until next time.

For queries/suggestions find me on Twitter @accesscheck.

Web Developer | Security Researcher | OSCP | Noob