Local File Inclusion (LFI) Attacks

Local file inclusion (LFI) is the process of including files that are already present on the target server by exploiting vulnerable dynamic file-inclusion mechanisms in a web application. It arises when user-supplied input is used to construct a filesystem path — typically passed to a language-level include/require-style statement or file-read function — without proper validation. Depending on context, LFI can yield sensitive information disclosure, denial of service, or (in the worst case) remote code execution.

LFI is most common in PHP, where the include()/require() family of functions will happily evaluate the included file as code, but the same pattern occurs in other languages and frameworks whenever a path is built from untrusted input.

Path traversal basics

Since web servers typically serve content from /var/www, /srv/www, or an immediate subdirectory for virtual hosts, reaching the filesystem root generally requires traversal sequences like ../../, ../../../, or ../../../../ to climb out of the document root. The exact depth needed depends on where the including script lives.

For example, if the vulnerable parameter is page:

GET /index.php?page=../../../../etc/passwd

See also directory-traversal for the closely related attack class (reading files directly rather than including them).

PHP-specific LFI tricks

Poison null byte

For PHP < 5.3.4, the poison null byte (%00) defeats simple path filters and situations where the developer appends a suffix to user input (e.g. include($page . ".php")). The null byte terminates the string in the underlying C call, so the appended .php is silently dropped.

Trailing-dot trick

If the null byte doesn’t work, another quirk is that PHP allows files to be referenced with . notation just like directories. In other words, /etc/passwd/. resolves to /etc/passwd. This can slip past naive suffix checks that expect a directory separator.

Filter-bypass with ....//

Representing ../ as ....// bypasses filters that replace ../ with an empty string, because PHP’s search-and-replace only makes a single pass through the string — after removing the inner ../, the remaining characters re-form another ../. The trick extends naturally if a developer runs the replacement twice (.....///, etc.).

From LFI to RCE

When PHP is the includer, an LFI that reaches attacker-controlled content becomes code execution. A minimal webshell dropped via log poisoning, an uploaded avatar, or /proc/self/environ injection looks like:

<?php
	echo "<pre>" . shell_exec($_GET["cmd"]) . "</pre>";
?>

A slightly spruced-up version of this pattern ships with Kali Linux as /usr/share/webshells/php/simple-backdoor.php.

On space-constrained systems, the same idea compacts down to 15 bytes:

<?=`$_GET[1]`?>

This relies on PHP’s short-open-tag + backtick execution and is handy when the injection point (e.g. a User-Agent string destined for a log file) has tight length limits.

Testing approach

In a black-box test, look for scripts that take filenames or page names as parameters (?page=, ?file=, ?include=, etc.). Probe with traversal sequences aimed at known-readable files (/etc/passwd on Linux, C:\Windows\win.ini on Windows), then escalate to wrapper abuse (php://filter, data://, expect://) if the target runs PHP. See xxe-attacks for the related expect:// path to RCE via XML.

Defenses

  • Never pass user input directly to an include/file-read call — map to a fixed allowlist of pages instead.
  • If input must be used, canonicalize the path (resolve .., symlinks, and trailing-dot tricks) and verify the result stays under an expected base directory.
  • Strip null bytes explicitly on legacy PHP (see poison-null-byte-attack).
  • Disable dangerous PHP wrappers (allow_url_include=Off, restrict expect, etc.).

Sources

Related: php, poison-null-byte-attack, xxe-attacks, directory-traversal