Labs

Locker Room

A medium lab writeup with a realistic web foothold and a simple cron misconfiguration.

This lab looks small on the surface — just a basic site and SSH — but the path is a good reminder that boring mistakes compound fast. The winning thread was a ==amber:directory listing==, a ==blue:backup file==, and a ==red:writable maintenance script==.

!![amber] The whole path is simple but realistic: leak -> creds -> upload -> cron abuse.

Recon

I started with a single version scan and wrote the output to a file so I could refer back without re-running the scan.

nmap -sC -sV -Pn -oN nmap.txt target

Results (summarized):

  • 22/tcp open (SSH)
  • 80/tcp open (HTTP)

With only HTTP and SSH in play, I focused on the web app first.

Web enumeration

The landing page was a simple “team announcements” site with a tiny nav and a footer line: “Drafts are only visible to the team.” That line was enough to justify a quick wordlist probe.

ffuf -w /usr/share/wordlists/dirb/common.txt -u http://target/FUZZ -fs 0

Key hit:

  • /drafts/200 with directory listing enabled

Inside /drafts/ there were a few markdown files and a backup archive:

  • notes.md
  • release-plan.md
  • drafts.bak

I pulled the backup locally and inspected it.

wget http://target/drafts/drafts.bak
strings drafts.bak | head

The backup contained a stale .env file with a ==blue:database password and a username==:

APP_USER=locker
APP_PASS=stagingonly
DB_USER=locker
DB_PASS=stagingonly

Foothold

The creds didn’t work for SSH, but they _did_ work in the admin panel of the web app. The login form was at /admin/ and accepted the same locker / stagingonly pair.

!![blue] The backup was the real foothold pivot because it exposed working admin credentials.

Once inside, the “profile picture” upload was the weakest point. The client-side check only blocked extensions in the browser, but the backend didn’t validate content type or file extension.

I tested with a simple PHP payload by renaming it:

cp shell.php avatar.jpg.php

Upload response:

{ "ok": true, "path": "/uploads/avatars/locker.jpg.php" }

Hitting the upload path gave a shell under the web user.

Stabilize and situational awareness

python3 -c 'import pty; pty.spawn("/bin/bash")'
export TERM=xterm
id
pwd
ls -la

I confirmed the web user and checked the app directory permissions. The user owned the app tree and could write to /opt/maintenance/ — that ==red:stood out immediately==.

Privilege escalation

A quick check of cron jobs showed a root task running every minute:

cat /etc/crontab

Entry (summarized):

* * * * * root /opt/maintenance/cleanup.sh

The script was ==red:writable by the web user==. I replaced it with a single-purpose action to drop my SSH key.

!![red] A root cron job executing a writable script is effectively a direct path to root.

echo 'ssh-ed25519 AAAA... attacker@box' > /opt/maintenance/cleanup.sh
chmod +x /opt/maintenance/cleanup.sh

After the next cron tick, SSH as root worked:

ssh -i id_ed25519 root@target

Notes and cleanup

If this were a real system, I’d restore the original script and remove the key. For the lab, I left a note of the original script contents so I can reset quickly when re-running the box.

Takeaways

  • Directory listings are still a ==amber:high-value target==. Always check.
  • Backup files quietly ==blue:expose credentials==.
  • Cron jobs + writable scripts = ==red:fast privilege escalation==.

This lab is a clean example of why “low‑risk” misconfigurations add up. None of the individual issues were impressive on their own, but the chain was enough.