File Inclusion: LFI, RFI, and the Road to RCE

PHP Wrappers for LFI: Source Disclosure and Code Injection

Use PHP stream wrappers against an LFI: php://filter to read source, data:// and php://input to inject code, and the filter chain to RCE with no file write.

Published

PHP stream wrappers turn a file-inclusion bug into two distinct wins: reading source code that would otherwise execute, and, on a loose configuration, injecting code directly. This page covers the wrappers that matter, in the order you should try them, with the configuration caveats that decide whether each one works. Practice on a free live target.

What a stream wrapper is

PHP can open more than filesystem paths. Prefixes like php://, data://, and phar:// are stream wrappers, and include accepts them wherever it accepts a path. That means an LFI parameter is not limited to files on disk, it can be pointed at a wrapper that transforms, supplies, or executes content. Which wrappers are available depends on the PHP build and config.

php://filter: read source without executing it

The workhorse. Including a .php file normally runs it, so you never see the source. Wrap it in a base64-encode filter and the bytes are encoded before the include receives them, so nothing executes and you get the source back as base64:

GET /index.php?page=php://filter/convert.base64-encode/resource=config.php HTTP/1.1
Host: target.example
HTTP/1.1 200 OK
Content-Type: text/html
 
PD9waHAKJGRiX3VzZXIgPSAnYXBwJzsKJGRiX3Bhc3MgPSAnUzNjcjN0UGFzcyc7Cg==

Decode the blob and you have the source of config.php, database password included. This needs only that the parameter reach a file read, and it does not require allow_url_include.

php://filter/read=convert.base64-encode/resource=config.php    explicit read= form
php://filter/convert.base64-encode/resource=../../../../etc/passwd   plain files too
php://filter/convert.iconv.utf-8.utf-7/resource=index.php      alt when base64 is filtered (UTF-7 output, still decode it)

The base64 form is the one to reach for, because the encoded output survives an include that would otherwise mangle or run the content.

data:// and php://input: inject code directly

When the configuration is loose, you can supply the code yourself. The data:// wrapper carries an inline payload and php://input pulls it from the request body:

data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOz8+    (decodes to <?php system($_GET['c']);?>)
data://text/plain,<?php system($_GET['c']);?>
php://input        (the raw POST body becomes the included PHP)

Both of these run code, so PHP gates them: using data:// or php://input as an include source requires allow_url_include=On, which is off by default on modern PHP (the same switch that governs remote includes). When it is off, these do nothing for inclusion, and you fall back to php://filter for reading plus log or session poisoning for execution. In a URL parameter, prefer the base64 data:// form, since the raw one needs its ?, spaces, and tags URL-encoded to survive the query string. Do not expect data:// to be a way around a disabled URL include, it is governed by the exact same setting.

expect:// and archive wrappers

Situational, but worth knowing:

expect://id         direct command execution, needs the (rarely installed) expect extension
zip://shell.zip%23shell.php     include a file inside an uploaded zip  (%23 is #)
phar://shell.phar/shell.php     include a file inside a Phar archive

expect:// is almost never available. The archive wrappers pair naturally with a file upload: drop an archive that contains a .php, then include the entry inside it. phar:// additionally triggers object deserialization when the Phar metadata is read, a separate escalation.

The filter chain to RCE (no file write)

On a hardened box where nothing is writable and allow_url_include is off, there is still a route. The convert.* and iconv.* filters can be chained so that applying them in sequence to an empty resource emits an arbitrary byte sequence, which means a pure php://filter string can synthesise a working PHP payload with no file on disk. The chains are long and are produced by a generator rather than written by hand (the well-known one is Synacktiv's php_filter_chain_generator). Conceptually:

php://filter/<a long generated chain of convert.iconv.* steps>/resource=php://temp

Treat this as an advanced, tool-generated technique: you run the generator with your desired PHP payload, paste the resulting filter string into the LFI parameter, and the include materialises and runs your code. It needs the PHP build to include iconv support (common but not universal, worth confirming from a phpinfo leak). It is the reason a "read-only" php://filter primitive can still mean RCE on an otherwise locked-down target.

Fixing it

  • Disable allow_url_include and allow_url_fopen, which removes data:///php://input code injection and remote includes.
  • Allow-list view names so the parameter can never carry a php:// prefix at all.
  • Do not route user input into include. Map a key to a fixed path, which defeats every wrapper at once.

Full guidance is in preventing file inclusion. Next, reach code execution without a loose config by writing PHP into a server log in log poisoning to RCE, or try the wrappers on a free lab.

← Back to File Inclusion: LFI, RFI, and the Road to RCE