#!/usr/bin/perl -w ### popauth - maillog analysis daemon ### Copyright (C) 2002, Rudy Rucker ### ### This program is free software; you can redistribute it and/or modify ### it under the terms of the GNU General Public License as published by ### the Free Software Foundation, version 2 of the License. # eraper - This script watches the maillog file which your SMTP and POP # and/or IMAP server writes to. This has two functions: # [1] set up "pop before auth" # Derived From: http://spam.abuse.net/adminhelp/smPbS.shtml # + Cleans up popauth IPs automatically # + Fixed some little bugs # [2] look for email harvesters # + block in sendmail via the access file # + counter attack the harvesting IP - optional! # # Version Newest - http://www.monkeybrains.net/~rudy/example/ # # Version 0.4 - Thu Nov 21 22:29:52 PST 2002 # + fixed regex to include 'may be forged' # + changed loggin' to have sendmail id # + added startup script and install instructions to the end. # + cleaned up comments, added GPL to top # Version 0.3 - Tue Oct 29 17:45:40 PST 2002 # Version 0.2 # + added the harvesting blocking code # Version 0.1 # + mutated Neil Harkins and John Levine's script # # TODO # + to add some code to catch multiple single harvests from the same IP. # + all the HUP stuff is broke. The cronjob and start script hack is # a workaround. But hey, it works and is stable! # + get people to email me regex's for other SMTP and POP servers. # + make the counterattack leave an open TCP connection instead of pinging... use POSIX qw(setsid); # ------------------------------------------------------------- # # Change the following for your system: # ------------------------------------------------------------- # $POPAUTH_SPOOL_cleaning_interval = 7200; # in seconds (60 minutes = 3600 seconds) $ACCESS_REBUILD_SCIRPT = '/etc/mail/access.command.csh'; # example at end of this script $RAPER_FILE = '/etc/mail/access.RAPERS'; $POPHASH_FILE = '/etc/mail/access.POPHASH'; $POPAUTH_SPOOL = '/var/spool/popauth'; $MAILLOG_FILE = '/var/log/maillog'; $POPAUTH_LOG = '/var/log/popauth'; $POPAUTH_PID = '/var/run/popauth.pid'; # these IPs do not need popauth and are not harvesting sources... # (probalby your local network will work here...) $LOCAL_MATCH = "(69.1.75.|127.0.0.1)"; #simple example # here are some common unix commands needed $TAIL = '/usr/bin/tail'; $LOGGER = '/usr/bin/logger'; $DEBUG = undef; # YOU DON'T NEED TO EDIT MUCH BELOW HERE, UNLESS YOU WANT TO HAVE SOME FUN! $DO_COUNTERATTACK = 0; # set to 1 to cause trouble # the following variables are only needed if you want to counterattack # these commands will never be run otherwise. $LCRZOEX = '/usr/local/bin/lcrzoex'; $ARP = '/usr/sbin/arp'; $AWK = ' /usr/bin/awk'; $NETSTAT = '/usr/bin/netstat'; $GREP = '/usr/bin/grep'; # ------------------------------------------------------------- # # We are startingi! # Set some perl magic. # ------------------------------------------------------------- # $| = 0; chdir '/' or die "Can't chdir to /: $!"; umask 0; defined(my $pid = fork) or die "Can't fork: $!"; exit if $pid; setsid or die "Can't start a new session: $!"; # ------------------------------------------------------------- # # Don't trust any of the settings from above... test variables # ------------------------------------------------------------- # -d $POPAUTH_SPOOL or mkdir $POPAUTH_SPOOL, 0750 or die "Can't make spool path ($POPAUTH_SPOOL): $!\n"; -f $MAILLOG_FILE or die "Can't find mail syslog file ($MAILLOG_FILE): $!\n"; -x $TAIL or die "Can't execute tail, $TAIL: $!\n"; unless (-x $ACCESS_REBUILD_SCIRPT) { print <> access.source; # make a temporary file and then use mv /usr/sbin/makemap hash access.junk < /etc/mail/access.source; mv access.junk.db access.db; EOexample exit; } # ------------------------------------------------------------- # # figure out if we are really going to counterattack. # ------------------------------------------------------------- # my ($gw_eth, $my_eth); if ($DO_COUNTERATTACK) { # if lcrzoex is installed, this script will counter attack email harvesters. # to do this, we need to know the gateway's ethernet MAC. if (-x $LCRZOEX and -x $ARP and -x $NETSTAT and -x $GREP and -x $AWK) { my $gw_ip; ($my_eth,$gw_ip) = split(' ', `$NETSTAT -rn | $GREP default | $AWK '{print \$2" "\$6}'`); # ha! awk skills! $gw_ip =~ /^[0-9.]{6,17}$/ or undef $gw_ip; $gw_eth = `$ARP $gw_ip` if $gw_ip; $gw_eth =~ s/^.*(..:..:..:..:..:..).*$/$1/ or undef $DO_COUNTERATTACK; } else { print STDERR "Skipping counter attack... you don't have $0 properly configured...\n"; undef $DO_COUNTERATTACK; } } # ------------------------------------------------------------- # # Write the PID to the the pid file # ------------------------------------------------------------- # if (-f $POPAUTH_PID) { print "Unclean shutdown of popauth! pid file remains!\n"; my $checkps = `/bin/ps -ax | /usr/bin/grep 'popauth' | /usr/bin/grep -v "^ \*$$ " | /usr/bin/grep -v "grep "`; if ($checkps =~ m!/usr/local/sbin/popauth!) { print "Old popauth found running! I am bailing!\n$checkps\n\n"; exit; } else { unlink $POPAUTH_PID; } } open(PID,">$POPAUTH_PID") or die "Can't open pid file ($POPAUTH_PID): $!\n"; print PID "$$\n"; close(PID); # ------------------------------------------------------------- # # start the log file and stamp it with a starting time # ------------------------------------------------------------- # open(LOG,">>$POPAUTH_LOG") || die "Can't open popauth log ($POPAUTH_LOG): $!\n"; my $starttime = scalar localtime; print LOG "\n$starttime Starting popauth at pid $$\n"; print LOG "Loacl Match = $LOCAL_MATCH\n"; # Lets just do this calculation once. # Convert the clenaing interval from seconds to days... $POPAUTH_SPOOL_cleaning_interval_indays = $POPAUTH_SPOOL_cleaning_interval/(60*60*24); # ------------------------------------------------------------- # # this is a deamon, so tweak the STDIN OUT and ERR handles. # ------------------------------------------------------------- # open STDIN, '/dev/null' or die "Can't read /dev/null: $!"; open STDOUT, '>/dev/null'; open STDERR, '>/dev/null'; select(LOG); $| = 1; select(STDOUT); $| = 1; $SIG{'USR1'} = 'USR1_handler'; $SIG{'USR2'} = 'USR2_handler'; $SIG{'HUP'} = 'HUP_handler'; $SIG{'TERM'} = 'handler'; $SIG{'INT'} = 'handler'; $SIG{'QUIT'} = 'handler'; $SIG{'KILL'} = 'handler'; # ------------------------------------------------------------- # # make database of IPs seen so far ... clean up old entries # ------------------------------------------------------------- # RESTART: undef %ip_ok; &clean_POPAUTH_SPOOL(); &rebuild_access_db(); print LOG "HUP - restarted ", scalar localtime,"\n" if $HUP_occured; $HUP_occured = 0; # ------------------------------------------------------------- # # THE IS THE MAIN LOOP OF THE DEAMON! # now watch log file and add new IPs as encountered # ------------------------------------------------------------- # open(MAILLOG,"$TAIL -f $MAILLOG_FILE |") || die("Can't $TAIL -f $MAILLOG_FILE"); while() { /^STOP/ and last; # --------------------------------------------------------- # # POPAUTH - see end of file for explaination of this regex. # --------------------------------------------------------- # if (/^([A-Za-z]{3}\s+\d+\s+\d{2}\:\d{2}\:\d{2}).+ (pop3d|imapd)\: (AUTH|LOGIN), user=(\S+), ip=.*\:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]/o) { my ($time, $user, $ip) = ($1, $4, $5); print LOG "Local Match on $ip\n" if ($DEBUG and $ip =~ /^$LOCAL_MATCH/o); $ip =~ /^$LOCAL_MATCH/o and next; #this could be more complex... if ($ip_ok{$ip} eq 'OK') { print LOG "$time $user $ip $ip_ok{$ip} already exists\n" if $DEBUG; open(TEMP,"> $POPAUTH_SPOOL/$ip"); # retouch the file! close(TEMP); } else { print LOG "$time $user $ip\n"; $ip_ok{$ip} = 'OK'; open(TEMP,"> $POPAUTH_SPOOL/$ip"); # touch the file close(TEMP); &rebuild_access_db(); } (time - $POPAUTH_SPOOL_last_cleaned > $POPAUTH_SPOOL_cleaning_interval) and &clean_POPAUTH_SPOOL(); # --------------------------------------------------------- # # RAPERS - Check for email rapers! # --------------------------------------------------------- # } elsif (/\[\d+\]: ([^:]+): ([^:]+)\.\.\. (User unknown)/) { $mark_id{$1}++; print LOG "$2 $3 #$1\n"; } elsif (/\[\d+\]: ([^:]+): from.*\[([0-9\.]{7,15})\]( .may be forged.)?$/ and defined $mark_id{$1}) { my $ip = $2; my $sendmail_id = $1; # TODO: if the harvester had more than 4 'User Unknown's then block IP. ... this may # work better if a running tally for an IP over the past hour was kept. if ($mark_id{$1} > 4 and $ip !~ /^$LOCAL_MATCH/) { # check for dupes open A, "<$RAPER_FILE"; while () { /^$ip\ / or next; undef $ip; last; } close A; # time to block IP! if ($ip) { open A, ">>$RAPER_FILE" or next; # hmmm... should never actually do 'next' flock A, 2; seek A, 0, 2; # get exclusive lock, seek to end of file print A "$ip 550 $ip blocked due to email harvesting. Contact support\n"; print "$ip 550 $ip blocked due to email harvesting. Contact support\n"; flock A, 8; close A; # release flock, close file system("$ACCESS_REBUILD_SCIRPT") == 0 or print LOG "Error running $ACCESS_REBUILD_SCIRPT, $?\n"; # if lcrzoex is on the system, gw_eth will be defined (see above) # and a sounter-strike on the RAPER ip will happen. print LOG ×tamp(), " RAPER $ip - $sendmail_id\n"; &counter_attack($ip, $my_eth, $gw_eth) if ($DO_COUNTERATTACK); } } undef $mark_id{$1}; # clean up } } close(MAILLOG); $HUP_occured == 1 and goto RESTART; close(LOG); exit(1); # ------------------------------------------------------------- # # some subroutines... # ------------------------------------------------------------- # sub counter_attack { # you could modify this routine... if ($child = fork) { return 1; } elsif (defined $child) { sleep 1; my ($ip, $my_eth, $gw_eth) = @_; print LOG ×tamp(), " counter-strike on $ip\n"; for my $a (3,4,11..21,51..57,61..69,212..221) { for (1..666) { my ($b, $c, $d) = (int(rand(254))+1,int(rand(254))+1,int(rand(254))+1); `$LCRZOEX 71 $my_eth 00:cc:cc:cc:cc:cc $gw_eth $a.$b.$c.$d $ip`; } } print LOG ×tamp(), " counter-strike on $ip complete\n"; exit; } else { print LOG ×tamp(), " Fork Error $!\n"; return undef; } } sub rebuild_access_db { open (OUT,">$POPHASH_FILE"); foreach my $key (keys %ip_ok) { print OUT "Connect:$key\tRELAY\n"; } close (OUT); system("$ACCESS_REBUILD_SCIRPT") == 0 or print LOG "Error running $ACCESS_REBUILD_SCIRPT, $?\n"; } sub clean_POPAUTH_SPOOL { # Auth'd IPs are read in, and old ones cleaned up. my $datetime = ×tamp(); print LOG "$datetime Cleaning $POPAUTH_SPOOL\n" if $DEBUG; while (<$POPAUTH_SPOOL/*>) { /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/o or next; # -M gives mod time sice script started... if (((-M $_) + (time-$^T)/86400) > $POPAUTH_SPOOL_cleaning_interval_indays) { print LOG "\tNuking $1\n" if $DEBUG; print LOG "\tERROR: Couldn't remove stale IP $_ ($!)\n" unless unlink $_; undef $ip_ok{$1}; #They need to pop again! } else { $m1 = -M $_; $m2 = (time-$^T)/86400; print LOG "\tAdding $& ($m1 + $m2 > $POPAUTH_SPOOL_cleaning_interval_indays)\n" if $DEBUG; $ip_ok{$1} = "OK"; } } $POPAUTH_SPOOL_last_cleaned = time; } sub timestamp { my $datetime = scalar localtime; $datetime =~ s/^\w{3} //; $datetime =~ s/ \d\d\d\d$//; return $datetime; } # ------------------------------------------------------------- # # This signal handler stuff was more for me to learn about how # signals work with perl... # ------------------------------------------------------------- # sub HUP_handler { # ------------------------------------------------------------- # # This closes and reopens the LOG. Also, the MAILLOG is closed... # The main 'while' loop blocks until the maillog gets another line # appended to it. The $HUP_occured flag will restart that while loop and # the MAILLOG handle will be recreated once the while loop exits. # ... Instead of using the HUP to restart this at midnight, I stop # and start this script with a cron job... the while loop blocking # is something I need TODO. I bet there is a way to timeout on a # file line read.... oh well, the cronjob hack works. # ------------------------------------------------------------- # close(LOG); open(LOG,">>$POPAUTH_LOG") || die "Can't open popauth log ($POPAUTH_LOG): $!\n"; print LOG " HUP - ", scalar localtime, "\n"; $HUP_occured = 1; close(MAILLOG); # until the maillog file gets another line added to it `$LOGGER -p mail.info "popauth got a HUP signal"`; print LOG " HUP - Closed Maillog\n" if $DEBUG; return 1; } sub USR2_handler { # I just learned signal handlers in Perl! Check this out! print LOG " USR2 - ", scalar localtime, " - Hello there!\n"; } sub USR1_handler { # This handler dumps out the contents of the %ip_ok hash to a dump file. print LOG " USR1 - ", scalar localtime, " - Dumping popaugh hash:!\n"; if ($child = fork) { return 1; # let the parent return. The child will dump the hash. } elsif (defined $child) { for (keys %ip_ok) { print LOG "$_\n"; } exit; } else { print LOG " Fork error\n"; } } sub handler { local($sig) = @_; print LOG " $sig - ", scalar localtime, " - $0 exiting.\n"; close(MAILLOG); close(LOG); `$LOGGER -p mail.info "popauth exited on a $sig signal"`; unlink $POPAUTH_PID; exit(0); } __END__ You have to do these 6 steps to get this system up and running. [1] Save this script to your mail server! This step is obvious, but needed. I named this script: /usr/local/sbin/popauth [2] Setting up this script to match your IMAP or POP server log lines. In your mail syslog file, you need to figure out the format that your popper program logs in. Here are some examples: FreeBSD 4.3 uw-ipop3d (Auth and Login lines appear) Aug 14 00:03:57 hostname ipop3d[30744]: Auth user=jenna host=56k-33.monkeybrains.net [209.21.40.233] nmsgs=0/0 /^([A-Za-z]{3}\s+\d+\s+\d{2}\:\d{2}\:\d{2}).+ ipop3d\[\d+\]\: (Auth|Login) user=(\S+) host=.+ \[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\] .*$/o FreeBSD 4.7 courier-imap (pop and imap users) Oct 29 16:25:48 hostname imapd: LOGIN, user=george, ip=[::ffff:41.206.193.62] /^([A-Za-z]{3}\s+\d+\s+\d{2}\:\d{2}\:\d{2}).+ (pop3d|imapd)\: (AUTH|LOGIN), user=(\S+), ip=.*\:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]/o unknown OS popper none /^([A-Za-z]+\s+\d+\s+\d+\:\d+\:\d+).+POP login for \"(.+)\".+\s(\d+\.\d+\.\d+.\d+).*$/ Usually, your mail syslogd logs to this file: /var/log/maillog [3] You may need to alter the regex on catching email harvesters. Here is how I identify a email harvester with sendmail on my FreeBSD box: Sep 17 02:01:03 pizza sm-mta[80948]: g8H90qdf080948: ... User unknown Sep 17 02:01:23 pizza sm-mta[80948]: g8H90qdf080948: ... User unknown Sep 17 02:01:45 pizza sm-mta[80948]: g8H90qdf080948: ... User unknown Sep 17 02:02:09 pizza sm-mta[83014]: g8H90qdf080948: from=, size=0, class=0, nrcpts=0, bodytype=8BITMIME, proto=ESMTP, daemon=MTA, relay=paperboy1.cdnow.com [12.33.58.160] Nov 21 21:05:22 mail sm-mta[2766]: gAM55KIn002766: from=, size=0, class=0, nrcpts=0, proto=ESMTP, daemon=MTA, relay=host65.200-43-43.telecom.net.ar [200.43.43.65] (may be forged) [4] Set up the rebuild script for your sendmail access file Here is the script I use to rebuild the access.db file. I made a separate script to do this, because I sometimes want to run it manually. I actually have 3 access files and use 'cat' to concatenate the files. My MANUAL file has some IPs I always want to relay for. The POPHASH has has IPs who are allowed due to a recent POP or IMAP login. the RAPERS are the evil email harvesting IPs. Block em! ------------------------------------------ /etc/mail/access.command.csh #!/bin/csh # # /etc/mail/access.command.csh # Rebuild the access.db file for sendmail chdir /etc/mail/; rm access.source; # cat, in order! cat access.MANUAL access.POPHASH access.RAPERS >> access.source; # make a temporary file and then use mv /usr/sbin/makemap hash access.junk < /etc/mail/access.source; mv access.junk.db access.db; [5] You need a cronjob. You newsyslogd rotates your mail log file. Usually at midnight. add this to your root crontab: ------------------------------------------ crontab (for root) 58 23 * * * /usr/local/etc/rc.d/popauth.sh stop 3 0 * * * /usr/local/etc/rc.d/popauth.sh start [6] now you need the startup script! on BSDs, this should live in the /usr/local/etc/rc.d directory. ON Linux, it goes in /etc/inet.d or somewhere. Make sure to start it when your server reboots! oh, and chmod it to 744. ------------------------------------------ /usr/local/etc/rc.d/popauth.sh #!/bin/sh GREP=/usr/bin/grep AWK=/usr/bin/awk LOGGER=/usr/bin/logger POPAUTH=/usr/local/sbin/popauth PIDFILE=/var/run/popauth.pid case "$1" in stop) if [ -f $PIDFILE ]; then kill -TERM `cat $PIDFILE` $LOGGER -p mail.info "Stopping popauth" sleep 1; fi for BADPID in `/bin/ps -ax | $GREP popauth | $GREP perl | $AWK '{print $1}'`; do echo "Bad popauth! killing!" sleep 2; kill -9 $BADPID done ;; -h) echo "Usage: `basename $0` { start | stop | restart }" ;; *) if [ -f $PIDFILE ]; then kill -TERM `cat $PIDFILE` $LOGGER -p mail.info "Restarting popauth" sleep 1; fi for BADPID in `/bin/ps -ax | $GREP popauth | $GREP perl | $AWK '{print $1}'`; do echo "Bad popauth! killing!" sleep 2; kill -9 $BADPID done $LOGGER -p mail.info "Starting popauth" $POPAUTH ;; esac