Local File Inclusion (LFI) is a vulnerability that seems simple at first glance. An attacker can read a file and execute it, which they're not supposed to have access to. What's the big deal? The "big deal" is that in the hands of a skilled attacker, this simple file-read vulnerability is often just the first step in a chain that leads to complete server compromise and Remote Code Execution (RCE).

FriendLink🔗

Understanding an attacker's methodology is one of the best ways to build a robust defense. Let's walk through how an attacker finds, exploits, and escalates an LFI flaw, and then cover the concrete steps you can take to prevent it.

Step 1: Finding the entry point

Attackers don't just guess. They use automated tools and a systematic process to find a vulnerable entry point.

  • Fuzzing for Parameters: An attacker's first step is often to find the parameter that accepts a file. They will use fuzzing tools with large wordlists to hammer a URL with thousands of common parameter names (page=, file=, lang=, template=, etc.), looking for any that change the server's response.
  • Fuzzing for Payloads: Once a parameter is found (e.g., index.php?file=guide.php), the next step is to see if it's vulnerable. They'll replace the valid input (guide.php) with LFI payloads from another wordlist. These payloads contain common traversal sequences (../../, ....//) and encodings to test for basic vulnerabilities.
  • Fuzzing for Files: If a payload works, the attacker will then fuzz for sensitive files. Their goal is to find critical configuration files (/etc/apache2/apache2.conf, php.ini), log files (/var/log/apache2/access.log), or application files (config.php) that can reveal the server's structure, webroot path, and credentials.

Step 2: Try to Bypass Common Techniques

Many developers try to patch LFI vulnerabilities with simple string filters. Attackers know this and have a playbook of bypasses for almost every common, weak defense.

  • Non-Recursive Filters: A developer might block ../. An attacker will simply use ....// or ..././. If the filter only runs once, it will replace the first ../, leaving a valid ../ behind.
  • Encoding: If a filter blocks characters like . or /, an attacker will URL-encode them (e.g., %2E and %2F). The server decodes these after the filter checks, rendering the filter useless.
  • Approved Path Filters: A developer might require the input to start with a "safe" path, like /languages/. An attacker simply includes this path before their traversal payload (e.g., file=/languages/../../../../etc/passwd).
  • Appended Extensions: A common defense is to force an extension, like .php, onto the end of the user's input. An attacker bypasses this with a Null Byte (%00). Since C-based languages (like PHP's backend) treat a null byte as the end of a string, the server sees /etc/passwd%00.php and simply stops reading at /etc/passwd.

Step 3: From File-Read to Server Takeover (RCE)

This is the most critical phase. An attacker isn't just content to read files; they want to execute commands.

Source Code Disclosure

Before attempting RCE, try to read the application's source code. They can't justinclude 'config.php', as the server would execute it. Instead, use a clever trick called a PHP Filter Wrapper.

By sending a payload like php://filter/read=convert.base64-encode/resource=config.phpthey tell the server, Don't execute config.php, just read it, encode it in Base64, and output the result. We can then decode the Base64 text to get the raw source code, which often contains database passwords and other secrets.

Achieving Remote Code Execution (RCE)

Once an attacker has enough information, they will attempt to take full control. This requires the LFI vulnerability to be in an "execution" function (like include or require) and often relies on insecure server settings.

  • Prerequisite Check (allow_url_include): The first thing an attacker checks is if the PHP setting allow_url_include is set to On. If it is, the server is wide open. This setting, which is OFF by default for a reason, allows the include() function to execute code from remote URLs.
  • PHP Wrappers for RCE: If allow_url_include is on, an attacker can use the data:// wrapper to pass malicious code directly in the URL.
http://<IP>/index.php?language=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8%2BCg%3D%3D&cmd=id
  • php://input: This wrapper tells the server to execute the raw data sent in the body of a POST request. The attacker sends their malicious code in the request body and uses the LFI to point to this wrapper.
curl -X POST --data '<?php system($_GET["cmd"]); ?>' "http://<IP>/index.php?language=php://input&cmd=id"
  • expect://: A less common but powerful wrapper that allows commands to be passed directly in the URL.
curl "http://<IP>/index.php?language=expect://id"
  • Log Poisoning: This is a classic RCE technique. An attacker knows that server logs like (/var/log/apache2/access.log) record incoming requests. They will send a request where the User-Agent header (or another logged field) is replaced with a small, malicious PHP code snippet. This "poisons" the log file. The attacker then uses the LFI vulnerability to include the log file, and the server executes the malicious code stored inside it.
  • LFI + File Upload: If the application allows users to upload files (like a profile picture), an attacker can combine these two vulnerabilities. They will upload a seemingly harmless file (e.g.gif) that has malicious PHP code hidden inside it. The file upload's security checks see a valid GIF, but when the attacker uses the LFI vulnerability to include that uploaded .gif, the server's PHP interpreter executes the hidden code.
http://<SERVER_IP>:<PORT>/index.php?language=./profile_images/shell.gif&cmd=id

How to Prevent LFI for Good

You can't just patch one or two bypasses. A real defense requires a multi-layered, secure-by-design approach.

  1. NEVER Trust User Input: This is the golden rule. Treat all data from a user, cookie, or hidden field as malicious.
  2. Use a Strict Allow-List: Do not try to block "bad" characters (a blacklist). Instead, define a very strict list of "good" values (an allow-list). If a parameter should only be en.php or es.php, your code should explicitly check: if ($lang === "en.php" || $lang === "es.php"). If it's anything else, reject it.
  3. Disable Dangerous Directives: In your php.ini file, ensure allow_url_include and allow_url_fopen are set to Off. This single step mitigates the most dangerous RCE vectors.
  4. Store Files Outside the Webroot: If you must allow users to upload files, store them in a directory above your public webroot (/var/www/html). This makes it impossible for an attacker to access them directly via a URL. Serve these files via a secure script that checks permissions.
  5. Use Base Paths: Instead of including a path directly from user input, build a safe, absolute path.
  6. The basename() function is crucial here: it strips all directory information from the user's input, leaving only the filename. This single-handedly defeats all directory traversal attacks.
  7. Principle of Least Privilege: Ensure your web server (www-data, apache) runs with the absolute minimum permissions. It should not have permission to read system logs, configuration files, or write to most directories.