#!/usr/bin/perl -wT # # $Id: sshdict,v 1.1 2007/10/19 18:05:13 jtk Exp $ # # sshdict - parse logs for SSH brute force attempts # 2005-02-21,jtk: initial prototype # 2005-02-22,jtk: report from FreeBSD 4.7/OpenSSH 3.8.1p1 user # auto detection of local timezone # minor edits # 2005-03-18,jtk: hack to decrement year if logmonth = december # and currentsysmonth = januarary # 2005-04-03,jtk: changed to scoring system, added $DEBUG # adjust THRESHOLD to >=, added/changed tests # 2005-04-13,jtk: fixed last change date, Koh is a butthead # 2007-10-19,jtk: applied patch to #03 regex (credit: prg) # added caching of locked account attempts # added 05-09 based on latest OpenSSH auth.c # # This tries to discern from STDIN, whether the SSH log files # indicate brute force attempts. If so, each source IP address # is sent to stdout in the following format: # # 192.0.2.0 YYYY-MM-DD HH:MM:SS # # Having your system in UTC is highly recommended, especially # if your system's geographical location uses daylight savings # time. Otherwise, during time changes, logs (if they are from # standard syslog anyway) will be ambigious at certain times of # the year. # # The algorithm uses a simple count of attempts from a single # source address from the logs provided. Recommended default # threshold is 10 attemps. You probably want to send your log # files, via a crontab, every hour or every day to this script # for processing and send any output to a file, which can be # checked with a simple 'test -s' to see if the file size is # greater than zero and if so, mail it off to the appropriate # reporting destination. # # The following SSH log formats have been tested: # # Linux 2.4 syslog / OpenSSH 3.8.1p1 # FreeBSD 4.7 syslog / OpenSSH 3.8.1p1 # NetBSD 4.0 syslog / OpenSSH 4.6 # # Note: the commercial ssh.com implementation may not support easy # parsing of logs. On a Solaris 2.x box using SSH secure shell # 2.4.0, multiple log messages are needed to correlate a source # address and failed login attempts. This makes this simple # script a little more complicated and so I just didn't bother # with that scenario at this time. # 2007-10-18: actually I think it's possible, but I never got # around to implementing it. unfortunately I no # longer have readily available access to a ssh.com # on Solaris box to test now. If someone wants to # send me some sample logs or a patch that'd be great. # # TODO: support more SSH log formats # # FYI: reverse address checking log (probably requires address history) # whitelist/blacklist scoring might work for some, but DNS is likely # broken for enough people to make it sufficiently unreliable. Not # implemented. use strict; $|=1; my $DEBUG = 0; # 0 = disabled use Date::Manip; # to convert localtime to UTC use POSIX; # to get timezone my $THRESHOLD = 10; # min score til categorized as brute force my $low_score = 1; # min increment for scoring algorithm my $med_score = 2; # normal increment for scoring algorithm my $high_score = 3; # high increment for scoring algorithm my %not_allowed = (); # hash cache of users not allowed for scoring # get timezone my $TIMEZONE = strftime('%Z', localtime) || die "ERROR($!): Unable to retrieve timezone.\n"; my $list = (); # hash of hashes (addresses w/ count and timestamp values) # read SSH logs from STDIN while(defined (my $line =<>)) { # cache disallowed users and groups # Mon DD HH:MM:SS host sshd[1234]: User h4x0r not allowed because # (not listed in Allow[Users|Groups]|listed in Deny[Users|Groups]) if ($line =~ /(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+ # month \d{1,2}\s+ # day \d{2}:\d{2}:\d{2}\s+ # time \S+\s+sshd\[\d+\]:\s+User\s+(\S+)\s+not\s+allowed\s+ # id because\s+ (?:not\s+listed\s+in\s+Allow(?:Users|Groups)| # allow listed\s+in\s+Deny(?:Users|Groups))/x # deny ) { # create not_allowed{id} list my $userid = lc($1); $not_allowed{$userid} = 1; } # cache locked accounts # Mon DD HH:MM:SS host sshd[1234]: User h4x0r not allowed because # account is locked if ($line =~ /(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+ # month \d{1,2}\s+ # day \d{2}:\d{2}:\d{2}\s+ # time \S+\s+sshd\[\d+\]:\s+User\s+(\S+)\s+not\s+allowed\s+ # user because\s+account\s+is\s+locked/x ) { # create not_allowed{id} list my $userid = lc($1); $not_allowed{$userid} = 1; } # Format #01 - low score # Mon DD HH:MM:SS host sshd[1234]: Failed password for # h4x0r from 192.0.2.0 port 65535 ssh2 if ($line =~ /((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+ # month \d{1,2}\s+ # day \d{2}:\d{2}:\d{2})\s+ # time \S+\s+sshd\[\d+\]:\s+Failed\s+password\s+for\s+ # msg format (\S+)\s+from\s+ # id (\d{1,3}(?:\.\d{1,3}){3}) # src addr (?:\s+port\s+\d+\s+ssh2)?/x # msg format ) { my $timestamp = $1; my $userid = lc($2); my $srcaddress = $3; adjustScore($timestamp, $srcaddress, $low_score); } # Format #02 - med score # Mon DD HH:MM:SS host sshd[1234]: Failed password for # illegal user h4x0r from 192.0.2.0 port 65535 ssh2 if ($line =~ /((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+ # month \d{1,2}\s+ # day \d{2}:\d{2}:\d{2})\s+ # time \S+\s+sshd\[\d+\]:\s+Failed\s+password\s+for\s+ # msg format illegal\s+user\s+(\S+)\s+from\s+ # id (\d{1,3}(?:\.\d{1,3}){3}) # src addr (?:\s+port\s+\d+\s+ssh2)?/x # msg format ) { my $timestamp = $1; my $userid = lc($2); my $srcaddress = $3; adjustScore($timestamp, $srcaddress, $med_score); # if not an allowed group or user, increase score even more if ( $not_allowed{$userid} ) { adjustScore($timestamp, $srcaddress, $high_score); } } # Format 03 - med score # Mon DD HH:MM:SS host sshd[1234]: Illegal|Invalid user h4x0r from # 192.0.2.0 if ($line =~ /((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+ # month \d{1,2}\s+ # day \d{2}:\d{2}:\d{2})\s+ # time \S+\s+sshd\[\d+\]:\s+(?:Illegal|Invalid)\s+user\s+ (\S+)\s+from\s+ # id (\d{1,3}(?:\.\d{1,3}){3})/x # src addr ) { my $timestamp = $1; my $userid = lc($2); my $srcaddress = $3; adjustScore($timestamp, $srcaddress, $med_score); # if not an allowed group or user, increase score even more if ( $not_allowed{$userid} ) { adjustScore($timestamp, $srcaddress, $high_score); } } # Format 04 - low score # Mon DD HH:MM:SS host sshd[1234]: Did not receive identification # string from 192.0.2.0 if ($line =~ /((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+ # month \d{1,2}\s+ # day \d{2}:\d{2}:\d{2})\s+ # time \S+\s+sshd\[\d+\]:\s+Did\s+not\s+receive\s+ # desc identification\s+string\s+from\s+ # "" (\d{1,3}(?:\.\d{1,3}){3})/x # src addr ) { my $timestamp = $1; my $srcaddress = $2; adjustScore($timestamp, $srcaddress, $low_score); } # Format 05 - high score # Mon DD HH:MM:SS host sshd[1234]: User h4x0r from 192.0.2.0 not # allowed because listed in DenyUsers if ($line =~ /((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+ # month \d{1,2}\s+ # day \d{2}:\d{2}:\d{2})\s+ # time \S+\s+sshd\[\d+\]:\s+User\s+(\S+)\s+ # user from (\d{1,3}(?:\.d{1,3}){1,3})\s+not\s+allowed\s+ # srcaddr because\s+listed\s+in\s+DenyUsers/x ) { my $timestamp = $1; my $userid = lc($2); # currently unused my $srcaddress = $3; adjustScore($timestamp, $srcaddress, $high_score); } # Format 06 - high score # Mon DD HH:MM:SS host sshd[1234]: User h4x0r from 192.0.2.0 not # allowed because not listed in AllowUsers if ($line =~ /((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+ # month \d{1,2}\s+ # day \d{2}:\d{2}:\d{2})\s+ # time \S+\s+sshd\[\d+\]:\s+User\s+(\S+)\s+ # user from\s+(\d{1,3}(?:\.\d{1,3}){1,3})\s+not\s+allowed\s+ # srcaddr because\s+not\s+listed\s+in\s+AllowUsers/x ) { my $timestamp = $1; my $userid = lc($2); # currently unused my $srcaddress = $3; adjustScore($timestamp, $srcaddress, $high_score); } # Format 07 - high score # Mon DD HH:MM:SS host sshd[1234]: User h4x0r from 192.0.2.0 not # allowed because a group is listed in DenyGroups if ($line =~ /((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+ # month \d{1,2}\s+ # day \d{2}:\d{2}:\d{2})\s+ # time \S+\s+sshd\[\d+\]:\s+User\s+(\S+)\s+ # user from\s+(\d{1,3}(?:\.\d{1,3}){1,3})\s+not\s+allowed\s+ # srcaddr because\s+a\s+group\s+is\s+listed\s+in\s+DenyGroups/x ) { my $timestamp = $1; my $userid = lc($2); # currently unused my $srcaddress = $3; adjustScore($timestamp, $srcaddress, $high_score); } # Format 08 - high score # Mon DD HH:MM:SS host sshd[1234]: User h4x0r from 192.0.2.0 not # allowed because none of the user's groups are list in AllowGroups if ($line =~ /((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+ # month \d{1,2}\s+ # day \d{2}:\d{2}:\d{2})\s+ # time \S+\s+sshd\[\d+\]:\s+User\s+(\S+)\s+ # user from\s+(\d{1,3}(?:\.\d{1,3}){1,3})\s+not\s+allowed\s+ # srcaddr because\s+none\s+of\s+the\user\'s\s+groups\s+are\s+ listed\s+in\s+AllowGroups/x ) { my $timestamp = $1; my $userid = lc($2); # currently unused my $srcaddress = $3; adjustScore($timestamp, $srcaddress, $high_score); } } # loop through the hash of address hashes, printing break in attempts for my $address (keys %$list) { # break in attempt is any address score >= $THRESHOLD if($list->{$address}->{'score'} >= $THRESHOLD) { # set timestamp to UTC in YYYY-MM-DD HH:MM:SS format formatTimestamp($address); # output format is simply: ip-address timestamp print "$address $list->{$address}->{'timestamp'}"; if ($DEBUG) { print " score=", $list->{$address}->{'score'}; } print "\n"; } } sub formatTimestamp { my $address = shift; my $timestamp = $list->{$address}->{'timestamp'}; $timestamp = ParseDate($timestamp); $timestamp = Date_ConvTZ($timestamp, $TIMEZONE, "UTC"); # HACK: if log month=December, but system month=January, rollback year my $logmonth = UnixDate($timestamp, "%m"); my $currentmonth = (localtime)[4]+1; my $currentyear = (localtime)[5]+1900; if($currentmonth == 1 && $logmonth =~ /^12$/) { $timestamp = Date_SetDateField($timestamp, "y", $currentyear-1); } else { $timestamp = Date_SetDateField($timestamp, "y", $currentyear); } # format timestamp $timestamp = UnixDate($timestamp, "%Y-%m-%d %H:%M:%S"); $list->{$address}->{'timestamp'} = $timestamp; } sub adjustScore { my $timestamp = shift; my $address = shift; my $adjustment = shift; # update address' current timestamp, presume it's the latest $list->{$address}->{'timestamp'} = $timestamp; # adjust score by amount passed as argument $list->{$address}->{'score'} += $adjustment; }