Some time ago I decided to do something about the number of SSH scans I was receiving on various machines under my control.  I am not overly concerned as I use keys for access, but there is always a possibility that a vulnerability exists which has not been publicised yet.  With this in mind, it is better to thwart the attempts early on.

I have seen people approach this various ways, and here is mine. It requires a database to keep a history of scans; I have used MySQL but any database should be fine.
The script will work under Linux and FreeBSD. Although configured to use Shorewall under Linux it would be trivial to replace the Shorewall commands with a line similar to “iptables -I INPUT -s -j DROP”.

Firstly, create a custom destination that points to our external script to process the reports from the SSH daemon:

destination ssh_scan { program(“/root/bin/sshscan”); };

Then create a filter to match the SSH daemon reporting an “Invalid User”:

filter f_ssh_scan   { program(“sshd.*”) and match(“Invalid user”); };

We can then create a log rule that passes any line that matches our filter to our script for processing:

log { source(s_sys); filter(f_ssh_scan); destination(ssh_scan); };

The contents of the script are as follows (including the code to create the MySQL database):


#!/usr/bin/perl

use strict;
use warnings;

use POSIX qw(uname);
use DBI();

$| = 1;

my $dbh;
my $scans;

my($uname_s, $uname_r) = (POSIX::uname())[0,2];

sub _db_connect
{
  $dbh   = DBI->connect("DBI:mysql:database","username","password") or die "Completely fucked";
  $scans = $dbh->prepare("SELECT COUNT(*) FROM sshd WHERE ip=? AND time > (now() - 1000)");
}

#CREATE TABLE `sshd` (
#  `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
#  `username` varchar(20) NOT NULL DEFAULT '',
#  `ip` varchar(16) NOT NULL DEFAULT '',
#  KEY `time` (`time`)
#) ENGINE=MyISAM DEFAULT CHARSET=latin1;

#
# Open the connection to the database
#
_db_connect();

open LOG, ">>/var/tmp/sshcan.log";

while()
{
  print LOG $_;

  chomp;

  if ( /[Invalid|Illegal] user (\S+) from (\d+\.\d+\.\d+\.\d+)/ )
  {
    my($name,$ip) = ($1,$2);

    _db_connect();

    if ( $dbh )
    {
      $dbh->do("INSERT INTO sshd (username,ip) VALUES ('$name','$ip')") or die "Failed to update database";

      $scans->execute($ip);
      if ( $scans->rows() )
      {
        my @cc = $scans->fetchrow_array();

        if ( $cc[0] >= 5 )
        {
          print LOG "dropping $ip\n";

          if ( $uname_s eq "FreeBSD" )
          {
            # Spoilt for choice here...

            #system("/sbin/ipfw -q add drop ip from $ip to any");
            # Delete a rule
            #open OUT, "|/sbin/ipf -rf -";
            # Add a rule
            open OUT, "|/sbin/ipf -f - 2>/dev/null";
            print OUT "block in quick on em0 from $ip/32 to any";
            close OUT;
            #system("/sbin/route add -host $ip 127.0.0.1 -blackhole > /dev/null 2>&1");
          }
          elsif ( $uname_s eq "Linux" )
          {
            system("/sbin/shorewall nolock drop $ip > /dev/null");
            system("/sbin/shorewall nolock save > /dev/null");
          }
        }
      }
      $scans->finish();
      $dbh->disconnect() if ($dbh);
    }
    else
    {
      print LOG "$DBI::err\n$DBI::errstr\n$DBI::state";
    }
  }
}
close(LOG);

# Exit zero so's we get called again
exit (0);

« »