SSH bruteforce bots blacklisting script
This commit is contained in:
parent
d9f75447a6
commit
652079119e
4 changed files with 270 additions and 0 deletions
1
README
1
README
|
@ -1,5 +1,6 @@
|
||||||
This repo contains various scripts which I figure could be useful to someone.
|
This repo contains various scripts which I figure could be useful to someone.
|
||||||
|
|
||||||
backup/ A set of backup scripts
|
backup/ A set of backup scripts
|
||||||
|
ban-ssh-morons/ A "SSH bruteforce attempts to iptables blacklist" daemon
|
||||||
|
|
||||||
License: WTFPL (http://sam.zoy.org/wtfpl/)
|
License: WTFPL (http://sam.zoy.org/wtfpl/)
|
||||||
|
|
24
ban-ssh-morons/README
Normal file
24
ban-ssh-morons/README
Normal 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.
|
111
ban-ssh-morons/ban-ssh-morons.initd
Executable file
111
ban-ssh-morons/ban-ssh-morons.initd
Executable 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
134
ban-ssh-morons/ban-ssh-morons.pl
Executable 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;
|
||||||
|
}
|
Loading…
Reference in a new issue