Bali, Indonesia
Insights

Finding the Source Script Behind Outbound Postfix Spam Queues

October 16, 2026
0:00
A sudden surge in outbound mail usually announces itself when external mail providers begin deferring your server or when the Postfix mail queue jumps from dozens of messages to tens of thousands. When a server gets flagged for spamming, clearing the queue with postsuper -d ALL only provides a few minutes of peace before new messages flood in again. The real problem is not the mail queue itself, but a compromised web application or malicious script running locally and submitting mail through the local Postfix daemon. On shared hosting servers and multi-tenant virtual private servers, identifying the exact script responsible for outbound spam comes down to choosing between three diagnostic methods. Depending on whether the queue is still actively filling, whether your web applications execute as separate system users, and how much performance overhead your server can handle, each approach offers a different balance of speed and depth.
When a PHP script on a Linux server sends an email, it rarely connects to localhost over SMTP port 25 unless explicitly configured with an SMTP library like PHPMailer or SwiftMailer. Instead, standard CMS installations and unauthenticated webshells call the native PHP mail() function, which hands the message off to the local /usr/sbin/sendmail binary. Postfix's sendmail wrapper then writes the message into the local maildrop or incoming queue. Because this handoff happens entirely inside the operating system, remote authentication logs (/var/log/maillog or /var/log/mail.log) rarely show SASL login events. To find the source file, you must inspect the transaction using one of three methods: direct envelope inspection of messages already sitting in the Postfix queue, PHP-level execution logging, or a custom wrapper script placed around the system sendmail binary.
This is the fastest, read-only approach when you already have thousands of spam messages sitting in Postfix's deferred or active queues. By selecting a single Queue ID from mailq and running postcat -q <QueueID>, you read the raw spool file, including the internal Postfix envelope records, client attributes, and RFC 822 headers.
  • When to choose this: The spamming is actively occurring, the queue holds unpurged messages, and you cannot afford to restart PHP or modify server configuration files.
  • What it tells you: Postfix envelope records record the Unix user ID (UID) of the process that called the sendmail binary. If your web server runs via PHP-FPM pools where each website has its own Linux user (such as user_client1), this UID immediately isolates the compromised account.
  • Where it falls short: If the entire server runs PHP under a shared web server user (such as www-data, nobody, or apache), the envelope will only show that shared UID. Furthermore, if you or an automated cleanup script already purged the queue, postcat has nothing left to read.
Modern PHP runtimes include built-in directives designed specifically to trace automated emails back to their source files. Configuring mail.add_x_header = On forces PHP to inject an X-PHP-Originating-Script header into every generated message, showing the UID and the exact PHP file path (such as 1001:upload_handler.php). Complementing this with mail.log = /var/log/php_mail.log records the timestamp, filename, and line number of every mail() call without waiting for messages to be inspected in the mail queue.
  • When to choose this: You manage the PHP configuration, all or most websites run via PHP, and you want persistent auditing that survives queue purges and captures spam before external hosts reject it.
  • What it tells you: The exact script path on disk and the line number where mail() was invoked, regardless of whether all sites share a single system user.
  • Where it falls short: It only catches scripts that use the PHP mail() function. If an attacker uploads a compiled binary, runs a Perl/Python script, or uses an application that connects directly to localhost:25 via sockets, PHP's internal logging will never see it. Modifying php.ini or pool files also requires a reload of PHP-FPM or Apache.
In a wrapper setup, the real /usr/sbin/sendmail binary is renamed (for instance, to sendmail.real), and a small shell or Perl script is placed at /usr/sbin/sendmail. Whenever any program on the server tries to send mail, the wrapper logs the current working directory (pwd), the caller's process tree, the date, and the standard input, before piping the message along to the real binary.
  • When to choose this: You have an elusive compromise where attackers bypass PHP logging, multiple runtimes (Node.js, Python, Perl) are installed, or your PHP stack is locked down and cannot be easily modified.
  • What it tells you: The absolute working directory of any process invoking the system mailer, catching every language and non-PHP CGI script.
  • Where it falls short: It introduces process overhead for every outbound message. If a spam loop sends 500 emails per minute, spawning shell wrappers can cause CPU spikes. In addition, package managers (like apt or dnf) can overwrite your custom wrapper during routine Postfix updates, breaking logging or delivery.
The table below outlines how these three methods compare across practical operational constraints:
CriteriaQueue Inspection (postcat)PHP Native AuditingSendmail Wrapper
Setup RequiredNone (instant)Low (edit php.ini & reload)Moderate (rename binary, write script)
Runtime OverheadZeroNegligibleLow to Moderate (spawns extra subshell)
Shared-User ResolutionPoor (shows UID only)High (shows exact script file)High (shows current directory & process)
Non-PHP Mail VisibilityHigh (captures any sendmail call)NoneHigh (captures all local calls)
Durability After Queue FlushNoneHigh (persists in dedicated log)High (persists in dedicated log)
If you are currently facing a deferred queue, starting with queue inspection is almost always the right first move because it requires zero configuration changes. You grab a sample Queue ID from the mail spool, extract its metadata, and determine whether you are dealing with an authenticated user or a local script.
BASH
# Find the top sender addresses and queue IDs currently waiting
mailq | head -n 20

# Inspect the envelope and headers of a specific queue entry (example ID: 4Tz9sK1Q3rz8wB)
postcat -q 4Tz9sK1Q3rz8wB | grep -E -i '^(named_attribute: log_ident|X-PHP-Originating-Script|From:|User-Agent:)'
In the output of postcat, look closely for two fields:
  1. named_attribute: log_ident: On many Postfix setups, this records the system user who executed the local sendmail command. If you see log_ident=siteuser4, you immediately know which document root contains the compromised software.
  2. X-PHP-Originating-Script: If PHP mail headers were previously enabled on the system, this line will immediately state the script name (for example, 1004:class-smtp.php(326) : eval()'d code).
If the envelope records point to a generic apache or www-data user with no script header, queue inspection has given you all the information it can. At that point, you should switch immediately to PHP auditing by appending mail.log = /var/log/php_mail.log and mail.add_x_header = On to your global php.ini or target PHP-FPM pool configuration, followed by a graceful service reload.
Once queue metadata or PHP logs point you to a specific file (such as a random .php file in /wp-content/uploads/ or an injected snippet inside a theme template), do not stop at simply deleting the file. Web shells and spam injection scripts are usually the symptom of an unpatched plugin, weak credentials, or an outdated CMS, not the initial entry point. Take the path of the identified file and cross-reference its creation timestamp with your web server's access logs. Look for incoming POST requests occurring at the same time the spam queue began swelling. Keep in mind that a malicious request does not necessarily trigger an error; as detailed in Why HTTP 200 in Access Logs Does Not Mean an Exploit Succeeded, attackers often leverage vulnerabilities that return clean HTTP 200 responses while executing payload deliveries in the background. Matching the outbound queue surge with incoming HTTP requests confirms whether the script was uploaded through a web vulnerability or executed via an authorized shell session.
Handling an outbound Postfix spam incident requires working backward systematically: use postcat on existing queue items to establish whether the mail was submitted via an authenticated SMTP account or a local process UID. If the server runs a multi-tenant environment under a single web server user, rely on PHP native mail logging directives rather than guessing which site is at fault. Once the file path is confirmed, remove the offending script, patch the underlying web application, and only then purge the backlog of deferred spam messages to allow legitimate mail to clear.