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

Local File Inclusion (LFI) — exploitation basics

After you know how file inclusion happens (user input resolving to a filesystem path), the next step is reading local files on the server by steering that path. The ideas below apply broadly—not only to PHP—whenever a parameter or stored value ends up in a file read/include primitive.

Basic LFI

A classic pattern is a language (or page) parameter that maps to an included fragment, for example index.php?language=es.php. If the server literally includes the path derived from the parameter, you can swap the intended file for a known readable system file:

  • Linux: /etc/passwd (user accounts, often world-readable)
  • Windows (older examples): C:\Windows\boot.ini

Example idea: change language=es.php to language=/etc/passwd. If the page shows passwd contents, the app is including or rendering attacker-chosen paths—classic LFI for disclosure (and worse if the primitive executes PHP).

Path traversal (..)

Often the code does not pass your input straight to include(); it prepends a directory, for example:

include("./languages/" . $_GET['language']);

Then language=/etc/passwd resolves to ./languages//etc/passwd, which does not exist. Verbose errors (turned on only in labs) may echo the final path—useful for understanding behavior, but real attacks should not depend on errors being visible.

Bypass: walk up with ../ until you reach the filesystem root, then append the target file, e.g. ../../../../etc/passwd. From /var/www/html/languages/, going up four levels can land you at / before etc/passwd. Extra ../ beyond the root typically collapses to /, so over-shooting depth is often safe—still prefer the minimum ../ that works for cleaner reports and exploits.

Filename prefix

If the developer concatenates a prefix before your input:

include("lang_" . $_GET['language']);

then ../../../etc/passwd becomes lang_../../../etc/passwd, which is not a valid path.

Bypass: start your payload with / so the resolved path treats the prefix as a directory segment and the rest as traversal, e.g. language=/../../../etc/passwd. This may fail if lang_ is not a real directory on disk, and prefixes can interfere with advanced tricks (wrappers, filters, RFI) covered elsewhere.

Appended extensions

Another common hardening attempt is forcing an extension:

include($_GET['language'] . ".php");

Then /etc/passwd becomes /etc/passwd.php and fails. Multiple bypass styles exist (null bytes where allowed, wrappers/filters, truncation quirks, etc.)—those belong in dedicated follow-on notes.

Lab exercise: try LFI against a .php file (e.g. index.php). Observe whether you get source disclosure or executed HTML—this distinguishes read vs execute behavior of the sink.

Second-order LFI

Second-order attacks store the payload indirectly (often in a database), then a later feature uses that value as a path component.

Example: avatar download at /profile/$username/avatar.png. If username can be set to something like ../../../etc/passwd during registration, a later “download my avatar” flow may open the wrong file. Developers may sanitize direct query parameters but still trust DB-backed fields—that trust is the bug.

Exploitation is the same path logic as first-order LFI; the extra work is mapping which stored field flows into which file operation.

Takeaways

  • Test absolute paths when input is used verbatim; use ../ chains when a base directory is prepended; use /-prefixed traversal when a filename prefix is glued on.
  • Extension appending and other normalizations need their own bypass families.
  • Second-order issues follow data from write-time (registration, profile update) to read-time (export, report, attachment).
  • Techniques are language-agnostic at the logic level: any stack that builds paths from untrusted data can exhibit the same classes of flaws.