Viewing 2 replies - 1 through 2 (of 2 total)
  • Hi muskokaslowpitcher,

    Brilliant! this is exactly what I’m looking for. Would you be so kind sharing the script here so I and others can play (work, that is) with it. Thanks so much in advance. Have a good one:)

    Thread Starter muskokaslowpitcher

    (@muskokaslowpitcher)

    Ok, here it is. It was adapted from a script used to gather mail statistics.

    #!/usr/bin/perl -w
    
    # wp_ip6_bans --
    # copyright (c) 2015 Jeremy Baker <jab@mbcs.ca>
    # copyright (c) 2013 KokelNET
    # copyright (c) 2013 Tobias Hachmer <network@kokelnet.de>
    # copyright (c) 2000-2007 ETH Zurich
    # copyright (c) 2000-2007 David Schweikert <david@schweikert.ch>
    # released under the GNU General Public License
    
    #####################################################################
    #####################################################################
    #####################################################################
    
    use strict;
    use File::Tail;
    use Getopt::Long;
    use POSIX 'setsid';
    use NetAddr::IP;
    use Date::Parse;
    use Data::Validate::IP qw(is_ipv6);
    use Data::Validate::IP qw(is_ipv4);
    use Email::Simple;
    use Email::Sender::Simple qw(sendmail);
    use Email::Simple::Creator;
    use BerkeleyDB;
    use BerkeleyDB::Hash;
    
    my $VERSION = "0.3.1";
    
    # config
    
    my $daemon_logfile = '/var/log/wp_ip6_bans.log';
    my $daemon_pidfile = '/var/run/wp_ip6_bans.pid';
    my $ban_limit = 3;
    my $ban_search_time = 600; # search window - 10 min
    my $ban_time = 3600; # 1 hour
    my $rec_ban_limit = 3;
    my $rec_ban_search_time = 86400; # search window - 1 day
    my $rec_ban_time = 604800; # 7 days
    my $ban_grace_time = 30;  # depending on the speed of logging and reading the logs, sometimes there is a lapse between a ban and activity ceasing
    my $notification_email ='jab@mbcs.ca';
    my $ipv4_whitelist = '';
    my $ipv6_whitelist = '';
    
    # global variables
    my $logfile = '/var/log/wordpress';
    my $db = new BerkeleyDB::Hash( -Filename => '/var/lib/wp_ip6_bans.dbm',
    				-Flags => DB_CREATE ) or die "Cannot open file: $!";
    
    my %opt = ();
    
    # prototypes
    sub daemonize();
    sub process_line($);
    
    sub usage
    {
    	print "usage: wp_ip6_bans [*options*]\n\n";
    	print "  -h, --help         display this help and exit\n";
    	print "  -v, --verbose      be verbose about what you do\n";
    	print "  -V, --version      output version information and exit\n";
    #	print "  -c, --cat          causes the logfile to be only read and not monitored\n";
    	print "  -l, --logfile f    monitor logfile f instead of /var/log/wordpress\n";
    	print "  -t, --logtype t    set logfile's type (default: syslog)\n";
    	print "  -d, --daemon       start in the background\n";
    	print "  -m, --mail         send email for each ban\n";
    	print "  --daemon-pid=FILE  write PID to FILE instead of /var/run/wp_ip6_bans.pid\n";
    	print "  --daemon-log=FILE  write verbose-log to FILE instead of /var/log/wp_ip6_bans.log\n";
    
    	exit;
    }
    
    sub main
    {
    	Getopt::Long::Configure('no_ignore_case');
    	GetOptions(\%opt, 'help|h', 'cat|c', 'logfile|l=s', 'logtype|t=s', 'version|V',
    		'year|y=i', 'verbose|v', 'daemon|d!',
    		'daemon_pid|daemon-pid=s', 'mail|m',
    		'daemon_log|daemon-log=s'
    		) or exit(1);
    	usage if $opt{help};
    
    	if($opt{version}) {
    		print "wp_ip6_bans $VERSION by jab\@mbcs.ca\n";
    		exit;
    	}
    
    	$daemon_pidfile = $opt{daemon_pid} if defined $opt{daemon_pid};
    	$daemon_logfile = $opt{daemon_log} if defined $opt{daemon_log};
    
    	daemonize if $opt{daemon};
    	my $logfile = defined $opt{logfile} ? $opt{logfile} : '/var/log/wordpress';
    	my $file;
    	my $sl;
    	if($opt{cat}) {
    		$file = $logfile;
    	}
    	else {
    		$file = File::Tail->new(name=>$logfile, maxinterval=>2);
    	}
    	while (defined($sl=$file->read)) {
    	  if(($sl =~ /Authentication failure for .* from /)or($sl =~ /Blocked user enumeration attempt from /)) {
    	      process_line($sl);
    	  }
    	}
    }
    
    sub daemonize()
    {
    	open STDIN, '/dev/null' or die "wp_ip6_bans: can't read /dev/null: $!";
    	if($opt{verbose}) {
    		open STDOUT, ">>$daemon_logfile"
    			or die "wp_ip6_bans: can't write to $daemon_logfile: $!";
    	}
    	else {
    		open STDOUT, '>/dev/null'
    			or die "wp_ip6_bans: can't write to /dev/null: $!";
    	}
    	defined(my $pid = fork) or die "wp_ip6_bans: can't fork: $!";
    	if($pid) {
    		# parent
    		open PIDFILE, ">$daemon_pidfile"
    			or die "wp_ip6_bans: can't write to $daemon_pidfile: $!\n";
    		print PIDFILE "$pid\n";
    		close(PIDFILE);
    		exit;
    	}
    	# child
    	setsid			or die "wp_ip6_bans: can't start a new session: $!";
    	open STDERR, '>&STDOUT' or die "wp_ip6_bans: can't dup stdout: $!";
    }
    
    sub process_line($)
    {
        my $line = shift;
        my $stime=substr $line, 0 , 19;
        my $time=str2time(substr $line, 0, 19);
        my $data;
        my $exists=0;
        my $attempts;
        my $bans;
        my $ban_it=0;
        my $first_time;
        my $first_ban_time;
        my $last_ban_time;
        my $btime = $ban_time;
        my $ipset = '';
        my $log_message ='';
        my $log_message2 = 'not blocked';
        my @values = split(' ',$line);
        my $ip = $values[-1];
        if(is_ipv6($ip)) {
    	$ipset = 'F2BLIST6';  # this should be added to a configuration variable
        }
        elsif(is_ipv4($ip)) {
    	$ipset = 'F2BLIST';  # this should be added to a configuration variable
        }
        else {
    	return;
        }
    
        # database format $ip, $attempts:$bans:$first_time:$first_ban_time:$last_ban_time
        # find out if $ip is in database, and load variables
        if ($db->db_get( $ip, $data) == 0) { # record exists
    	( $attempts, $bans, $first_time, $first_ban_time, $last_ban_time ) = split ':', $data;
    	if (($time-$last_ban_time) < $ban_grace_time) {
    	    print "at $time $ip ignored because it was banned at $last_ban_time\n" if $opt{verbose};
    	    return;
    	}
    	$exists = 1;
        }
    
        # I immediately ban any attempts to login as admin, or use user enumeration
    
        if(($line =~ /Authentication failure for admin from /)or($line =~ /Blocked user enumeration attempt from /)) {
    	$ban_it=1;
    	$log_message = 'blocked after 1 attempt';
        }
        elsif($line =~ /Authentication failure for .* from /) {
    	if ($exists == 1) { # record exists
    	    if (($time-$first_time) < $ban_search_time) {
    		if ($attempts < ($ban_limit -1)) { # increment counter
    		    $attempts = $attempts + 1;
    		    $log_message = 'incremented attempts';
    		}
    		else { # ban ip,
    		    $ban_it=1;
    		    $log_message = 'blocked after repeated attempts';
    		}
    	    }
    	    else { # reset counter to 1, reset time
    		$attempts = 1;
    		$first_time = $time;
    		$log_message = 'reset attempts and time';
    	    }
    	}
    	else { # create record
    	    $attempts = 1;
    	    $bans = 0;
    	    $first_time = $time;
    	    $first_ban_time = 0;
    	    $last_ban_time = 0;
    	    $log_message = 'record created';
    	}
        }
    
        if ($ban_it == 1) {
    	$attempts = 0;
    	$first_time = 0;
    	$last_ban_time = $time;
    	if ($exists == 1) { # record exists
    	    if (($time-$first_ban_time) < $rec_ban_search_time) { # within recidive search window
    		if ($bans < ($rec_ban_limit -1)) { # increment and ban normally
    		    $bans = $bans + 1;
    		    $log_message2 = 'bans incremented';
    		}
    		else { # ban recidively,
    		    $btime=$rec_ban_time;
    		    $bans = 0;
    		    $first_ban_time = 0;
    		    $log_message2 = 'blocked recidively and reset';
    		}
    	    }
    	    else { # outside recidive window - reset counter to 1, reset time, ban normally
    		$bans = 1;
    		$first_ban_time = $time;
    		$log_message2 = 'ban count reset';
    	    }
    	}
    	else { # new record
    	    $bans = 1;
    	    $first_ban_time = $time;
    	    $log_message2 = 'record created';
    	}
    	# the actual ban command
    	system("/usr/sbin/ipset add $ipset $ip timeout $btime -exist");
    
    	# send an optional email
    	if ($opt{mail}) {
    	    my $email = Email::Simple->create(
    	      header => [
    		To	=> '"your name here" <someone@domain.tld>',
    		From	=> '"Wordpress Banning Daemon" <banning-daemon@domain.tld>',
    		Subject => "$ip banned for $btime seconds",
    	      ],
    	      body => "A firewall ban was triggered by the following log line \n$line",
    	    );
    	    sendmail($email);
    	}
        }
        # we saw a line, so update the database
        $db->db_put ( $ip, join(':', $attempts, $bans, $first_time, $first_ban_time, $last_ban_time) );
        print "at $stime $ip $attempts $bans $first_time $first_ban_time $last_ban_time $log_message and $log_message2\n" if $opt{verbose};
    }
    
    main;
    
    __END__
    
    =head1 NAME
    
    wp_ip6_bans.pl - script to ban brute force attempts on wordpress at the firewall
    
    =head1 SYNOPSIS
    
    B<wp_ip6_bans.pl> [I<options>...]
    
         --man          show man-page and exit
     -h, --help         display this help and exit
     -v, --verbose      be verbose about what you do
     -V, --version      output version information and exit
     -c, --cat          causes the logfile to be only read and not monitored
     -l, --logfile f    monitor logfile f instead of /var/log/wordpress
     -t, --logtype t    set logfile's type (default: syslog)
     -d, --daemon       start in the background
     -m, --mail	    send email for each ban
     --daemon-pid=FILE  write PID to FILE instead of /var/run/shoregraph.pid
     --daemon-log=FILE  write verbose-log to FILE instead of /var/log/shoregraph.log
    
    =head1 DESCRIPTION
    
    This script does parse syslog and bans IPs that make too many attempts
    to log in to wordpress
    
    =head2 Log-Types
    
    The following types can be given to --logtype:
    
    =over 10
    
    =item syslog
    
    Traditional "syslog" (default)
    
    =item metalog
    
    Metalog (see http://metalog.sourceforge.net/)
    
    =back
    
    =head1 COPYRIGHT
    
    Copyright (c) 2015 Jeremy Baker <jab@mbcs.ca>
    Copyright (c) 2013 KokelNET
    Copyright (c) 2013 Tobias Hachmer <tobias@hachmer.de>
    Copyright (c) 2000-2007 by ETH Zurich
    Copyright (c) 2000-2007 by David Schweikert
    
    =head1 LICENSE
    
    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; either version 2 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    
    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
    
    =head1 AUTHOR
    
    S<Jeremy Baker E<lt>jab@mbcs.caE<gt>>
    
    =cut
    
    # vi: sw=8

Viewing 2 replies - 1 through 2 (of 2 total)
  • The topic ‘ipv6 banning’ is closed to new replies.