Crosswind
A member-funded social network for cyclists — ride logs, route photos, weekly mileage leaderboards. No ads, no subscriptions, no syncing your heart-rate to a hedge fund. Profiles get a picture; members upload it.
The Scenario
Crosswind has run since 2019 on member dues and three volunteer mods — no ads, no premium tier, no syncing your heart-rate to a hedge fund. The stack is whatever ran fine on a Linode in 2019: PHP, a single database file, and a profile-picture form the founder wrote in one sitting after a long Saturday gravel ride. The next morning he added a short rejection list for "the obvious stuff people might try to upload," patted himself on the back, and went out for coffee.
Challenge Intel
Synopsis
Profile-picture uploader blacklists .php/.exe/.html but accepts .phtml/.php5/.phar — all of which Debian's default mod_php config (<FilesMatch \.ph(p[3457]?|t|tml|ar)$>) happily executes as PHP.
What It Is
account.php?action=avatar accepts $_FILES['avatar']. The validator extracts the extension with strtolower(pathinfo(name, PATHINFO_EXTENSION)) and rejects if it appears in the blacklist ['php', 'phar', 'exe', 'html', 'htm', 'js', 'svg', 'asp', 'aspx']. The list omits .phtml, .php5, .php3, .php4, .php7 — all of which the stock php:8.2-apache image executes as PHP via the bundled /etc/apache2/mods-enabled/php8.2.conf FilesMatch directive. The file is saved at /var/www/html/uploads/avatars/<userid>-<basename> and served directly by Apache from /uploads/avatars/. The uploaded path is rendered in an <img src=...> on the account page so the player can copy it. Uploading shell.phtml with <?php echo shell_exec($_GET['c']); ?> and visiting /uploads/avatars/<userid>-shell.phtml?c=cat+/flag.txt yields the flag from /flag.txt (0644).
Who It's For
A player ready for their first hands-on file-upload challenge. Assumes you can register a real account, find an upload form, and try a handful of extensions when one is rejected.
Skills You'll Practice
- Recognising an extension-blacklist filter from the rejection message
- Knowing the full list of PHP extensions Apache executes by default
- Walking from upload → public path → RCE → flag read
What You'll Gain
- Why deny-lists are structurally weaker than allow-lists for upload validation
- The canonical Debian mod_php FilesMatch regex that maps .phtml/.php5/.phar to the PHP handler