SSH bruteforce bots blacklisting script

This commit is contained in:
Emmanuel BENOîT 2012-07-28 17:15:51 +02:00
parent d9f75447a6
commit 652079119e
4 changed files with 270 additions and 0 deletions

1
README
View file

@ -1,5 +1,6 @@
This repo contains various scripts which I figure could be useful to someone.
backup/ A set of backup scripts
ban-ssh-morons/ A "SSH bruteforce attempts to iptables blacklist" daemon
License: WTFPL (http://sam.zoy.org/wtfpl/)

24
ban-ssh-morons/README Normal file
View file

@ -0,0 +1,24 @@
Ban SSH bruteforce bots
========================
This script maintains a blacklist based on repeated SSH log-in failures. I wrote
this after getting 800MB of authentication failure logs in one day on a home DSL
so the measures it takes are somewhat extreme.
The script normally runs in the background, reading /var/log/auth.log every
minute. When it detects 5 failed attempts from the same source, it will add an
iptables rule dropping all packets from that address. All addresses are also
added to a file and the iptables blacklist restored when it runs.
It is also possible to run the script with a specific input file. In this case
it will not fork to the background; it will load the file, find offending
entries, blacklist them, and exit. This allows the script to be "seeded" using
old logs.
Notes:
1/ Blacklist entries are *never* removed automatically.
2/ Updating the iptables blacklist is not efficient.
3/ If you want to customise the paths and various parameters, you need to
modify the script ("our $WHATEVER" variables).
4/ ban-ssh-morons.initd is an init script for Debian Squeeze.

View file

@ -0,0 +1,111 @@
#! /bin/sh
### BEGIN INIT INFO
# Provides: ban-ssh-morons
# Required-Start: $remote_fs
# Required-Stop: $remote_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: SSH moron banner
# Description: Launches the script that updates the SSH blacklist
### END INIT INFO
# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="Ban SSH morons"
NAME=ban-ssh-morons
DAEMON=/usr/local/sbin/$NAME.pl
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME
# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0
# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh
# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions
#
# Function that starts the daemon/service
#
do_start()
{
# Return
# 0 if daemon has been started
# 1 if daemon was already running
# 2 if daemon could not be started
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
|| return 1
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
$DAEMON_ARGS \
|| return 2
}
#
# Function that stops the daemon/service
#
do_stop()
{
# Return
# 0 if daemon has been stopped
# 1 if daemon was already stopped
# 2 if daemon could not be stopped
# other if a failure occurred
start-stop-daemon --stop --retry=TERM/30/KILL/5 --pidfile $PIDFILE
RETVAL="$?"
[ "$RETVAL" = 2 ] && return 2
# Many daemons don't delete their pidfiles when they exit.
rm -f $PIDFILE
return "$RETVAL"
}
case "$1" in
start)
[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
do_start
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
stop)
[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
do_stop
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
restart|force-reload)
#
# If the "reload" option is implemented then remove the
# 'force-reload' alias
#
log_daemon_msg "Restarting $DESC" "$NAME"
do_stop
case "$?" in
0|1)
do_start
case "$?" in
0) log_end_msg 0 ;;
1) log_end_msg 1 ;; # Old process is still running
*) log_end_msg 1 ;; # Failed to start
esac
;;
*)
# Failed to stop
log_end_msg 1
;;
esac
;;
*)
#echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2
exit 3
;;
esac
:

134
ban-ssh-morons/ban-ssh-morons.pl Executable file
View file

@ -0,0 +1,134 @@
#!/usr/bin/perl
use strict;
use Sys::Syslog;
use POSIX;
our $IDIOT_FILE = '/var/cache/ssh-morons';
our $AUTH_LOG = '/var/log/auth.log';
our $MAX_FAIL = 5;
our $BL_RULE = 'BLACKLIST';
sub writeLog
{
openlog( 'ban-ssh-morons' , 'nofatal,pid,perror' , 'LOCAL0' );
syslog( @_ );
closelog( );
}
sub checkForIdiots
{
my $MAX_FAIL = 5;
my $fn = shift;
my %idiots = ( );
if ( open( my $fh , $IDIOT_FILE ) ) {
while ( my $idiot = <$fh> ) {
chop $idiot;
$idiots{ $idiot } = $MAX_FAIL;
}
close $fh;
}
$fn = $AUTH_LOG unless defined $fn;
my $foundNewIdiots = 0;
open( my $fh , '<' , $fn )
or die "couldn't open $fn\n";
while ( my $line = <$fh> ) {
chop $line;
next unless $line =~ /sshd.*Failed password for( invalid user)? .* from (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) port.*/;
my $newIdiot = $2;
next if ( defined $idiots{ $newIdiot } && $idiots{ $newIdiot } >= $MAX_FAIL );
$idiots{ $newIdiot } = 0 unless defined $idiots{ $newIdiot };
$idiots{ $newIdiot } ++;
if ( $idiots{ $newIdiot } >= $MAX_FAIL ) {
writeLog( 'notice' , 'Adding %s to SSH blacklist' , $newIdiot );
$foundNewIdiots = 1;
}
}
close $fh;
writeLog( 'info' , 'Blacklist now contains %d entries' , scalar( keys( %idiots ) ) )
if $foundNewIdiots;
my @commands = ( );
open( my $fh , '>' . $IDIOT_FILE );
foreach my $cretin ( keys %idiots ) {
next unless $idiots{ $cretin } >= $MAX_FAIL;
print $fh "$cretin\n";
push @commands , "iptables -A $BL_RULE -s $cretin -j DROP";
}
close $fh;
system( 'iptables -F ' . $BL_RULE );
foreach my $cmd ( @commands ) {
system( $cmd );
}
}
sub mainLoop
{
my $mustExit = 0;
my $sigHandler = sub {
$mustExit = 1;
};
local $SIG{TERM} = $sigHandler;
local $SIG{INT} = $sigHandler;
my $signals = new POSIX::SigSet( &POSIX::SIGINT , &POSIX::SIGTERM , &POSIX::SIGHUP );
writeLog( 'info' , 'SSH blacklist updater starting' );
while ( !$mustExit ) {
sigprocmask( SIG_BLOCK , $signals , new POSIX::SigSet( ) );
checkForIdiots;
sigprocmask( SIG_UNBLOCK , $signals , new POSIX::SigSet( ) );
sleep 60;
}
writeLog( 'info' , 'SSH blacklist updater terminating' );
}
sub runDaemon
{
# Fork to background
exit 0 if fork( );
close( STDIN );
close( STDOUT );
close( STDERR );
open( STDIN , "/dev/null" );
open( STDOUT , ">/dev/null" );
open( STDERR , ">/dev/null" );
# Write PID file
my $pidFile = '/var/run/ban-ssh-morons.pid';
if ( -e $pidFile ) {
writeLog( 'crit' , 'PID file %s exists; exiting' , $pidFile );
die;
}
if ( open( my $f , '>' , $pidFile ) ) {
print $f "$$\n";
close $f;
} else {
writeLog( 'crit' , 'unable to create PID file %s' , $pidFile );
die;
}
# Run main loop
mainLoop;
# Delete PID file
unlink $pidFile;
}
if ( @ARGV ) {
checkForIdiots( $ARGV[0] );
} else {
runDaemon;
}