Tracing Spam from PHP Scripts

Posted on September 24, 2013

In a perfect world, everyone would keep their websites updated with the latest software. But because it’s not a perfect world, web servers are prime targets for attackers looking for easy exploits. Being a web hosting sysadmin, it’s rare to see website owners take a vested interest in keeping their sites updated with the latest patches.

The other day I got a report from our upstream provider notifying us about a server that was sending out spam emails. It was my task to track down where the spam was coming from and stop it.

Occam’s Razor

Our web server and its software is pretty lean, and kept up-to-date with the latest patches. The main scripting language available to our customers is PHP. From the relative size of this potential attack vector, and the fact that customers tend to not update their site’s software (or use poorly written, insecure software), a PHP script is by far the most likely source of spam.

Default Headers

You should be able to obtain a copy of a spam message that was sent. Here’s an example of an email’s headers:

Return-Path: <sitename [at] server [dot] example [dot] com>
Received: from [] by
(MTA v5/:PGFiZWxsZW5AbWFuYWdlZHNoYXJlZDIuYXJyb3dxdWljay5uZXQ_)
with SMTP id <20130717204350103198600015> for <victim [at] example [dot] com>;
Wed, 17 Jul 2013 20:43:50 -0500 (CDT)
(envelope-from sitename [at] server [dot] example [dot] com, notifiable emailhost
Received: by (Postfix, from userid 1040)
id 888C414E32F; Wed, 17 Jul 2013 20:27:09 -0500 (CDT)
To: victim [at] example [dot] com
Subject: Order Detail
From: "First-Class Mail Service" <forged [at] example [dot] com>
Reply-To: "First-Class Mail Service" <forged [at] example [dot] com>
Mime-Version: 1.0
Content-Type: multipart/alternative;boundary="----------137411082951E7446D85129"
Message-Id: <20130718012709 [dot] 888C414E32F [at] server [dot] example [dot] com>
Date: Wed, 17 Jul 2013 20:27:09 -0500 (CDT)

If each site on your server runs under its own user account, then you’ll see it as the originating username. In this example, the Return-Path and Received headers both contain the site account (sitename) and the server (

That’s a good place to start, but we’ll need to get info from the server to quickly trace the problem.

MTA Logs

By default, PHP’s built-in mail() function uses the Sendmail (or equivalent) message transfer agent (MTA) on the server. As you see from this Postfix log, though, the only useful info is the date and recipient:

Sep 22 17:36:04 managedshared2 postfix/qmgr[13822]: 9414B142052: from=<forged [at] example [dot] com>, size=1196, nrcpt=1 (queue active)
Sep 22 17:36:04 managedshared2 postfix/smtpd[7937]: disconnect from localhost[]
Sep 22 17:36:11 managedshared2 postfix/smtp[7942]: 9414B142052: to=<victim [at] example [dot] com>,[]:25, delay=6.7, delays=0.12/0.04/5.7/0.86, dsn=2.0.0, status=sent (250 OK id=1VNsGB-000096-3o)
Sep 22 17:36:11 managedshared2 postfix/qmgr[13822]: 9414B142052: removed
Sep 22 17:36:31 managedshared2 postfix/smtpd[7937]: connect from localhost[]
Sep 22 17:36:31 managedshared2 postfix/smtpd[7937]: DA220142052: client=localhost[]
Sep 22 17:36:32 managedshared2 postfix/cleanup[7940]: DA220142052: message-id=<859f6c4e5a4f2a2b1b72b6831cd778cd [at] www [dot] example [dot] com>

The sender address is logged, but this is provided by the user agent (the PHP script), so it is typically forged.

So, the mail log is not the best place to look. There’s probably a way to trace the messages using Postfix’s arcane commands, but it turns out there are easier ways to trace the problem.

With this log, you can at least verify if spam is being sent, if you don’t typically send a lot of mail through the MTA — just tail -f /var/log/mail.log and watch the messages fly!

X Header

Starting in version 5.3, PHP has a configuration option called mail.add_x_header. It adds a header to emails called X-PHP-Originating-Script, which looks like this:

To: victim [at] example [dot] com
Subject: Order Detail
X-PHP-Originating-Script: 1040:kka3f2.php(1) : eval()'d code

I’m not sure if there is an easy way to trace a script from its ID number, but hopefully the filename will be unique enough to provide a good lead.

The add_x_header option is turned off by default, but can easily be enabled in the php.ini file, or on a per-site/-folder basis. You may not want to leave it on all the time, as by definition it reveals some info about the originating PHP script.

PHP Mail Log

PHP 5.3+ also provides a way to log all calls to mail(), using a configuration option called mail.log. Now, if you have a lot of sites, you probably don’t want to leave this on all the time. (It is disabled by default.) Fortunately, with spam you never have to wait long — you can turn it on, log some of the mail() calls, then turn it off.

This is probably the best way to collect info about PHP-based spam, because it includes the full path of the script, including the line number where mail() is called in the file:

mail() on [/var/www/badsite-com/html/components/com_users/kka3f2.php(1) : eval()'d code:118]: 
To: victim [at] example [dot] com -- Headers:
From: "First-Class Mail Service" <forged [at] example [dot] com> 
X-Mailer: MIME::Lite2.117 
Reply-To: "First-Class Mail Service" <forged [at] example [dot] com> 
Mime-Version: 1.0 
Content-Type: multipart/alternative;boundary="----------137417401251E83B3CF186D"

In this example, the kka3f2.php file was a backdoor script that was uploaded through a security hole in a Joomla site.

Note that the log file must be writable by the web server. Whether the server runs as a generic Apache user, or the site account, the log file must have the necessary write permissions. For temporary tracing, you can probably just give it world-writable permissions.


Even if you stop the source of the spam, you probably have dozens (if not hundreds) of emails waiting in the MTA queue. You could just wait and let them get sent (what’s a few more compared to the millions sent every minute?), but you may opt to delete them. In Postfix, you can run postsuper -d ALL to clear out the whole queue — note that this will also delete any valid emails.

Analysis of an Attack

Spammers are at a bit of a disadvantage here. Most backdoor scripts that spammers use are simple attacks that go for the easiest targets. Limiting the source of email to a single language or piece of software forces them to attack through that vector. And with these techniques, you can zero in on the source within minutes.

Of course, if website (and server) administrators kept their stuff up to date, we’d see a lot less spam. Website owners, do your part — update the WordPress/Joomla/Drupal software running your site!

Leave a Reply