home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
linuxmafia.com 2016
/
linuxmafia.com.tar
/
linuxmafia.com
/
pub
/
linux
/
security
/
ssh-dictionary-attack-blacklist
< prev
next >
Wrap
Text File
|
2004-10-28
|
12KB
|
346 lines
Archivist's note: The successor to this initial version is in
subdirectory ssh_sentry, within this directoriy.
From rick
Date: Mon, 27 Sep 2004 10:28:34 -0400 (EDT)
From: Victor Danilchenko <danilche@cs.umass.edu>
To: secureshell@securityfocus.com
Subject: Re: OpenSSH -- a way to block recurrent login failures?
On Tue, 21 Sep 2004, Victor Danilchenko wrote:
> We are looking for a way to temporarily block hosts from which we
> receive a given number of sequential failed login attempts, not
> necessarily within the same SSH session (so MaxAuthTries is not
> enough). The best solution I could come up with so far would be to
> run OpenSSH through TCPWrappers, and set up a log watcher daemon which
> would edit /etc/hosts.deny on the fly based on the tracked number of
> failed logins for each logged host.
>
> Is there a better solution known for the sort of problems we have been
> plagued with lately -- repeated brute-force crack attempts from remote
> hosts? I looked on FreshMeat and I searched the mailing lists, only to
> come up empty-handed.
>
> Thanks in advance,
Thanks to all who replied with the suggestions. Alas, none of them were
quite suitable.
The IPTables manipulation is a fine idea, but we need a solution that
runs in a very heterogeneous environment. At the very least, we are
looking at protecting Redhat Linux, OS/X, and Solaris systems.
Portsentry is IMO a little too complicated to easily deploy on a wide
number of systems -- we need a fire-and-forget solution (ideally a
simple modification to the sshd_config file, but that obviously is not
in the cards).
In the end, I wrote a perl script that did solved the problem the brute
way -- trail the SSHD logs, and modify /etc/hosts.deny on the fly.
Attached in this script, should anyone here find it useful. The next
logical step would be to turn this into a distributed solution where
blacklists could be shared among individual nodes. Would be nice to have
a DNS-based blacklisting solution eventually, similar to how SPAMming
can be handled by MTAs...
Note that the attached script has been stripped of our information
before being posted, so a typo or two may have crept in somewhere during
the cleanup editing. It currently works on OS/X and Linux, I haven't yed
added the code to make it work on Solaris.
--
| Victor Danilchenko | When in danger or in doubt, |
| danilche@cs.umass.edu | run in circles, scream, and shout. |
| CSCF | 5-4231 | Robert Heinlein |
[Script is intended to be named "sshd-sentry".]
#!/usr/bin/perl -w
# Written by Victor Danilchenko, 09/22/2004
#
# The purpose of this script is to monitor the sshd logs, detect
# repeated failed login attempts, and blacklist the hosts whence
# such attempts originate.
#
# Supports Linux and OS/X
#
#################################################################
#
# Changelog:
# 09/22/2004 Victor Danilchenko
# Added notification by mail capability, via
# direct SMTP injection
#
################################################################
use strict;
use Getopt::Long;
use IO::Seekable;
use IO::Socket::INET;
my $name = "sshd_sentry";
my $pidfile = "/var/run/$name";
my $hosts_deny = "/etc/hosts.deny";
my $hosts = {};
my @bad_users = sort qw(root user test admin guest);
my $baddies = join (", ", @bad_users);
my $tag = 'ROBOSENTRY';
my ($help, $file, $restart, $interval, $threshold, $duration, $penalty);
my $shost = (split(/\./, (`hostname`)[0]))[0]; chomp $shost;
my $mydomain = "yourdomain.com";
my $SMTPserver = "smtp.$mydomain";
my @sysmail = ("sysadmin\@$mydomain");
# Various places where system logs can be found on different platforms
my @files = qw(/var/log/messages /var/log/system.log /var/adm/messages);
my $file_default;
for (@files) { if (-e $_) { $file_default = $_; last;} }
my $interval_default = 10;
my $threshold_default = 8;
my $duration_default = "1 day";
my $penalty_default = 1;
sub help () {
my $filr = " " x length($name);
return << "EOT";
Usage: $name [-h | --help]
$filr [-r | --restart ]
$filr [-f | --file <log file name> ]
$filr [-i | --interval <polling interval> ]
$filr [-t | --threshold <threshold number of failures> ]
$filr [-d | --duration <duration of time to disable host for> ]
$filr [-p | --penalty <penalty for known-exploitable accounts> ]
help Show this message
restart Focibly restart $name, kill current process if needed
file Specify the log file name to use
default: $file_default
interval Number of seconds between polling of the log file
default: $interval_default
threshold Number of detected failed logins, before the host is blocked.
Notice that the user names which are commonly used in exploits
($baddies) count double.
default: $threshold_default.
duration Duration of time for which the host which went over the failure
threshold should be blocked. Must be a number followed by units
(e.g. '1 hr' or '3 days'). Unqualified number is treated as hours.
default: $duration_default
penalty The extra points to count as authentication failures for accounts
commonly used in exploits ($baddies)
default: $penalty_default
EOT
}
sub mail_to_users {
# We go this bizarre route because OS/X systems tend to not be
# properly configured for local mail delivery, so we need to
# inject the mail straight into the SMTP stream.
my $text = shift;
my $subject = shift;
my @users = @_; @users = @sysmail unless @users;
my $socket=IO::Socket::INET->new("$SMTPserver:25");
#my $socket = \*STDOUT;
print $socket ("HELO $shost.$mydomain\n");
print $socket ("MAIL FROM: root\@$shost.$mydomain\n");
print $socket ("RCPT TO: ", join ("\nRCPT TO: ", @users), "\n");
print $socket ("DATA\n");
print $socket ("To: ", join (",", @users), "\n");
print $socket ("Subject: $subject\n\n");
print $socket($text);
print $socket ("\n.\nQUIT\n");
close $socket;
}
sub die_with_mail($;@) {
my $text = shift;
my @users = @_; @users = @sysmail unless @users;
my $subject = "$name died on $shost";
mail_to_users ($text, $subject, @users);
if (-t STDIN) { die $text;}
else { exit 1; }
}
sub negotiate_pid ($) {
my $restart = shift;
# Negotiate over possible prior instances
if (-s $pidfile) {
# PID file exists and is not empty
open (PID, $pidfile) or die_with_mail "Cannot read PID file $pidfile\n";
chomp (my $pid = <PID>);
close PID;
die_with_mail "Corrupt PID file! (read '$pid' from it)\n" unless $pid =~ /^\d+$/;
if (kill (0, $pid)) {
# The process is alive
if ($restart) {
# We are gonna kill the current process
kill (9, $pid);
sleep 1;
if (kill (0, $pid)) { die_with_mail "Cannot kill predecessor, PID $pid\n";}
else { unlink $pidfile; }
} else {
# There's another instance already running, leave it alone.
exit 1;
}
} else {
# PID file exists but the process is dead, proceed
unlink $pidfile;
}
} elsif (-e $pidfile) {
# PID file exists but it empty, ignore it.
unlink $pidfile;
}
if (-e $pidfile) { die_with_mail "PID file $pidfile unepectedly exists!\n"; }
elsif (open (PID, "> $pidfile")) {
print PID "$$\n";
close PID;
} else { die_with_mail "Couldn't write my PID ($$) to $pidfile\n"; }
}
sub process_line ($$) {
my $line = shift;
my $hosts = shift;
chomp $line;
if ($line =~ /\bsshd\b.*(failed|accepted)\s+\S+\s+for\s+(?:illegal user\s+)?(\S+)\s+from\s+(?:\S+:)?(\S+)/i) {
# matched line
my ($result, $user, $host) = ($1, $2, $3);
if ($host !~ /(\.cs\.umass\.edu$)|(^128\.119\.24[01234567]\.\d+$)|(^128\.119\.4[01]\.\d+$)/) {
# print "$result $user from $host\n";
if ($result =~ /accepted/) {
# Successful login, validate this address
delete $hosts->{$host};
} else {
$hosts->{$host}->{users}->{$user}++;
$hosts->{$host}->{count}++;
# Count known-exploited users double
$hosts->{$host}->{count}++ if grep (/^$user$/, @bad_users);
}
}
}
return $hosts;
}
sub normalize_duration ($) {
my $duration = shift()."h";
$duration =~ s/\s//g;
my ($num, $unit) = (lc($duration) =~ /^(\d+)(\w)/);
return undef unless ($num && $unit);
my $multiplier = 0;
if ($unit eq "s") { $multiplier = 1;}
elsif ($unit eq "m") { $multiplier = 60;}
elsif ($unit eq "h") { $multiplier = 60*60;}
elsif ($unit eq "d") { $multiplier = 60*60*24;}
elsif ($unit eq "w") { $multiplier = 60*60*24*7;}
elsif ($unit eq "m") { $multiplier = 60*60*24*30;}
elsif ($unit eq "y") { $multiplier = 60*60*24*365;}
else { return undef;}
return $num * $multiplier;
}
sub process_hosts ($) {
my $hosts = shift;
open (DENY, ">> $hosts_deny") or die_with_mail "Cannot write to $hosts_deny\n";
my $expo = time() + normalize_duration($duration);
for my $host (keys %$hosts) {
if ($hosts->{$host}->{count} >= $threshold) {
# Too many authentication failures for the host
my $time = scalar (localtime($expo)); $time =~ s/^\w+\s+//;
my $str = sprintf ("ALL : %-18s \# $tag %i (expires %s)\n",
$host, $expo, $time);
printf DENY $str;
mail_to_users("Inserting deny string:\n$str\n",
"$shost: Blocking $host");
delete $hosts->{$host};
}
}
close DENY;
return $hosts;
}
sub expire_denials () {
# expire old entries in $hosts_deny
open (DENY, $hosts_deny) or die_with_mail "Cannot read $hosts_deny\n";
my @data = <DENY>;
my @new = ();
my $change = 0;
for my $line (@data) {
if ($line =~ /\#.*\b$tag\b\s+(\d+)/) {
# Our line, process it
if ($1 > time()) {
push @new, $line; # This entry has future timestamp
} else {
$change = 1; # We reaped an entry, set the change flag
}
} else {
push @new, $line;
}
}
if ($change) {
# We changed the contents, write them back to file
my ($mode, $uid, $gid) = (stat($hosts_deny))[2,4,5];
open (DENY, "> $hosts_deny") or die_with_mail "Cannot write to $hosts_deny\n";
print DENY @new; #"Deny:\n\n", @new,"\n\n"; exit 0;
close DENY;
chown ($uid, $gid, $hosts_deny);
chmod ($mode, $hosts_deny);
}
}
#############################
# #
# Execution begins here #
# #
#############################
GetOptions ("help" => \$help,
"file:s" => \$file,
"restart" => \$restart,
"threshold=i" => \$threshold,
"interval=i" => \$interval,
"duration=s" => \$duration,
"penalty=i" => \$penalty,
);
if ($help) { print help(); exit 0;}
negotiate_pid($restart);
# Activate $<option>_default values
eval "no strict 'vars'; \$$_ ||= \$${_}_default" for qw(file interval threshold duration penalty);
die_with_mail "Bad duration spec ($duration)\n" unless normalize_duration ($duration);
my $inode = (stat($file))[1];
open (LOG, $file) or die_with_mail "Cannot read log file '$file'\n";
while (1) {
# Spin infinitely, polling $file every $interval seconds
while (<LOG>) { $hosts = process_line ($_, $hosts) if /\bsshd\b/;} # parse SSH log
$hosts = process_hosts ($hosts); # See if any hosts need blacklisting
expire_denials (); # Reap expired blacklist entries
my $new_inode = (stat($file))[1];
if ($new_inode && ($inode != $new_inode)) {
# logs have been rotated! Open the new log file, don't sleep or reset EOF
close LOG;
open (LOG, $file) or die_with_mail "Cannot reopen rotated log file '$file'\n";
$inode = $new_inode;
} else {
# the logs have not been rotated
sleep $interval;
LOG->clearerr();
}
}