diff --git a/README b/README index de9ad5d..5e84348 100644 --- a/README +++ b/README @@ -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/) diff --git a/ban-ssh-morons/README b/ban-ssh-morons/README new file mode 100644 index 0000000..61e1ca9 --- /dev/null +++ b/ban-ssh-morons/README @@ -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. diff --git a/ban-ssh-morons/ban-ssh-morons.initd b/ban-ssh-morons/ban-ssh-morons.initd new file mode 100755 index 0000000..87d9017 --- /dev/null +++ b/ban-ssh-morons/ban-ssh-morons.initd @@ -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 + +: diff --git a/ban-ssh-morons/ban-ssh-morons.pl b/ban-ssh-morons/ban-ssh-morons.pl new file mode 100755 index 0000000..6773f11 --- /dev/null +++ b/ban-ssh-morons/ban-ssh-morons.pl @@ -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; +}