Every section required answering a slightly different question: can I overwrite an existing file? Can I upload executable code? Where does the file land? Is it served or executed? What filter is actually blocking me? What backend is processing the file?
1. Basic Webshell Upload
Gobuster identified the upload location. Uploaded a tiny PHP webshell:
<?php echo system($_GET["cmd"]); ?>
The uploaded shell was reachable and executable. whoami returned www-data. Flag retrieved from /var/www/flag.txt.
The file wasn't just stored — it was being interpreted server-side as executable PHP. That distinction matters.
2. Client-Side Filter Bypass
Page source referenced client-side-filter.js — a JavaScript file enforcing image upload restrictions in the browser. Browser-side validation is not validation.
The PHP webshell was renamed to shell.png to pass the file picker. Then in Burp:
- filename changed from
shell.pngtoshell.php - content type changed from
image/pngtotext/x-php
Upload succeeded. Shell executed from /images/shell.php.
3. Extension Blacklist Bypass
Server-side extension blacklist blocked common PHP extensions. The PentestMonkey reverse shell was renamed to php-reverse-shell.jpg.php5 — the blacklist didn't cover .php5, which still executed.
The upload interface used CLI-style commands rather than a standard form. After upload, the file landed in /privacy/ with a randomized timestamp prefix. Triggered it directly. Reverse shell connected back.
4. Magic Number Bypass
Server-side validation checked the file's magic bytes rather than the extension. Modified the payload in hexeditor to begin with the GIF magic number:
47 49 46 38 39 61 → GIF89a
The file command confirmed the spoofed type. Upload accepted. Retrieved the flag via the known path using a webshell instead of a reverse shell for simplicity.
5. Black-Box Node/Express Exploitation — Jewel
The hardest challenge. The upload and execution paths were completely separate, and the backend wasn't PHP.
The upload endpoint response contained X-Powered-By: Express. That was the key clue. A PHP payload was the wrong fit entirely.
Gobuster identified three directories: /admin, /content/, /modules/. Uploaded files landed in /content/ with randomized short filenames. Retrieving them with curl showed the raw payload — they were being served, not executed.
The /admin page contained a form that activated modules from /modules/. Intercepting the admin POST in Burp revealed:
POST /admin?submit=failure HTTP/1.1
body: cmd=...
Removing ?submit=failure from the request line and supplying a traversal path to the uploaded file converted a failed module lookup into code execution.
A JS reverse shell payload wrapped to satisfy JPEG client-side checks was uploaded, the randomized filename identified, and the corrected admin request fired. Root shell connected back.
What This Room Actually Teaches
The biggest conceptual shifts across all six challenges:
- Don't trust the upload path to also be the execution path
- Recognize when the backend invalidates your payload choice
- Use Burp to correct or reshape requests, not just bypass browser restrictions
- Treat small enumeration details as execution clues rather than noise
The Jewel challenge especially reinforced that the right answer is often not "try more payloads" but "better understand how the application routes uploaded content."