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.