To identify PHP Spam when running PHP 5.3 or higher: (Please note the variables below are not enabled by default in php.ini) utilize the following php.ini variable to narrow down the offending script.
If you suspect there is a PHP script sending out email (and it is still doing so) try adding these two lines:
mail.add_x_header = On
mail.log = /var/log/php_mail.log
to the [mail] section of:
/usr/local/lib/php.ini
Also make sure to create the log file manually otherwise you may get permissions errors and it won’t work:
touch /var/log/php_mail.log
chmod 666 /var/log/php_mail.log
The first variable adds:
X-PHP-Originating-Script:
to the exim email header (the header variable should only show up when PHP does the sending). So, for example if you had a a PHP script sending from bad_script.php the header would look something like:
X-PHP-Originating-Script: 500:bad_script.php
You can actually search the queue for this header (btw, this doesn’t show up in the regular exim_mainlog) by using:
exiqgrep 'X-PHP-Originating-Script'
This should give you a list of emails that have that have been sent using PHP or, if you know the script name, you could also use:
exiqgrep 'bad_script.php'
This should give you a list of emails that have that have that script name in it and if you’re REALLY sure that only spam emails are listed you could use:
exiqgrep -i 'bad_script.php' | xargs exim -Mrm
which filters out all the emails with bad_script.php in it, then only displays the message ids and then delete them.
The above header information is really useful, but when combined with the mail.log variables can be useful. This causes PHP to record whenever:
mail()
is used. A simple output would be similar to:
mail() on [/home/user/public_html/bad_script.php:11]: To: alice@domain.com -- Headers: From: eve@other.net Reply-To: bob@next.org Content-type: text/html; charset=iso-8859-1
If you were to compare the cwd output of a spam script to this log you could get a pretty good idea of where the spam is coming from.
Ejoy!
p.s.
If you need a spamfu script that identifies spam coming from the server, try this one. This is a script designed to help you find the source of spam quickly. It currently can parse spam from the email queue as well as the exim logs. (Big ups to mwineland for the script; he rox!)
#!/bin/bash
#Global variables
queue_total=`exim -bpc` # Saving how many emails are currently in the queue
version='1.1.1'
# Logfile checking variables:
LOGDIR=/var/log
LOGFILE=exim_mainlog
GREP="grep"
NUM_RCPTS=15
#############################################
# User changeable variables to choose which #
# checks are performed during log parsing #
############################################
# Check for emails that were sent from scripts
# by searching for CWD
CHECK_FOR_SCRIPTS="true"
# Checks for emails that were sent by a user
# that logged in with a password
CHECK_FOR_AUTH="true"
# Checks for emails sent by
# a cpanel/system account
CHECK_FOR_ACCOUNT="true"
# Show the address the most bounce backs
# were returned to
CHECK_FOR_BOUNCES="false"
# Does a check for emails sent by a email address
# However this can be forged, may get rid of this function
CHECK_MOST_DOMAIN="false"
# Shows what IP's sent the most emails
# 127.0.0.1 is common if sent via script/webmail
CHECK_MOST_IP="false"
# Shows single emails that had the most recipients
CHECK_MOST_RCPTS="true"
##################################################
# !!!END OF USER DEFINABLE VARIABLES!!! #
##################################################
# Sends script version information to my server
#commented out because the domain expired
#curl --connect-timeout 5 -d "version=$version" http://morzain.net/spamfu/spamfu.php
###################################################
###################################################
# This is the main menu of the script #
# This asks you to check the logs, queue, or exit #
###################################################
###################################################
function spamfu_init
{
echo "####################################"
echo ""
echo "SpamFu for Dummies:"
echo "There are currently ${queue_total} emails in the queue"
echo ""
echo "What would you like to do?:"
echo " (1) Check for spammers via email logs"
echo " (2) Check for spammers via emails in the queue"
echo " (3) Exit"
echo -n "Select option: "
read spamfu_option
if [ -z $spamfu_option ]; then
spamfu_init
else
echo "You have selected $spamfu_option"
fi
if ! [ $spamfu_option -ge 1 -a $spamfu_option -le 3 ];then
echo Invalid option
spamfu_init
fi
if [ $spamfu_option = "1" ]; then
logs_init
fi
if [ $spamfu_option = "2" ]; then
queue_init
fi
if [ $spamfu_option = "3" ]; then
echo "Exiting"
exit
fi
}
#######################################################
#######################################################
# This is the menu for checking the queue #
# It asks you how many seconds to check the queue for #
#######################################################
#######################################################
function queue_init
{
echo "There are currently ${queue_total} emails in the queue"
echo ""
echo "Parsing the entire queue can take a while"
echo "Instead we can look at a snapshot of the emails in the queue"
echo -n "How many seconds would you like to parse the queue for? 0 is unlimited [0]: "
read queue_option
if [ -z $queue_option ]; then
echo ""
echo "You have selected: 0 (unlimited)"
echo ""
queue_timeout="0"
check_queue
elif
echo $queue_option | grep -Eq '^[0-9]{0,3}?\.?[0-9]+$'; then
queue_timeout=$queue_option
echo "Setting timeout to ${queue_timeout} seconds"
check_queue
else
echo ""
echo "****************************"
echo "Please enter a valid integer"
echo "****************************"
echo ""
queue_init
fi
}
#######################################################
#######################################################
# This is the menu for checking the logs #
#######################################################
#######################################################
function logs_init
{
echo ""
echo "##########################################################"
echo "Parsing a large file with lots of checks can take a while"
echo "Choose options to pick which logfile, which checks to perform"
echo "as well as how many lines of the file to parse"
echo ""
echo "LOGFILE: `echo $LOGFILE | awk '{print $1}'`"
echo "SIZE: `du -sh $LOGDIR/$LOGFILE | awk '{print $1}'`"
echo " (1) Proceed with check"
echo " (2) Change log file"
echo " (3) Change which checks are performed"
echo " (4) Change how many lines to parse"
echo " (5) Main Menu"
echo -n "Select option: "
read logmenu_option
if ! [ $logmenu_option -ge 1 -a $logmenu_option -le 5 ];then
echo Invalid option
logs_init
fi
if [ $logmenu_option = "1" ]; then
check_logs
fi
if [ $logmenu_option = "2" ]; then
logfile_menu
fi
if [ $logmenu_option = "3" ]; then
logcheck_menu
fi
if [ $logmenu_option = "4" ]; then
logline_menu
fi
if [ $logmenu_option = "5" ]; then
spamfu_init
fi
}
################################
function logfile_menu
{
LOG_NUMBER=0
echo ""
echo "######################################"
echo "choose one of the following:"
# create an array called LOG_LIST with a list of any file in $LOGDIR
# that starts with exim_mainlog
for logfile in `ls $LOGDIR/exim_mainlog*`; do
let "LOG_NUMBER += 1"
LOG_LIST[$LOG_NUMBER]=`echo $logfile | awk -F/ '{print $NF}'`
done
# save the number of files in the array and subtract 1
# becuase we want to start at 1 instead of 0
LOG_NUMBER=${#LOG_LIST[@]}
# let "LOG_NUMBER -= 1"
# Create the menu with a loop of 1 to however many files are in the array
for i in `seq 1 $LOG_NUMBER`; do
echo " ($i) `du -sh $LOGDIR/${LOG_LIST[$i]} | awk '{print $1}'` ${LOG_LIST[$i]}"
done
echo -n "Select option: "
read logmenu_option
if [ $(echo "$logmenu_option" | grep -E "^[0-9]+$") ]; then
if [ $logmenu_option -le $LOG_NUMBER -a $logmenu_option -gt 0 ]; then
LOGFILE=${LOG_LIST[$logmenu_option]}
echo "Using ${LOG_LIST[$logmenu_option]}"
if [[ $(file $LOGDIR/$LOGFILE | grep "gzip") ]]; then
GREP="zgrep"
echo "using zgrep"
else
GREP="grep"
echo "using grep"
fi
logs_init
else
echo "Invalid option"
logfile_menu
fi
else
echo "Invalid option"
logfile_menu
fi
}
###########################
function logline_menu
{
echo "not implemented yet"
logs_init
}
function logcheck_menu
{
echo "not implemented yet"
echo "these can be modified by editing the script"
logs_init
}
####################################################
####################################################
#Function to check the logs #
#Called if you choose that option on the main menu #
####################################################
####################################################
check_logs()
{
# This sets a variable so we can ignore emails sent to domains on the server, as we only want outgoing emails.
# it takes the list of domains, adds "for .*@" to the front of each domain
# then replaces the newline characters with pipes, and removes the pipe that ends up at the end of the line
LOCAL_DOMAINS=`cat /etc/localdomains | sed 's/^/for .*@/g' | tr '\n' '|' | sed 's/|$//'`
check_for_scripts()
{
echo "Checking for scripts..."
SCRIPTED_EMAILS=`$GREP -o " cwd=[[:alnum:][:graph:]]*" $LOGDIR/$LOGFILE | grep -v spool | sort | uniq -c | sort -rn | head`
echo "Emails sent from scripts:"
echo "$SCRIPTED_EMAILS"
echo
}
check_for_auth()
{
echo "Checking for auth users..."
AUTH_EMAILS=`$GREP '< =' $LOGDIR/$LOGFILE | egrep -o " A\=(fixed|courier|dovecot)_(login|plain):[[:alnum:][:graph:]]*" | cut -d: -f2 | sort | uniq -c | sort -rn | head`
echo "Most emails sent by authenticated users:"
echo "$AUTH_EMAILS"
echo
}
check_for_account()
{
echo "Checking for cpanel/system accounts..."
ACCOUNT_EMAILS=`$GREP '<=' $LOGDIR/$LOGFILE | egrep -v "$LOCAL_DOMAINS" | grep -v ' U=mailnull' | grep -o " U=[[:alnum:][:graph:]]*" | cut -d= -f2 | sort | uniq -c | sort -rn | head`
echo "Emails sent from cpanel/system accounts:"
echo "$ACCOUNT_EMAILS"
echo
}
check_for_bounces()
{
echo "Checking for bounces..."
BOUNCE_BACKS=`$GREP " U=mailnull.*returning message" $LOGDIR/$LOGFILE | grep -o " for [[:alnum:][:graph:]]*@[[:alnum:][:graph:]]*" | cut -d' ' -f3 | sort | uniq -c | sort -rn | head`
echo "Most bounces returned to:"
echo "$BOUNCE_BACKS"
echo
}
check_most_domain()
{
echo "Checking 'from' addresses..."
MOST_SENT_DOMAIN=`$GREP '<=' $LOGDIR/$LOGFILE | grep -v mailnull | egrep -v "$LOCAL_DOMAINS" | cut -d" " -f6 | sort | uniq -c | sort -rn | head`
echo "Most frequent senders by 'from' address:"
echo "Note: Could be forged addresses"
echo "$MOST_SENT_DOMAIN"
echo
}
check_most_ip()
{
echo "Checking for sender IP..."
MOST_SENT_IP=`$GREP '<=' $LOGDIR/$LOGFILE | egrep -v "$LOCAL_DOMAINS" | grep -o ' H=.*\ \[[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\]' | grep -o '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}' | sort | uniq -c | sort -rn | head`
echo "Most frequent senders by IP address:"
echo "$MOST_SENT_IP"
echo
}
check_most_rcpts()
{
echo "Checking for recipients..."
MOST_RCPTS=`$GREP '<=' $LOGDIR/$LOGFILE | grep -v mailnull | egrep -v "$LOCAL_DOMAINS" | awk -v NUM_RCPTS="$NUM_RCPTS" '
function shift_list(x)
{
y=x
x++
while (x <= NUM_RCPTS)
{
TOP_RCPTS[x] = TOP_RCPTS[y]
MAIL_ID[x] = MAIL_ID[y]
SENDER_ID[x] = SENDER_ID[y]
x++
y++
}
}
function save_current()
{
SENDER_ID[CUR_NUM]=$6
MAIL_ID[CUR_NUM]=$4
TOP_RCPTS[CUR_NUM]=NUM_ADDRESSES
}
function display_results()
{
CUR_NUM=1
while (CUR_NUM <= NUM_RCPTS) {
print MAIL_ID[CUR_NUM], "with", TOP_RCPTS[CUR_NUM], "recipients was sent by", SENDER_ID[CUR_NUM]
CUR_NUM++
}
}
function duplicate_check()
{
x=NUM_RCPTS
y=x-1
while (x > 0)
{
if (MAIL_ID[x] == MAIL_ID[y])
{
MAIL_ID[x]=0
TOP_RCPTS[x]=0
SENDER_ID[x]=0
}
x--
y--
}
}
BEGIN {
CUR_NUM=NUM_RCPTS
while (CUR_NUM > 0){
MAIL_ID[CUR_NUM] = 0
TOP_RCPTS[CUR_NUM] = 0
SENDER_ID[CUR_NUM] = 0
CUR_NUM--
}
}
{
split($0,RCPTS_TMP,"from.*for ")
split(RCPTS_TMP[2],RCPTS," ")
NUM_ADDRESSES=0
for (ADDRESSES in RCPTS)
++NUM_ADDRESSES
CUR_NUM=1
for (EACH in TOP_RCPTS)
{
if (NUM_ADDRESSES > TOP_RCPTS[CUR_NUM])
{
shift_list(CUR_NUM)
save_current()
duplicate_check()
break
}
CUR_NUM++
}
}
END {
display_results()
}
'`
echo "Most recipients by Mail and Sender ID's:"
echo "$MOST_RCPTS"
}
# This is where the functions that were declared above
# Are actually called, if their variables are set to true
if [ $CHECK_FOR_SCRIPTS = "true" ]; then
check_for_scripts
fi
if [ $CHECK_FOR_AUTH = "true" ]; then
check_for_auth
fi
if [ $CHECK_FOR_ACCOUNT = "true" ]; then
check_for_account
fi
if [ $CHECK_FOR_BOUNCES = "true" ]; then
check_for_bounces
fi
if [ $CHECK_MOST_DOMAIN = "true" ]; then
check_most_domain
fi
if [ $CHECK_MOST_IP = "true" ]; then
check_most_ip
fi
if [ $CHECK_MOST_RCPTS = "true" ]; then
check_most_rcpts
fi
}
#############################################
#############################################
# Function to check the emails in the queue #
#############################################
#############################################
check_queue()
{
#Function that stops the find command after X number of seconds
kill_it()
{
sleep $queue_timeout
PID=`ps aux | grep "find /var/spool/exim/input" | grep -v grep | awk '{print $2}'`
if [ -n "${PID}" ]; then
kill $PID
fi
}
# Set timer for parsing the queue unless it is 0 (unlimited)
if [ "$queue_timeout" != "0" ]; then
kill_it &
fi
# Find emails in exim's spool folder
queue_tmp=`find /var/spool/exim/input -name '*-H' | sed '$d' | xargs grep 'auth_id'`
echo "Done finding emails, starting to parse"
# Parse the queue_tmp variable to sort by auth_id
queue_senders=`echo -e "$queue_tmp" | cut -d: -f2 | sort | uniq -c | sort -rn | head -n5`
echo -e "Parsed `echo -e "$queue_tmp" | wc -l` emails out of $queue_total in the queue with a $queue_timeout second timeout"
echo -e "Highest number of emails in queue by auth_id:"
echo -e "$queue_senders"
echo ""
queue_senders_tmp=`echo "$queue_senders" | awk '{print $3}'`
for each in `echo "$queue_senders_tmp"`
do
echo "Example emails sent by $each:"
echo "$queue_tmp" | grep $each | cut -d: -f1 | head -n5
echo ""
done
}
clear
spamfu_init
To install this script
wget -O /scripts/spamfu.sh http://layer3.liquidweb.com/scripts/spamfu.sh
or
touch /scripts/spamfu.sh
chmod +x /scripts/spamfu.sh
/scripts/spamfu.sh