From 0e221166ec7b0bcc2612fd8b9210feb38f7b3b1d Mon Sep 17 00:00:00 2001 From: root Date: Wed, 9 Jul 2008 08:20:06 -0500 Subject: [PATCH] Hawk IDS initial git setup --- LICENSES | 21 ++ blacklist.tmpl | 11 + broots.tmpl | 10 + failed.tmpl | 10 + graphs-xml.tmpl | 15 ++ graphs.tmpl | 24 ++ hawk-big.pl | 112 ++++++++++ hawk-master.pl | 240 ++++++++++++++++++++ hawk-test.pl | 400 +++++++++++++++++++++++++++++++++ hawk-updater.pl | 77 +++++++ hawk-web.pl | 565 +++++++++++++++++++++++++++++++++++++++++++++++ hawk.db | 31 +++ hawk.init | 251 +++++++++++++++++++++ hawk.pl | 422 +++++++++++++++++++++++++++++++++++ machines | 11 + main-master.tmpl | 104 +++++++++ main.tmpl | 102 +++++++++ menu.tmpl | 4 + mm.stat | 26 +++ search.tmpl | 49 ++++ stats-list.tmpl | 52 +++++ summary.tmpl | 86 ++++++++ templates.pl | 44 ++++ 23 files changed, 2667 insertions(+) create mode 100644 LICENSES create mode 100644 blacklist.tmpl create mode 100644 broots.tmpl create mode 100644 failed.tmpl create mode 100644 graphs-xml.tmpl create mode 100644 graphs.tmpl create mode 100644 hawk-big.pl create mode 100644 hawk-master.pl create mode 100644 hawk-test.pl create mode 100644 hawk-updater.pl create mode 100755 hawk-web.pl create mode 100644 hawk.db create mode 100755 hawk.init create mode 100755 hawk.pl create mode 100644 machines create mode 100644 main-master.tmpl create mode 100644 main.tmpl create mode 100644 menu.tmpl create mode 100644 mm.stat create mode 100644 search.tmpl create mode 100644 stats-list.tmpl create mode 100644 summary.tmpl create mode 100644 templates.pl diff --git a/LICENSES b/LICENSES new file mode 100644 index 0000000..8a1a938 --- /dev/null +++ b/LICENSES @@ -0,0 +1,21 @@ +Copyright (c) 2007, Marian Marinov + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the SiteGround LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/blacklist.tmpl b/blacklist.tmpl new file mode 100644 index 0000000..16a1533 --- /dev/null +++ b/blacklist.tmpl @@ -0,0 +1,11 @@ +Search in the blacklist log:
+
+ +
+ +IP Address:
+ +
+
+ + diff --git a/broots.tmpl b/broots.tmpl new file mode 100644 index 0000000..a4b132e --- /dev/null +++ b/broots.tmpl @@ -0,0 +1,10 @@ +Bruteforce attempts by hour(last 56 hours only):
+ + + + + + +__CONTENTS__ +
DateIP AddressService
+ diff --git a/failed.tmpl b/failed.tmpl new file mode 100644 index 0000000..1a9002b --- /dev/null +++ b/failed.tmpl @@ -0,0 +1,10 @@ +Failed attempts by hour(last 24 hours only):
+ + + + + + + +__CONTENTS__ +
DateIP AddressServiceUser
\ No newline at end of file diff --git a/graphs-xml.tmpl b/graphs-xml.tmpl new file mode 100644 index 0000000..74a6ac1 --- /dev/null +++ b/graphs-xml.tmpl @@ -0,0 +1,15 @@ + + __OPTIONS__ + diff --git a/graphs.tmpl b/graphs.tmpl new file mode 100644 index 0000000..4e5e194 --- /dev/null +++ b/graphs.tmpl @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/hawk-big.pl b/hawk-big.pl new file mode 100644 index 0000000..91583b5 --- /dev/null +++ b/hawk-big.pl @@ -0,0 +1,112 @@ +#!/usr/bin/perl -T +use strict; +use warnings; +use DBD::mysql; +use POSIX qw(setsid), qw(strftime); # use only setsid & strftime from POSIX + +# system variables +$ENV{PATH} = ''; # remove unsecure path +my $version = '0.1'; # version string + +# Hawk files +my $logfile = '/var/log//hawk.log'; # daemon logfile +my $pidfile = '/var/run/hawk.pid'; # daemon pidfile +my $ioerrfile = '/home/sentry/public_html/io.err'; # File where to add timestamps for I/O Errors +my $log_list = '/usr/bin/tail -f /var/log/messages |'; +our $debug = 0; # by default debuging is OFF + +my $start_time = time(); + +# check for debug +if ( defined($ARGV[0]) && $ARGV[0] =~ /debug/ ) { + $debug=1; # turn on debuging +} + +# changing to unbuffered output +our $| = 1; + +# Change program name +$0 = "[Hawk]"; + +# open the logfile +open HAWK, '>>', $logfile or die "DIE: Unable to open logfile $logfile: $!\n"; +logger("Hawk version $version started!"); +#print HAWK get_time(), " Hawk version $version started!\n"; + + +# execute this before DIE-ing :) +$SIG{__DIE__} = sub { logger(@_); }; + +# check if the daemon is running +if ( -e $pidfile ) { + # get the old pid + open PIDFILE, '<', $pidfile or die "DIE: Can't open pid file($pidfile): $!\n"; + my $old_pid = ; + close PIDFILE; + # check if $old_pid is still running + if ( $old_pid =~ /[0-9]+/ ) { + if ( -d "/proc/$old_pid" ) { + logger("Hawk is already running!"); + die "DIE: Hawk is already running!\n"; + } + } else { + logger("Incorrect pid format!"); + die "DIE: Incorrect pid format!\n"; + } +} + +# generate time format: 15.May.07 02:41:52 +sub get_time { + return strftime('%b %d %H:%M:%S', localtime(time)); +} + +sub logger { + print HAWK strftime('%b %d %H:%M:%S', localtime(time)) . ' ' . $_[0] . "\n"; +} + +# Fork to background +defined(my $pid=fork) or die "DIE: Cannot fork process: $! \n"; +exit if $pid; +setsid or die "DIE: Unable to setsid: $!\n"; +umask 0; + +# redirect standart file descriptors to /dev/null +open STDIN, '>/dev/null' or die "DIE: Cannot write to stdout: $! \n"; +if (!$debug) { + open STDERR, '>>/dev/null' or die "DIE: Cannot write to stderr: $! \n"; +} + +# write the program pid to the $pidfile +open PIDFILE, '>', $pidfile or die "DIE: Unable to open pidfile $pidfile: $!\n"; +print PIDFILE $$; +close PIDFILE; + +# open logs +open LOGS, $log_list or die "DIE: Unable to open logs: $!\n"; + +# make the output unbuffered +select((select(HAWK), $| = 1)[0]); +select((select(LOGS), $| = 1)[0]); + + +while () { + # Feb 13 19:18:35 serv01 kernel: end_request: I/O error, dev sdb, sector 1405725148 + # Feb 13 19:18:58 serv01 kernel: end_request: I/O error, dev sdb, sector 1405727387 + if ( $_ =~ /I\/O error/i ) { + my @line = split /\s+/, $_; + open IOERR, '>', $ioerrfile or logger('Unable to log I/O Error'); + print IOERR get_time() . "$line[9]\n"; + close IOERR; + } else { + next; + } +} +close LOGS; +close HAWK; +close STDIN; +close STDOUT; +if (!$debug) { + close STDERR; +} +exit 0; diff --git a/hawk-master.pl b/hawk-master.pl new file mode 100644 index 0000000..af7dfee --- /dev/null +++ b/hawk-master.pl @@ -0,0 +1,240 @@ +#!/usr/bin/perl -T +use strict; +use warnings; +use DBD::Pg; +use CGI qw(param); +use CGI::Carp qw(fatalsToBrowser); +use POSIX qw(setsid), qw(strftime); # use only setsid & strftime from POSIX +use File::Basename; + +# system variables +$ENV{PATH} = ''; # remove unsecure path +my $version = '0.1'; # version string + +my $conf = '/home/sentry/hackman/hawk-web.conf'; +# make DB vars +my $html = ''; +my %config; +# changing to unbuffered output +our $| = 1; +sub web_error { + print $_[0], "\n"; + exit 1; +} +# parse the configuration file $conf into the hash %config +sub parse_conf { + my %hash; + die "No config defined!\n" if !defined($_[0]); + open CONF, '<', $_[0] or die "Unable to open $_[0]: $!\n"; + while () { + if ($_ =~ /^#/ or $_ =~ /^[\s]*$/) { + # if this is a comment + # or blank line + # skip to next line + next; + } else { + # clean unwanted chars + $_ =~ s/[\r|\n]$//; + $_ =~ s/([\s]*=[\s]*){1}/=/; + my $key = my $val = $_; + $key =~ s/=.*//; + $val =~ s/.*?=//; + $hash{$key} = $val; + } + } + close CONF; + return %hash; +} +%config = parse_conf($conf); + +sub get_template { + my $out=''; + if (defined($_[0])) { + my $template = $config{'template_path'}.basename($_[0]).'.tmpl'; + open FILE, '<', $template or die "Unable to open template $template: $!\n"; + $out .= $_ while (); + close FILE; + } + return $out; +} + +print "Content-type: text/html\r\n\r\n"; + +# prepare the connection +our $conn = DBI->connect_cached( $config{'db'}, $config{'dbuser'}, $config{'dbpass'}, { PrintError => 1, AutoCommit => 1 } ) + or web_error("Unable to connecto to DB: $DBI::errstr\n"); +our $list = $conn->prepare(" + SELECT TO_CHAR(\"date\", 'DD.Mon.YYYY HH24:MI'), server, brutes0, brutes1, brutes2, failed0, failed1, failed2 + FROM \"system\".hawk_stats ORDER BY ? DESC") or web_error("Unable to prepare list query: $DBI::errstr\n"); +# our $clear_list = $conn->prepare("DELETE FROM \"system\".hawk_stats"); +# our $add_stats = $conn->prepare(" +# INSERT INTO \"system\".hawk_stats +# ( server, brutes0, brutes1, brutes2, failed0, failed1, failed2 ) +# VALUES ( ?, ?, ?, ?, ?, ?, ? )"); +# our $servers = $conn->prepare("SELECT \"server\",\"ip\" FROM \"system\".sitecur ORDER BY server ASC") or web_error("Unable to prepare list query: $DBI::errstr\n"); +my $out = get_template('main-master'); +$out =~ s/__VER__/$version/gi; + +print $out; + +sub get_info { + my $out = ''; + open SERV, sprintf('/usr/bin/lynx --dump http://%s/~sentry/cgi-bin/hawk-web.pl\?action=summary\&cgi=1 2>&1|', $_[0]) + or web_error("Unable to open $_[0]: $!\n"); + $out .= $_ while (); + close SERV; + return $out; +} +if (defined(param('action')) && param('action') eq 'update') { + $clear_list->execute; + $servers->execute or web_error("Tegavo e: $DBI::errstr"); + while (my ($server, $ip) = $servers->fetchrow_array) { + print "Checking $server($ip)...\n" if defined(param('debug')); + my $out = get_info($ip); + my @stats = split /\|/, $out; + for (my $i=0;$i<=5;$i++) { + $stats[$i] = '0' if (!defined($stats[$i])) + } + $stats[0] = '0' if ($out =~ /Unable/); + $add_stats->execute($server,@stats) or web_error("Unable to execute add_stats: $DBI::errstr"); + } +} +# ID DATE SERVER BRUTES0 BRUTES1 BRUTES2 FAILED0 FAILED1 FAILED2 +my $table = get_template('stats-list'); +my @stats = $conn->selectrow_array(" + SELECT + SUM(brutes0) AS B0, + SUM(brutes1) AS B1, + SUM(brutes2) AS B2, + SUM(failed0) AS F0, + SUM(failed1) AS F1, + SUM(failed2) AS F2 + FROM \"system\".hawk_stats") or web_error("Unable to prepare list query: $DBI::errstr\n"); +$table =~ s/__BRUTES0__/$stats[0]/; +$table =~ s/__BRUTES1__/$stats[1]/; +$table =~ s/__BRUTES2__/$stats[2]/; +$table =~ s/__FAILED0__/$stats[3]/; +$table =~ s/__FAILED1__/$stats[4]/; +$table =~ s/__FAILED2__/$stats[5]/g; + +my $order='brutes0'; +if (defined(param('sort'))) { + if (param('sort') == 0) { + $order='failed0'; + } elsif (param('sort') == 1) { + $order='brutes0'; + } elsif (param('sort') == 2) { + $order='failed1'; + } elsif (param('sort') == 3) { + $order='brutes1'; + } elsif (param('sort') == 4) { + $order='failed2'; + } elsif (param('sort') == 5) { + $order='brutes2'; + } elsif (param('sort') == 6) { + $order='"server"'; + } elsif (param('sort') == 7) { + $order='"bl1h-a"'; + } elsif (param('sort') == 8) { + $order='"bl1d-a"'; + } elsif (param('sort') == 9) { + $order='"bl1h-r"'; + } elsif (param('sort') == 10) { + $order='"bl1d-r"'; + } else { + $order='brutes0'; + } +} +our $list = $conn->prepare(" + SELECT TO_CHAR(\"date\", 'DD.Mon.YYYY HH24:MI'), server, brutes0, brutes1, brutes2, failed0, failed1, failed2,\"bl1h-a\",\"bl1d-a\",\"bl1h-r\",\"bl1d-r\" + FROM \"system\".hawk_stats ORDER BY $order DESC") or web_error("Unable to prepare list query: $DBI::errstr\n"); + +$list->execute or web_error("Unable to execute query: $DBI::errstr"); +my $serv_count = $list->rows; +$table =~ s/__COUNT__/$serv_count/; +my %test_servers = ( + 'clev9.com' => 1, + 'clev10.com' => 1, + 'clev11.com' => 1, + 'clev15.com' => 1, + 'siteground.net' => 1, + 'siteground12.com' => 1, + 'siteground118.com' => 1, + 'siteground120.com' => 1, + 'siteground121.com' => 1, + 'siteground122.com' => 1, + 'siteground123.com' => 1, + 'siteground125.com' => 1, + 'siteground126.com' => 1, + 'siteground127.com' => 1, + 'siteground128.com' => 1, + 'siteground129.com' => 1, + 'siteground132.com' => 1, + 'siteground133.com' => 1, + 'siteground136.com' => 1, + 'siteground139.com' => 1, + 'siteground143.com' => 1, + 'siteground149.com' => 1, + 'siteground150.com' => 1, + 'siteground153.com' => 1, + 'siteground160.com' => 1, + 'siteground162.com' => 1, + 'siteground164.com' => 1, + 'siteground166.com' => 1, + 'siteground167.com' => 1, + 'siteground169.com' => 1, + 'siteground171.com' => 1, + 'siteground175.com' => 1, + 'siteground177.com' => 1, + 'siteground179.com' => 1, + 'siteground180.com' => 1, + 'siteground181.com' => 1, + 'siteground182.com' => 1, + 'siteground184.com' => 1, + 'siteground187.com' => 1, + 'siteground188.com' => 1, + 'siteground191.com' => 1, + 'siteground192.com' => 1 +); +my $line0 = ' + __DATE__ + __SERVER__ + __FAILED0__ + __BRUTES0__ + + __BL1HA__ + __BL1DA__ + __BL1HR__ + __BL1DR__ +'; +my $lines = ''; +while (my @str = $list->fetchrow_array) { + $lines .= $line0; + my $class = 'td0'; +# $class='redtd' if ( exists $test_servers {$str[1]} ); + $lines =~ s/__CLASS__/$class/g; + $lines =~ s/__DATE__/$str[0]/; + $lines =~ s/__SERVER__/$str[1]/g; + $lines =~ s/__BRUTES0__/$str[2]/; + $lines =~ s/__BRUTES1__/$str[3]/; + $lines =~ s/__BRUTES2__/$str[4]/; + $lines =~ s/__FAILED0__/$str[5]/; + $lines =~ s/__FAILED1__/$str[6]/; + $lines =~ s/__FAILED2__/$str[7]/; + for (my $z=8; $z<=11; $z++) { + $str[$z] =~ s/.*\|//; + $str[$z] =~ s/\s+/\ /; + $str[$z] = ' ' if ($str[$z] == 0); + } + $lines =~ s/__BL1HA__/$str[8]/; + $lines =~ s/__BL1DA__/$str[9]/; + $lines =~ s/__BL1HR__/$str[10]/; + $lines =~ s/__BL1DR__/$str[11]/; +} +$table =~ s/__CONTENTS__/$lines/; +print $table; + +exit 0; diff --git a/hawk-test.pl b/hawk-test.pl new file mode 100644 index 0000000..bbd39d5 --- /dev/null +++ b/hawk-test.pl @@ -0,0 +1,400 @@ +#!/usr/bin/perl -T +use strict; +use warnings; +use DBD::mysql; +use POSIX qw(setsid), qw(strftime); # use only setsid & strftime from POSIX + +# system variables +$ENV{PATH} = ''; # remove unsecure path +my $version = '0.57'; # version string + +# defining fault hashes +our %ssh_faults; # ssh faults storage +our %ftp_faults; # ftp faults storage +our %pop3_faults; # pop3 faults storage +our %imap_faults; # imap faults storage +our %smtp_faults; # smtp faults storage +our %cpanel_faults; # cpanel faults storage +our %notifications; # notifications + +# make DB vars +my $db = 'DBI:Pg:database=hawk;host=localhost;port=5432'; +my $user = 'hawk'; +my $pass = '157856cc61d4'; + +# Hawk files +my $logfile = "/var/log//hawk.log"; # daemon logfile +my $pidfile = "/var/run/hawk.pid"; # daemon pidfile +my $log_list = '/usr/bin/tail -f /var/log/messages /var/log/secure /var/log/maillog /usr/local/cpanel/logs/access_log |'; +our $broot_time = 300; # time(in seconds) before cleaning the hashes +our $max_attempts = 5; # max number of attempts(for $broot_time) before notify +our $debug = 0; # by default debuging is OFF +our $do_limit = 0; # by default do not limit the offending IPs + +my $start_time = time(); +my $myip = get_ip(); + + +# check for debug +if ( defined($ARGV[0]) && $ARGV[0] =~ /debug/ ) { + $debug=1; # turn on debuging +} + +# changing to unbuffered output +our $| = 1; + +# Change program name +$0 = "[Hawk]"; + +# open the logfile +open HAWK, '>>', $logfile or die "DIE: Unable to open logfile $logfile: $!\n"; +logger("Hawk version $version started!"); +#print HAWK get_time(), " Hawk version $version started!\n"; + + +# execute this before DIE-ing :) +$SIG{__DIE__} = sub { logger(@_); }; + +# check if the daemon is running +if ( -e $pidfile ) { + # get the old pid + open PIDFILE, '<', $pidfile or die "DIE: Can't open pid file($pidfile): $!\n"; + my $old_pid = ; + close PIDFILE; + # check if $old_pid is still running + if ( $old_pid =~ /[0-9]+/ ) { + if ( -d "/proc/$old_pid" ) { + logger("Hawk is already running!"); + die "DIE: Hawk is already running!\n"; + } + } else { + logger("Incorrect pid format!"); + die "DIE: Incorrect pid format!\n"; + } +} + +# get the server IP address +sub get_ip { + my @ip; + open IP, "/sbin/ip a l |" or die "DIE: Unable to get local IP Address: $!\n"; + while () { + if ( $_ =~ /eth0$/) { + @ip = split /\s+/, $_; + $ip[2] =~ s/\/[0-9]+//; + if ($debug) { + print $ip[2], "\n"; + } + } + } + close IP; + return $ip[2] +} +sub check_ip { + if ( $_[0] =~ /[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}/ ) { + return 1; + } else { + return 0; + } +} + +# generate time format: 15.May.07 02:41:52 +sub get_time { + return strftime('%b %d %H:%M:%S', localtime(time)); +} + +sub logger { + print HAWK strftime('%b %d %H:%M:%S', localtime(time)) . ' ' . $_[0] . "\n"; +} + +# clean the hashes +sub clean_hashes { + delete @ssh_faults{keys %ssh_faults}; + delete @pop3_faults{keys %pop3_faults}; + delete @imap_faults{keys %imap_faults}; + delete @cpanel_faults{keys %cpanel_faults}; + delete @notifications{keys %notifications}; + logger("hashes cleaned!"); +} + +# check for broots +sub check_broot { + while ( my ($k,$v) = each (%ssh_faults) ) { + if ( $v > $max_attempts ) { + log_broot('ssh', $k, $v); + } + } + while ( my ($k,$v) = each (%pop3_faults) ) { + if ( $v >= $max_attempts ) { + log_broot('pop3', $k, $v); + } + } + while ( my ($k,$v) = each (%imap_faults) ) { + if ( $v > $max_attempts ) { + log_broot('imap', $k, $v); + } + } + while ( my ($k,$v) = each (%smtp_faults) ) { + if ( $v > $max_attempts ) { + log_broot('imap', $k, $v); + } + } + while ( my ($k,$v) = each (%cpanel_faults) ) { + if ( $v > $max_attempts ) { + log_broot('cPanel', $k, $v); + } + } + while ( my ($k,$v) = each (%ftp_faults) ) { + if ( $v > $max_attempts ) { + log_broot('ftp', $k, $v); + } + } +} + +# Fork to background +defined(my $pid=fork) or die "DIE: Cannot fork process: $! \n"; +exit if $pid; +setsid or die "DIE: Unable to setsid: $!\n"; +umask 0; + +# redirect standart file descriptors to /dev/null +open STDIN, '>/dev/null' or die "DIE: Cannot write to stdout: $! \n"; +if (!$debug) { + open STDERR, '>>/dev/null' or die "DIE: Cannot write to stderr: $! \n"; +} + +# write the program pid to the $pidfile +open PIDFILE, '>', $pidfile or die "DIE: Unable to open pidfile $pidfile: $!\n"; +print PIDFILE $$; +close PIDFILE; + +# open logs +open LOGS, $log_list or die "DIE: Unable to open logs: $!\n"; + +# make the output unbuffered +select((select(HAWK), $| = 1)[0]); +select((select(LOGS), $| = 1)[0]); + +# prepare the connection +# these should be after the select command or they will NOT work +our $conn = DBI->connect_cached( $db, $user, $pass, { PrintError => 1, AutoCommit => 1 }) or die "DIE: Unable to connecto to DB: $!\n"; +our $log_me = $conn->prepare('INSERT INTO failed_log ( ip, "user", service ) VALUES ( ?, ?, ? ) ') + or die "DIE: Unable to prepare log query: $!\n"; +our $log_me2 = $conn->prepare('INSERT INTO failed_log ( ip, "user", service, broot ) VALUES ( ?, ?, ?, ? ) ') + or die "DIE: Unable to prepare log query: $!\n"; +our $broot_me = $conn->prepare('INSERT INTO broots ( ip, service ) VALUES ( ?, ? ) ') + or die "DIE: Unable to prepare broot query: $!\n"; +my $get_failed = $conn->prepare('SELECT COUNT(id) AS id FROM failed_log') + or die "Unable to prepare log query: $!\n"; + +sub log_broot { + # 0 - SERVICE + # 1 - IP + # 2 - COUNT + my %services = ( + ftp => '21', + ssh => '22', + smtp => '25', + pop3 => '110', + imap => '143', + cPanel => '2083' + ); + ## log to DB and file + # check if already logged + if ( ! exists $notifications {$_[1]} ) { + $notifications{$_[1]}=1; + $broot_me->execute($_[1],$_[0]) or logger("Failed broot: $_[0], $_[1], $_[2] ($DBI::errstr)"); + logger("!!! $_[0] $_[1] failed $_[2] times in $broot_time seconds"); + } +} + +# start the real work +# everything bellow this line is executed for every line that enters any of the logs +# keep this as simple as possible in order to stay light +while () { + my $brute = 'false'; # define that this line is not brute force + if ( $_ =~ /ssh/ && $_ =~ /Failed/ ) { + ## check ssh + # match theese lines + #May 15 11:36:27 serv01 sshd[5448]: Failed password for support from ::ffff:67.15.243.7 port 47597 ssh2 + #May 16 03:27:24 serv01 sshd[25536]: Failed password for invalid user suport from ::ffff:85.14.6.2 port 52807 ssh2 + my @sshd = split /\s+/, $_; + my $ip = ''; + my $user = ''; + if ( $sshd[8] =~ /invalid/ ) { + $ip = $sshd[12]; + $user = $sshd[10]; + } else { + $ip = $sshd[10]; + $user = $sshd[8]; + } + $ip =~ s/::ffff://; + # do not log the failures from server's local IP address + next if ( $ip =~ /$myip/ ); + if ( exists $ssh_faults {$ip} ) { + $ssh_faults{$ip}++; + } else { + $ssh_faults{$ip} = 1; + } + logger(" IP $ip failed to identify to ssh.") if ($debug); + # check for brureforce + while ( my ($k,$v) = each (%ssh_faults) ) { + if ( $v > $max_attempts ) { + log_broot('ssh', $k, $v); + $brute = 'true'; + } + } + $log_me2->execute($ip, $user, 'ssh', $brute) + or logger("Failed to insert: ssh $ip $user ($DBI::errstr)"); + + } elsif ( $_ =~ /cpanelpop/ ) { + #May 16 02:37:31 serv01 cpanelpop[29746]: Session Closed host=67.15.172.8 ip=67.15.172.8 user=root realuser= totalxfer=47 + my @pop3 = split /\s+/, $_; + if ( defined($pop3[10]) && $pop3[10] =~ /realuser=/ ) { + $pop3[7] =~ s/host=//; + # do not log the failures from server's local IP address + next if ( $pop3[7] =~ /$myip/ ); + + if ( exists $pop3_faults {$pop3[7]} ) { + $pop3_faults{$pop3[7]}++; + } else { + $pop3_faults{$pop3[7]} = 1; + } + logger("IP $pop3[7] failed to identify to cppop") if ($debug); + # check for brureforce + while ( my ($k,$v) = each (%pop3_faults) ) { + if ( $v > $max_attempts ) { + log_broot('pop3', $k, $v); + $brute = 'true'; + } + } + $log_me2->execute($pop3[7], '', 'pop3', $brute) + or logger("Failed to insert: cppop $pop3[7] ($DBI::errstr)"); + } + } elsif ( $_ =~ /pop3d:/ && $_ =~ /FAILED/ ) { + # Courier POP3 + #May 11 03:58:40 serv01 pop3d: LOGIN FAILED, user=kate, ip=[::ffff:72.43.28.210] + my @pop3 = split /\s+/, $_; + $pop3[8] =~ s/ip=\[(.*)\]/$1/; + $pop3[8] =~ s/.*:// if $pop3[8] =~ /ffff/; + # do not log the failures from server's local IP address + next if ( $pop3[8] =~ /$myip/ ); + $pop3[7] =~ s/user=(.*),/$1/; + if ( exists $pop3_faults {$pop3[8]} ) { + $pop3_faults{$pop3[8]}++; + } else { + $pop3_faults{$pop3[8]} = 1; + } + logger("IP $pop3[8]($pop3[7]) failed to identify to courier-pop3") if ($debug); + # check for brureforce + while ( my ($k,$v) = each (%pop3_faults) ) { + if ( $v > $max_attempts ) { + log_broot('pop3', $k, $v); + $brute = 'true'; + } + } + $log_me2->execute($pop3[8], $pop3[7], 'pop3', $brute) + or logger("Failed to insert: courier-pop $pop3[8] $pop3[7] ($DBI::errstr)"); + } elsif ( $_ =~ /imapd/ && $_ =~ /failed/ ) { + # cPanel IMAP + #May 17 17:06:44 serv01 imapd[32199]: Login failed user=dsada domain=(null) auth=dsada host=[85.14.6.2] + my @imap = split /\s+/, $_; + $imap[10] =~ s/host=\[(.*)\]/$1/; + # do not log the failures from server's local IP address + next if ( $imap[10] =~ /$myip/ ); + $imap[7] =~ s/user=//; + if ( exists $imap_faults {$imap[10]} ) { + $imap_faults{$imap[10]}++; + } else { + $imap_faults{$imap[10]} = 1; + } + logger("IP $imap[10]($imap[7]) failed to identify to cpimap.") if ($debug); + # check for brureforce + while ( my ($k,$v) = each (%imap_faults) ) { + if ( $v > $max_attempts ) { + log_broot('imap', $k, $v); + $brute = 'true'; + } + } + $log_me2->execute($imap[10], $imap[7], 'imap', $brute) + or logger("Failed to insert: cPanel imap $imap[10] $imap[7] ($DBI::errstr)"); + } elsif ( $_ =~ /imapd/ && $_ =~ /FAILED/ ) { + # Courier IMAP + #May 15 05:26:16 serv01 imapd: LOGIN FAILED, user=admin, ip=[::ffff:67.15.243.20] + my @imap = split /\s+/, $_; + $imap[8] =~ s/host=\[::ffff:(.*)\]/$1/; + # do not log the failures from server's local IP address + next if ( $imap[8] =~ /$myip/ ); + $imap[7] =~ s/user=//; + if ( exists $imap_faults {$imap[8]} ) { + $imap_faults{$imap[8]}++; + } else { + $imap_faults{$imap[8]} = 1; + } + logger(" IP $imap[8]($imap[7]) failed to identify to courier-imap.") if ($debug); + # check for brureforce + while ( my ($k,$v) = each (%imap_faults) ) { + if ( $v > $max_attempts ) { + log_broot('imap', $k, $v); + $brute = 'true'; + } + } + $log_me2->execute($imap[8], $imap[7], 'imap', $brute) + or logger("Failed to insert: courier-imap $imap[8] $imap[7] ($DBI::errstr)"); + } elsif ( $_ =~ /pure-ftpd:/ && $_ =~ /failed/ ) { + #May 16 03:06:43 serv01 pure-ftpd: (?@85.14.6.2) [WARNING] Authentication failed for user [mamam] + my @ftp = split /\s+/, $_; + $ftp[5] =~ s/\(.*\@(.*)\)/$1/; # get the IP + $ftp[11] =~ s/\[(.*)\]/$1/; # get the username + if ( exists $ftp_faults {$ftp[5]} ) { + $ftp_faults{$ftp[5]}++; + } else { + $ftp_faults{$ftp[5]} = 1; + } + logger("IP $ftp[5]($ftp[11]) failed to identify to Pure-FTPD.") if ($debug); + # check for brureforce + while ( my ($k,$v) = each (%ftp_faults) ) { + if ( $v > $max_attempts ) { + log_broot('ftp', $k, $v); + $brute = 'true'; + } + } + $log_me2->execute($ftp[5], $ftp[11], 'ftp', $brute) + or logger("Failed to insert: ftp $ftp[5] $ftp[11] ($DBI::errstr)"); + } elsif ( $_ =~ / 401 / && $_ =~ /GET / ) { + #85.14.6.2 - root [05/16/2007:07:58:28 -0000] "GET / HTTP/1." 401 0 "" "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20061208 Firefox/2.0.0.1" + my @cpanel = split /\s+/; + $cpanel[2] = '' if $cpanel[2] =~ /\[/; + if ( exists $cpanel_faults {$cpanel[0]} ) { + $cpanel_faults{$cpanel[0]}++; + } else { + $cpanel_faults{$cpanel[0]} = 1; + } + logger("IP $cpanel[0]($cpanel[2])failed to identify to cPanel.") if ($debug); + # check for brureforce + while ( my ($k,$v) = each (%cpanel_faults) ) { + if ( $v > $max_attempts ) { + log_broot('cpanel', $k, $v); + $brute = 'true'; + } + } + $log_me2->execute($cpanel[0], $cpanel[2], 'cpanel', $brute) + or logger("Failed to insert: cPanel $cpanel[0] $cpanel[2] ($DBI::errstr)"); + } else { + next; + } +# check_broot(); + my $passed_time = time() - $start_time; # get the pssed time + if ($passed_time > $broot_time) { # if the passed time is grater then $broot_time + clean_hashes(); # clean the hashes + $start_time = time(); # set the start_time to now + } +} +close LOGS; +close HAWK; +close STDIN; +close STDOUT; +if (!$debug) { + close STDERR; +} +exit 0; diff --git a/hawk-updater.pl b/hawk-updater.pl new file mode 100644 index 0000000..4505ab9 --- /dev/null +++ b/hawk-updater.pl @@ -0,0 +1,77 @@ +#!/usr/bin/perl -T +use strict; +use warnings; +use DBD::Pg; +use POSIX qw(setsid), qw(strftime); # use only setsid & strftime from POSIX + +# system variables +$ENV{BASH_ENV}=''; +$ENV{PATH} = ''; # remove unsecure path +my $version = '0.1'; # version string + +my $conf = '/home/sentry/hackman/hawk-web.conf'; +# make DB vars +my %config; +# changing to unbuffered output +our $| = 1; +# parse the configuration file $conf into the hash %config +sub parse_conf { + my %hash; + die "No config defined!\n" if !defined($_[0]); + open CONF, '<', $_[0] or die "Unable to open $_[0]: $!\n"; + while () { + if ($_ =~ /^#/ or $_ =~ /^[\s]*$/) { + # if this is a comment + # or blank line + # skip to next line + next; + } else { + # clean unwanted chars + $_ =~ s/[\r|\n]$//; + $_ =~ s/([\s]*=[\s]*){1}/=/; + my $key = my $val = $_; + $key =~ s/=.*//; + $val =~ s/.*?=//; + $hash{$key} = $val; + } + } + close CONF; + return %hash; +} +%config = parse_conf($conf); + + +# prepare the connection +our $local = DBI->connect_cached( $config{'db'}, $config{'dbuser'}, $config{'dbpass'}, { PrintError => 1, AutoCommit => 1 } ) + or die("Unable to connecto to DB: $DBI::errstr\n"); +our $clear_list = $local->prepare("DELETE FROM \"system\".hawk_stats"); +our $add_stats = $local->prepare(" + INSERT INTO \"system\".hawk_stats + ( server, brutes0, brutes1, brutes2, failed0, failed1, failed2 ) + VALUES ( ?, ?, ?, ?, ?, ?, ? )"); +our $servers = $local->prepare("SELECT \"server\",\"ip\" FROM \"system\".sitecur ORDER BY server ASC") or die("Unable to prepare list query: $DBI::errstr\n"); + +sub get_info { + my $out = ''; + open SERV, sprintf('/usr/bin/lynx --dump http://%s/~sentry/cgi-bin/hawk-web.pl\?action=summary\&cgi=1 2>&1|', $_[0]) + or die("Unable to open $_[0]: $!\n"); + $out .= $_ while (); + close SERV; + return $out; +} +$clear_list->execute; +$servers->execute or die("Tegavo e: $DBI::errstr"); +while (my ($server, $ip) = $servers->fetchrow_array) { + print "Checking $server($ip)...\n"; + my $out = get_info($ip); + my @stats = split /\|/, $out; + for (my $i=0;$i<=5;$i++) { + $stats[$i] = '0' if (!defined($stats[$i])) + } + $stats[0] = '0' if ($out =~ /Unable/); + $add_stats->execute($server,@stats) or die("Unable to execute add_stats: $DBI::errstr"); +} + + + +exit 0; diff --git a/hawk-web.pl b/hawk-web.pl new file mode 100755 index 0000000..902ae5b --- /dev/null +++ b/hawk-web.pl @@ -0,0 +1,565 @@ +#!/usr/bin/perl -T +use strict; +use warnings; +use DBD::Pg; +use CGI qw(param); +use CGI::Carp qw(fatalsToBrowser); +use POSIX qw(setsid), qw(strftime); # use only setsid & strftime from POSIX +use File::Basename; + +# system variables +$ENV{PATH} = ''; # remove unsecure path +my $version = '0.95'; # version string + +my $conf = '/home/sentry/hackman/hawk-web.conf'; +# make DB vars +my $html = ''; +my %config; +# changing to unbuffered output +our $| = 1; +sub web_error { + print $_[0], "\n"; + exit 1; +} +# parse the configuration file $conf into the hash %config +sub parse_conf { + my %hash; + die "No config defined!\n" if !defined($_[0]); + open CONF, '<', $_[0] or die "Unable to open $_[0]: $!\n"; + while () { + if ($_ =~ /^#/ or $_ =~ /^[\s]*$/) { + # if this is a comment + # or blank line + # skip to next line + next; + } else { + # clean unwanted chars + $_ =~ s/[\r|\n]$//; + $_ =~ s/([\s]*=[\s]*){1}/=/; + my $key = my $val = $_; + $key =~ s/=.*//; + $val =~ s/.*?=//; + $hash{$key} = $val; + } + } + close CONF; + return %hash; +} +%config = parse_conf($conf); + +sub get_template { + my $out=''; + if (defined($_[0])) { + my $template = $config{'template_path'}.basename($_[0]).'.tmpl'; + open FILE, '<', $template or die "Unable to open template $template: $!\n"; + $out .= $_ while (); + close FILE; + } + return $out; +} + +sub build_graph { + # lines should be in this format + # [0] - option name + # [1] - option color + # [2] - option value + my @values = @_; + my $graph = get_template('graphs'); + my $xml = get_template('graphs-xml'); + my $option = "\n"; + my $options = ''; + my @colors = ( 'AFD8F8', 'F6BD0F', '8BBA00', 'A66EDD', + 'F984A1', 'CCCC00', '999999', '0099CC', 'FF0000', + '006F00', '0099FF', 'FF66CC', '669966', '7C7CB4', + 'FF9933', '9900FF', '99FFCC', 'CCCCFF', '669900', + '1941A5', 'FFFF99', 'FFFF00', 'FFCC00', 'FF9966', + 'FF9999', 'FF6600', 'FF00FF', 'CCCCFF', 'CC3333', + 'CC0000', '99CCFF', '6666FF', '33FF33', '339900', + '00CCCC', '99CC33'); + $xml =~ s/__XNAME__/$values[0][0]/; + $xml =~ s/__YNAME__/$values[0][1]/; + $xml =~ s/__TITLE__/$values[0][2]/; + for (my $i=1; $i<=7;$i++) { + $options .= sprintf($option, $values[$i][0], $colors[$values[$i][1]], $values[$i][2]) if defined($values[$i][0]);; + } + $xml =~ s/__OPTIONS__/$options/; + $graph =~ s/__XML__/$xml/g; + return $graph; +} + +sub get_service_num { + if ($_[0] eq 'pop3') { + return '0'; + } elsif ($_[0] eq 'imap') { + return '1'; + } elsif ($_[0] eq 'ftp') { + return '2'; + } elsif ($_[0] eq 'ssh') { + return '3'; + } elsif ($_[0] =~ /cpanel/i) { + return 4; + } +} +sub get_num_service { + if ($_[0] == 0) { + return 'pop3'; + } elsif ($_[0] == 1) { + return 'imap'; + } elsif ($_[0] == 2) { + return 'ftp'; + } elsif ($_[0] == 3) { + return 'ssh'; + } elsif ($_[0] == 4) { + return 'cpanel'; + } +} + + +print "Content-type: text/html\r\n\r\n"; + +# prepare the connection +our $conn = DBI->connect( $config{'db'}, $config{'dbuser'}, $config{'dbpass'}, { PrintError => 1, AutoCommit => 1 } ) + or web_error("Unable to connecto to DB: $!\n"); +my $action=''; +$action=param('action') if defined(param('action')); +if (!defined(param('cgi'))) { + my $out = get_template('main'); + $out =~ s/__VER__/$version/gi; + print $out; +} +if ($action eq 'listfailed') { +# header('listfailed'); + my $table .= get_template('failed'); + my $lines = ''; + my $line0 = "__DATE____IP____USER__"; + my $order = '"date"'; + if (defined(param('order'))) { + if (param('order') == 1) { + $order = 'ip'; + } elsif (param('order') == 2) { + $order = 'service'; + } elsif (param('order') == 3) { + $order = '"user"'; + } else { + $order = '"date"'; + } + } + my $get_failed = $conn->prepare("SELECT TO_CHAR(\"date\", 'DD.Mon.YYYY HH24:MI') AS \"date\",ip,service,\"user\" FROM failed_log WHERE \"date\" > (now() - interval '12 hour') ORDER BY $order DESC") + or web_error("Unable to prepare get_broots: $DBI::errstr"); + $get_failed->execute; + while (my ($date,$ip,$service,$user) = $get_failed->fetchrow_array) { + my $line = $line0; + my $service_num = get_service_num($service); + $user =~ s/[\<\>]/_/g; + $line =~ s/__DATE__/$date/; + $line =~ s/__IP__/$ip/g; + $line =~ s/__SERVICE__/$service_num\'>$service/; + $line =~ s/__USER__/$user/g; + $lines .= $line; + } + $table =~ s/__CONTENTS__/$lines/; + $html .= $table; +} elsif ($action eq 'listbroots') { +# header('listbroots'); + my $table = get_template('broots'); + my $lines = ''; + my $line0 = "__DATE____IP__prepare("SELECT to_char(\"date\", 'DD.Mon.YYYY HH24:MI') AS \"date\",ip,service FROM broots WHERE \"date\" > (now() - interval '12 hour') ORDER BY $order DESC") + or web_error("Unable to prepare get_broots: $DBI::errstr"); + $get_broots->execute; + while (my ($date,$ip,$service) = $get_broots->fetchrow_array) { + my $line = $line0; + my $service_num = get_service_num($service); + $line =~ s/__DATE__/$date/; + $line =~ s/__IP__/$ip/g; + $line =~ s/__SERVICE__/$service_num\'>$service/; + $lines .= $line; + } + $table =~ s/__CONTENTS__/$lines/; + $html .= $table; +} elsif ($action eq 'search') { +# header('search'); + print get_template('search'); + if (defined(param('w'))) { + my $query = ''; + my @values; + if ( param('w') eq 'ip' && param('addr') =~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/ ) { + # search for IP + $query = "SELECT TO_CHAR(\"date\", 'DD.Mon.YYYY HH24:MI') AS \"date\",ip,\"user\",service FROM failed_log WHERE ip = ? ORDER BY \"date\" DESC LIMIT ?"; + push @values, param('addr'); + } elsif ( param('w') eq 'tp' && param('from') =~ /^[0-9:\.\-\s]+$/ && param('to') =~ /^[0-9:\.\-\s]+$/ ) { +# $query = sprintf("SELECT \"date\",ip,\"user\",service FROM failed_log WHERE \"date\" BETWEEN TO_DATE('%s') AND TO_DATE('%s') ORDER BY \"date\" DESC", param('from'), param('to')); + # search for time period + $query = "SELECT TO_CHAR(\"date\", 'DD.Mon.YYYY HH24:MI') AS \"date\",ip,\"user\",service FROM failed_log WHERE \"date\" BETWEEN TO_DATE( ? ) AND TO_DATE( ? ) ORDER BY \"date\" DESC LIMIT ?"; + push @values, param('from'); + push @values, param('to'); + } elsif ( param('w') eq 'us' && param('user') =~ /^[a-zA-Z0-9\.\@\-]+$/ ) { + # search for user + $query = "SELECT TO_CHAR(\"date\", 'DD.Mon.YYYY HH24:MI') AS \"date\",ip,\"user\",service FROM failed_log WHERE \"user\" ~ ? ORDER BY \"date\" DESC LIMIT ?"; + push @values, param('user'); + } elsif ( param('w') eq 'sv' && param('ss') =~ /^[0-9]+$/ ) { + # search for service + $query = "SELECT TO_CHAR(\"date\", 'DD.Mon.YYYY HH24:MI') AS \"date\",ip,\"user\",\"service\" FROM failed_log WHERE \"service\" = ? ORDER BY \"date\" DESC LIMIT ?"; + my $service = get_num_service(param('ss')); + push @values, $service; + } else { + print "

Invalid parameters!

"; + } + if ( defined(param('lim')) && param('lim') =~ /^[0-9+]$/ ) { + push @values, param('lim'); + } else { + push @values, 200; # default LIMIT for the SQL queryes + } + # minimum parameters 2 VALUE & LIMIT + if (defined($values[1])) { + my $get_info = $conn->prepare($query) or web_error("Unable to prepare query: $DBI::errstr"); + $get_info->execute(@values) or web_error("Unable to execute query: $DBI::errstr"); + my $lines=''; + my $line0 = "__DATE__
__IP____USER____SERVICE__\n"; + print "
+ + + + + + "; + + while ( my @str = $get_info->fetchrow_array) { + my $line = $line0; + $str[2] = ' ' if (!defined($str[2]) || $str[2] eq ''); + $str[2] =~ s/[\<\>]/_/g; + $line =~ s/__DATE__/$str[0]/; + $line =~ s/__IP__/$str[1]/g; + $line =~ s/__USER__/$str[2]/g; + $line =~ s/__SERVICE__/$str[3]/; + print $line; + } + print '
DateIP AddressUserService
'; + } + } +} elsif ($action eq 'blacklist') { + print get_template('blacklist'); + my $query =''; + my @values; + my $type = 1; + if ( defined(param('w')) && + defined(param('addr')) && + param('w') eq 'ip' && + param('addr') =~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/ ) { + $query = "SELECT TO_CHAR(date_add, 'DD.Mon.YYYY HH24:MI') AS date_add,date_rem,ip,reason FROM blacklist WHERE ip = ? ORDER BY date_add DESC LIMIT ?"; + push @values, param('addr'); + $type = 0; + } else { + if (defined(param('only')) && param('only') eq 'rem') { + $query = "SELECT + TO_CHAR(date_add, 'DD.Mon.YYYY HH24:MI') AS date_add, TO_CHAR(date_rem, 'DD.Mon.YYYY HH24:MI') AS date_rem,ip,reason + FROM blacklist + WHERE date_rem IS NOT NULL AND date_rem > (now() - interval '24 hour') + ORDER BY date_add DESC LIMIT ?"; + } elsif (defined(param('only')) && param('only') eq 'act') { + $query = "SELECT + TO_CHAR(date_add, 'DD.Mon.YYYY HH24:MI') AS date_add, TO_CHAR(date_rem, 'DD.Mon.YYYY HH24:MI') AS date_rem,ip,reason + FROM blacklist + WHERE date_rem IS NULL AND date_add > (now() - interval '24 hour') + ORDER BY date_add DESC LIMIT ?"; + } else { + $query = "SELECT TO_CHAR(date_add, 'DD.Mon.YYYY HH24:MI') AS date_add, TO_CHAR(date_rem, 'DD.Mon.YYYY HH24:MI') AS date_rem,ip,reason FROM blacklist ORDER BY date_add DESC LIMIT ?"; + } + } + if ( defined(param('lim')) && param('lim') =~ /^[0-9+]$/ ) { + push @values, param('lim'); + } else { + push @values, 200; # default LIMIT for the SQL queryes + } + my $get_info = $conn->prepare($query) or web_error("Unable to prepare query: $DBI::errstr"); + $get_info->execute(@values) or web_error("Unable to execute query: $DBI::errstr"); + my $lines=''; + my $line0 = "__DATE0____DATE1____IP____REASON__\n"; + if ($type) { + print "

Listing the blacklist for the last 26 hours(limited to the first 200 entries):

"; + } else { + print "

Listing search results(limited to the first 200 entries):

"; + } + print " + + + + + + "; + + while ( my @str = $get_info->fetchrow_array) { + my $line = $line0; + my $val = $str[1]; + $val = 'none' if ($str[1] eq ''); + $line =~ s/__DATE0__/$str[0]/; + $line =~ s/__DATE1__/$val/; + $line =~ s/__IP__/$str[2]/g; + $line =~ s/__REASON__/$str[3]/; + print $line; + } + print '
Date addedDate removedIP AddressReason
'; +} elsif ($action eq 'stat') { + my $query =''; + if ( defined(param('w')) && + defined(param('ss')) && + param('w') eq 'sv' && + param('ss') =~ /^[0-9]+$/ ) { + my $service = get_num_service(param('ss')); + web_error('Unknown service') if !defined($service); + my $sort = 'count'; + if (defined(param('sort'))) { + $sort = 'ip' if param('sort') == 0; + } + my $query1 = "SELECT ip,COUNT(ip) AS count FROM failed_log WHERE \"service\" ~ '$service' AND \"date\" > (now() - interval '1 hour') GROUP BY ip ORDER BY $sort DESC"; + my $query24 = "SELECT ip,COUNT(ip) AS count FROM failed_log WHERE \"service\" ~ '$service' AND \"date\" > (now() - interval '24 hour') GROUP BY ip ORDER BY $sort DESC"; + my $get_info1 = $conn->prepare($query1) or web_error("Unable to prepare query: $DBI::errstr"); + my $get_info24 = $conn->prepare($query24) or web_error("Unable to prepare query: $DBI::errstr"); + $get_info1->execute() or web_error("Unable to execute query: $DBI::errstr"); + my $script = " + +"; + my $table = "

Listing results for service %s(last 1 %s):

+ + + + +\n"; + print $script; + printf $table, $service, 'hour'; + my $lines=''; + my $line0 = "\n"; + while ( my @str = $get_info1->fetchrow_array) { + my $line = $line0; + $line =~ s/__IP__/$str[0]/g; + $line =~ s/__COUNT__/$str[1]/; + print $line; + } + print '
IP AddressFailed count
__IP____COUNT__
'; + $get_info1->execute() or web_error("Unable to execute query: $DBI::errstr"); + printf $table, $service, 'day'; + while ( my @str = $get_info24->fetchrow_array) { + my $line = $line0; + $line =~ s/__IP__/$str[0]/; + $line =~ s/__COUNT__/$str[1]/; + print $line; + } + print ''; + + + } + +} else { + my $lines = ''; + my $line0 = "__DATE____COUNT__\n"; + my $line1 = "__SSH____FTP____POP3____IMAP____CPANEL__\n"; + my $line2 = "__IP__\n"; + + # last 1 hour + my $broots0 = $conn->selectrow_array(" + SELECT COUNT(id) AS id + FROM broots + WHERE date > (now() - interval '1 hour')"); + my $failed0 = $conn->selectrow_array(" + SELECT COUNT(id) AS id + FROM failed_log + WHERE date > (now() - interval '1 hour')"); + my $broots1 = $conn->prepare("SELECT + TO_CHAR(\"date\", 'YY-MM-DD') AS ddate, COUNT( id ) AS count + FROM broots + GROUP BY TO_CHAR(\"date\", 'YY-MM-DD') + ORDER BY TO_CHAR(\"date\", 'YY-MM-DD') DESC LIMIT 7"); + my $failed1 = $conn->prepare("SELECT + TO_CHAR(\"date\", 'YY-MM-DD') AS ddate, COUNT( id ) AS count + FROM failed_log + GROUP BY TO_CHAR(\"date\", 'YY-MM-DD') + ORDER BY TO_CHAR(\"date\", 'YY-MM-DD') DESC LIMIT 7"); + + my $blacklisted_1h_active = $conn->selectrow_array("SELECT + COUNT(id) AS count + FROM blacklist + WHERE date_add > (now() - interval '1 hour') AND date_rem IS NULL"); + my $blacklisted_1h_removed = $conn->selectrow_array("SELECT + COUNT(id) AS count + FROM blacklist + WHERE date_add > (now() - interval '1 hour') AND date_rem IS NOT NULL"); + my @blacklisted_days_active0 = $conn->selectrow_array("SELECT + COUNT(id) AS count + FROM blacklist + WHERE date_rem IS NULL AND date_add > (now() - interval '24 hour') "); + my @blacklisted_days_removed0 = $conn->selectrow_array("SELECT + COUNT(id) + FROM blacklist + WHERE date_rem IS NOT NULL AND date_rem > (now() - interval '24 hour')"); + my $blacklisted_days_active = $conn->prepare("SELECT + TO_CHAR(date_add, 'YYYY-MM-DD') AS date_add, COUNT(TO_CHAR(date_add, 'YYYY-MM-DD')) AS count + FROM blacklist + WHERE date_rem IS NULL + GROUP BY TO_CHAR(date_add, 'YYYY-MM-DD') + ORDER BY TO_CHAR(date_add, 'YYYY-MM-DD') DESC LIMIT ?"); + my $blacklisted_days_removed = $conn->prepare("SELECT + TO_CHAR(date_add, 'YYYY-MM-DD') AS date_add, COUNT(TO_CHAR(date_add, 'YYYY-MM-DD')) AS count + FROM blacklist + WHERE date_rem IS NOT NULL + GROUP BY TO_CHAR(date_add, 'YYYY-MM-DD') + ORDER BY TO_CHAR(date_add, 'YYYY-MM-DD') DESC LIMIT 1"); + + if (defined(param('cgi'))) { + print "$broots0|$failed0|0:$blacklisted_1h_active|0:$blacklisted_days_active0[1]|1:$blacklisted_1h_removed|1:$blacklisted_days_removed0[1]"; +# $blacklisted_days_active->execute('1'); +# while (my ($date, $count) = $blacklisted_days_active->fetchrow_array) { +# print "|0:$date:$count"; +# } +# print "|1:$blacklisted_1h_removed"; +# $blacklisted_days_removed->execute('1'); +# while (my ($date, $count) = $blacklisted_days_removed->fetchrow_array) { +# print "|1:$date:$count"; +# } + print "\n"; + } else { + my $brutes1 = $conn->prepare(" + SELECT DISTINCT ip FROM broots + WHERE date > (now() - interval '1 hour') ORDER BY ip"); + my $brutes24 = $conn->prepare(" + SELECT DISTINCT ip FROM broots + WHERE date > (now() - interval '1 day') ORDER BY ip"); + my $brutes7 = $conn->prepare(" + SELECT DISTINCT ip FROM broots + WHERE date > (now() - interval '7 day') ORDER BY ip"); + my $ftp = $conn->selectrow_array('SELECT COUNT(id) FROM failed_log + WHERE service = \'ftp\' AND date > (now() - interval \'1 hour\')'); + my $ssh = $conn->selectrow_array('SELECT COUNT(id) FROM failed_log + WHERE service = \'ssh\' AND date > (now() - interval \'1 hour\')'); + my $pop3 = $conn->selectrow_array('SELECT COUNT(id) FROM failed_log + WHERE service = \'pop3\' AND date > (now() - interval \'1 hour\')'); + my $imap = $conn->selectrow_array('SELECT COUNT(id) FROM failed_log + WHERE service = \'imap\' AND date > (now() - interval \'1 hour\')'); + my $cpanel = $conn->selectrow_array('SELECT COUNT(id) FROM failed_log + WHERE service = \'cpanel\' AND date > (now() - interval \'1 hour\')'); + $html .= get_template('summary'); + + my $i=2; + my @values0; + my $lines_0 .= $line0; + $lines_0 =~ s/__DATE__/last 1 hour/; + $lines_0 =~ s/__COUNT__/$failed0/; + $values0[0][0] = 'Date'; + $values0[0][1] = 'attempts count'; + $values0[0][2] = 'Failed count for the last 7 days'; + $values0[1][0] = 'last 1 hour'; + $values0[1][1] = '0'; + $values0[1][2] = $failed0; + $failed1->execute(); + while (my ($date, $count) = $failed1->fetchrow_array) { + $lines_0 .= $line0; + $lines_0 =~ s/__DATE__/$date/; + $lines_0 =~ s/__COUNT__/$count/; + $values0[$i][0]=$date; + $values0[$i][1]=$i; + $values0[$i][2]=$count; + $i++; + } + my $graph0 = build_graph(@values0); + + + my @values1; + my $lines_1 .= $line0; + $lines_1 =~ s/__DATE__/last 1 hour/; + $lines_1 =~ s/__COUNT__/$broots0/; + $values1[0][0] = 'Date'; + $values1[0][1] = 'attempts count'; + $values1[0][2] = 'Bruteforce attempts for the last 7 days'; + $values1[1][0] = 'last 1 hour'; + $values1[1][1] = '0'; + $values1[1][2] = $broots0; + $i=2; + $broots1->execute(); + while (my ($date, $count) = $broots1->fetchrow_array) { + $lines_1 .= $line0; + $lines_1 =~ s/__DATE__/$date/; + $lines_1 =~ s/__COUNT__/$count/; + $values1[$i][0]=$date; + $values1[$i][1]=$i; + $values1[$i][2]=$count; + $i++; + } + my $graph1 = build_graph(@values1); + my $lines_2 = $line0; + $lines_2 =~ s/__DATE__/last 1 hour/; + $lines_2 =~ s/__COUNT__/$blacklisted_1h_active/; + $blacklisted_days_active->execute('3'); + while (my ($date, $count) = $blacklisted_days_active->fetchrow_array) { + $lines_2 .= $line0; + $lines_2 =~ s/__DATE__/$date/; + $lines_2 =~ s/__COUNT__/$count/; + } + my $lines_3 = $line0; + $lines_3 =~ s/__DATE__/last 1 hour/; + $lines_3 =~ s/__COUNT__/$blacklisted_1h_removed/; + $blacklisted_days_removed->execute('3'); + while (my ($date, $count) = $blacklisted_days_removed->fetchrow_array) { + $lines_3 .= $line0; + $lines_3 =~ s/__DATE__/$date/; + $lines_3 =~ s/__COUNT__/$count/; + } + + $html =~ s/__TABLE0__/$lines_0/; + $html =~ s/__TABLE1__/$lines_1/; + $html =~ s/__GRAPH0__/$graph0/ig; + $html =~ s/__GRAPH1__/$graph1/ig; + $html =~ s/__TABLE6__/$lines_2/; + $html =~ s/__TABLE7__/$lines_3/; + + + $line1 =~ s/__FTP__/$ftp/; + $line1 =~ s/__SSH__/$ssh/; + $line1 =~ s/__POP3__/$pop3/; + $line1 =~ s/__IMAP__/$imap/; + $line1 =~ s/__CPANEL__/$cpanel/; + $html =~ s/__TABLE2__/$line1/; + + $lines = ''; + $brutes1->execute; + while ( my $ip = $brutes1->fetchrow_array) { + $lines .= $line2; + $lines =~ s/__IP__/$ip/g; + } + $html =~ s/__TABLE3__/$lines/g; + $lines = ''; + $brutes24->execute; + while ( my $ip = $brutes24->fetchrow_array) { + $lines .= $line2; + $lines =~ s/__IP__/$ip/g; + } + $html =~ s/__TABLE4__/$lines/g; + $lines = ''; + $brutes7->execute; + while ( my $ip = $brutes7->fetchrow_array) { + $lines .= $line2; + $lines =~ s/__IP__/$ip/g; + } + $html =~ s/__TABLE5__/$lines/g; + } +} +print $html,"\n"; +exit 0; diff --git a/hawk.db b/hawk.db new file mode 100644 index 0000000..5100c37 --- /dev/null +++ b/hawk.db @@ -0,0 +1,31 @@ +## Variant 2 +CREATE DATABASE IF NOT EXISTS hawk; +USE hawk; + +CREATE TABLE IF NOT EXISTS `failed_log` ( + `id` int(11) NOT NULL auto_increment, + `date` timestamp NOT NULL, + `ip` varchar(16) NOT NULL default '', + `user` varchar(16) NULL default '', + `service` varchar(24) NOT NULL default '', + PRIMARY KEY (`id`), + KEY `ip` (`ip`), + KEY `date` (`date`), + KEY `service` (`service`) +) TYPE=MyISAM ; + +CREATE TABLE IF NOT EXISTS `broots` ( + `id` int(11) NOT NULL auto_increment, + `date` timestamp NOT NULL, + `ip` varchar(16) NOT NULL default '', + `service` varchar(24) NOT NULL default '', + PRIMARY KEY (`id`), + KEY `ip` (`ip`), + KEY `date` (`date`), + KEY `service` (`service`) +) TYPE=MyISAM ; + +GRANT USAGE ON * . * TO 'hawk'@'localhost' IDENTIFIED BY 'hawkpass' WITH MAX_QUERIES_PER_HOUR 0 MAX_CONNECTIONS_PER_HOUR 0 MAX_UPDATES_PER_HOUR 0 ; +FLUSH PRIVILEGES; +GRANT SELECT , INSERT , UPDATE , DELETE , REFERENCES , INDEX , ALTER ON `hawk`.`failed_log` TO 'hawk'@'localhost';GRANT SELECT , INSERT , UPDATE , DELETE , REFERENCES , INDEX , ALTER ON `hawk`.`broots` TO 'hawk'@'localhost'; +FLUSH PRIVILEGES; diff --git a/hawk.init b/hawk.init new file mode 100755 index 0000000..3ece9e9 --- /dev/null +++ b/hawk.init @@ -0,0 +1,251 @@ +#!/bin/bash +# Hawk init script +# version 2.0 +hawkurl='http://master.sgvps.net/hawk/' +pidfile='/var/run/hawk.pid' +program='/usr/local/sbin/hawk.pl' +check_status() { + check=0; + okcount=0; + for i in 1 2 3 4 5; do + if [ -f $pidfile ]; then + if [ "$1" == 0 ]; then + if [ -d /proc/$(cat $pidfile) ]; then + if [ "$okcount" -gt '1' ]; then + check=1; + continue 6; + fi + let okcount++; + fi + else + if [ ! -d /proc/$(cat $pidfile) ]; then + check=1; + continue 6; + fi + fi + else + if [ "$1" != 0 ]; then + check=1 + continue 6 + fi + fi + sleep 1 + done + if [ $check == "1" ]; then + echo -e "OK" + else + echo -e "FAILED" + fi +} +# Starting Hawk +start_program() { + if [ -x $program ]; then + if [ "$1" == 'debug' ]; then + $program debug + else + $program + fi + echo -n 'Starting Hawk: ' + fi + check_status 0 +} + +# Stop the Hawk +stop_program() { + echo -n 'Stopping Hawk: ' + if [ -f $pidfile ]; then + pid=`cat $pidfile` + if [ -d /proc/$pid ]; then + kill -9 $pid + fi + rm -f $pidfile + fi + check_status 1 +} + +check_run() { + if [ ! -f $pidfile ]; then + start_program + else + pid=`cat $pidfile`; + if [ ! -d /proc/$pid ]; then + start_program + fi + fi +} + +# change the DB password for all hawk tools +change_hawk_pass() { + echo "Changing Hawk password" + newpass=`echo $RANDOM|md5sum|cut -c 1-12` + echo "New password: $newpass" + su - postgres -c "psql -c \"ALTER USER hawk PASSWORD '$newpass'\" template1" + sed -i '/hawk/s/\(.*\)/#\1/' ~/.pgpass + echo "*:5432:hawk:hawk:$newpass" >> ~/.pgpass + if ( ! grep hawk /var/lib/pgsql/data/pg_hba.conf > /dev/null ); then + sed -i "/^local\s\+all\s\+all\s\+ident\s\+sameuser/ihost hawk hawk 127.0.0.1 md5" /var/lib/pgsql/data/pg_hba.conf + /etc/init.d/postgresql reload + fi + chmod 600 ~/.pgpass + sed -i "/^my \$pass/s/=.*/= '$newpass';/" /usr/local/sbin/hawk.pl + sed -i "/dbpass/s/=.*/=$newpass/" /home/sentry/hackman/hawk-web.conf + stop_program + sleep 1 + start_program +} + + + +# Install or reinstall the Hawk +hawk_install() { + echo "Checking and removing old hawk files" + if [ -f /usr/local/sbin/hawk.pl ]; then + rm -f /usr/local/sbin/hawk.pl + fi + if [ -f /root/hawk-blocker.sh ]; then + rm -f /root/hawk-blocker.sh + fi + if [ -f ~sentry/public_html/cgi-bin/hawk-web.pl ]; then + rm -f ~sentry/public_html/cgi-bin/hawk-web.pl + fi + if [ -f /root/hawk_db.sql ]; then + rm -f /root/hawk_db.sql + fi + echo 'Downloading HAWK Daemon' + wget -c $hawkurl/hawk.source -O /usr/local/sbin/hawk.pl + echo 'Downloading hawk-blocker' + wget -c $hawkurl/hawk-blocker.source -O /root/hawk-blocker.sh + chmod 750 /root/hawk-blocker.sh /usr/local/sbin/hawk.pl + echo 'Downloading hawk web interface' + wget -c $hawkurl/hawk-web.source -O /home/sentry/public_html/cgi-bin/hawk-web.pl + chmod 755 /home/sentry/public_html/cgi-bin/hawk-web.pl + if [ ! -d /home/sentry/hackman ]; then + mkdir -p /home/sentry/hackman + fi + echo 'Downloading hawk web interface configuration file' + wget -c $hawkurl/hawk-web.conf -O /home/sentry/hackman/hawk-web.conf + chown sentry: /home/sentry/public_html/cgi-bin/hawk-web.pl /home/sentry/hackman/hawk-web.conf + echo 'Downloading hawk web interface html templates' + wget -c $hawkurl/templates.tgz -O /home/sentry/hackman/templates.tgz + chown -R sentry: /home/sentry/hackman + echo 'Downloading hawk PgSQL database dump' + wget -c $hawkurl/hawk_db.sql -O /root/hawk_db.sql + echo "Installing hawk-web templates" + tar xfvz /home/sentry/hackman/templates.tgz -C /home/sentry/hackman + rm -f /home/sentry/hackman/templates.tgz + chown -R sentry: /home/sentry/hackman + echo "Initializing Hawk DB..." + su - postgres -c "psql -c \"CREATE USER hawk PASSWORD 'myHawkPass'\" template1" + su - postgres -c "psql -c 'CREATE DATABASE hawk OWNER hawk' template1" + mv hawk_db.sql /var/lib/pgsql/ + su - postgres -c "psql -f hawk_db.sql hawk" + change_hawk_pass + rm -f /var/lib/pgsql/hawk_db.sql + if ( ! grep 'HAWK-' /root/admin/sgfirewall > /dev/null ); then + echo -e "############ HAWK-BLCOKED ####################\n############ HAWK+BLOCKED ####################" >> /root/admin/sgfirewall + echo "Added HAWK lines to /root/admin/sgfirewall" + fi + + if ( ! grep hawk /var/spool/cron/root > /dev/null ); then + chattr -ia /var/spool/cron/root + echo '17 * * * * /root/hawk-blocker.sh >> /root/hawk-blocker.log 2>&1' >> /var/spool/cron/root + chattr +ia /var/spool/cron/root + fi +} +create_table() { + if ( ! wget -c $hawkurl/hawk_db.sql-$1 -O /var/lib/pgsql/hawk_db.sql-$1 ); then + return 0 + fi + su - postgres -c "psql -f hawk_db.sql-$1 hawk" + rm -f /var/lib/pgsql/hawk_db.sql-$1 +} + +# fix common problems with the DB +check_db() { + list='broots failed_log blacklist' + for table in ${list}; do + if ( ! psql -q -t -Uhawk hawk -c '\d'|cut -d " " -f 4|grep -P "^$table$" > /dev/null); then + echo "Missing table $table" + create_table $table + fi + done + echo "DB tables: OK" +} + +# fix missing files +check_files() { + error=0 + if [ ! -f /root/hawk-blocker.sh ]; then + let error++ + wget -c $hawkurl/hawk-blocker.sh -O /root/hawk-blocker.sh + fi + if [ ! -f ~sentry/public_html/cgi-bin/hawk-web.pl ]; then + let error++ + wget -c $hawkurl/hawk-web.pl -O ~sentry/public_html/cgi-bin/hawk-web.pl + + fi + if [ ! -f ~sentry/public_html/cgi-bin/hawk-web.pl ]; then + let error++ + if [ ! -d /home/sentry/hackman ]; then + mkdir -p /home/sentry/hackman + fi + wget -c $hawkurl/hawk-web.conf -O /home/sentry/hackman/ + + fi + if [ ! -f /usr/local/sbin/hawk.pl ]; then + let error++ + wget -c $hawkurl/hawk.pl -O /usr/local/sbin/hawk.pl + fi + if [ "$error" == '4' ]; then + hawk_install + else + change_hawk_pass + fi +} +# Control structure +case "$1" in + 'start') + start_program + ;; + 'stop') + stop_program + ;; + 'restart') + stop_program + sleep 1 + start_program + ;; + 'debug') + start_program debug + ;; + 'reload') + kill -HUP `cat $pidfile` + ;; + 'changepass') + change_hawk_pass + ;; + 'checkdb') + check_db + ;; + 'install') + if [ "$2" == "cgi" ]; then + hawk_install + else + echo -n "Are you sure you want a complete reinstall of Hawk(y/n): " + read a + if [ "$a" != 'y' ]; then + exit 0; + fi + hawk_install + fi + ;; + 'status') + echo -n 'Hawk status: ' + check_status 0 + ;; + 'check_run') + check_run + ;; + *) + echo "usage $0 start|stop|restart|debug|reload|install|changepass" +esac diff --git a/hawk.pl b/hawk.pl new file mode 100755 index 0000000..0e93d04 --- /dev/null +++ b/hawk.pl @@ -0,0 +1,422 @@ +#!/usr/bin/perl -T +use strict; +use warnings; +use DBD::mysql; +use POSIX qw(setsid), qw(strftime); # use only setsid & strftime from POSIX + +# system variables +$ENV{PATH} = ''; # remove unsecure path +my $version = '0.71'; # version string + +# defining fault hashes +our %ssh_faults; # ssh faults storage +our %ftp_faults; # ftp faults storage +our %pop3_faults; # pop3 faults storage +our %imap_faults; # imap faults storage +our %smtp_faults; # smtp faults storage +our %cpanel_faults; # cpanel faults storage +our %notifications; # notifications + +# make DB vars +my $db = 'DBI:Pg:database=hawk;host=localhost;port=5432'; +my $user = 'hawk'; +my $pass = '157856cc61d4'; + +# Hawk files +my $logfile = '/var/log//hawk.log'; # daemon logfile +my $pidfile = '/var/run/hawk.pid'; # daemon pidfile +my $ioerrfile = '/home/sentry/public_html/io.err'; # File where to add timestamps for I/O Errors +my $log_list = '/usr/bin/tail -f /var/log/messages /var/log/secure /var/log/maillog /usr/local/cpanel/logs/access_log |'; +our $broot_time = 300; # time(in seconds) before cleaning the hashes +our $max_attempts = 5; # max number of attempts(for $broot_time) before notify +our $debug = 0; # by default debuging is OFF +our $do_limit = 0; # by default do not limit the offending IPs + +my $start_time = time(); +my $myip = get_ip(); + + +# check for debug +if ( defined($ARGV[0]) && $ARGV[0] =~ /debug/ ) { + $debug=1; # turn on debuging +} + +# changing to unbuffered output +our $| = 1; + +# Change program name +$0 = "[Hawk]"; + +# open the logfile +open HAWK, '>>', $logfile or die "DIE: Unable to open logfile $logfile: $!\n"; +logger("Hawk version $version started!"); +#print HAWK get_time(), " Hawk version $version started!\n"; + + +# execute this before DIE-ing :) +$SIG{__DIE__} = sub { logger(@_); }; + +# check if the daemon is running +if ( -e $pidfile ) { + # get the old pid + open PIDFILE, '<', $pidfile or die "DIE: Can't open pid file($pidfile): $!\n"; + my $old_pid = ; + close PIDFILE; + # check if $old_pid is still running + if ( $old_pid =~ /[0-9]+/ ) { + if ( -d "/proc/$old_pid" ) { + logger("Hawk is already running!"); + die "DIE: Hawk is already running!\n"; + } + } else { + logger("Incorrect pid format!"); + die "DIE: Incorrect pid format!\n"; + } +} + +# get the server IP address +sub get_ip { + my @ip; + open IP, "/sbin/ip a l |" or die "DIE: Unable to get local IP Address: $!\n"; + while () { + if ( $_ =~ /eth0$/) { + @ip = split /\s+/, $_; + $ip[2] =~ s/\/[0-9]+//; + if ($debug) { + print $ip[2], "\n"; + } + } + } + close IP; + return $ip[2] +} +sub check_ip { + if ( $_[0] =~ /[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}/ ) { + return 1; + } else { + return 0; + } +} + +# generate time format: 15.May.07 02:41:52 +sub get_time { + return strftime('%b %d %H:%M:%S', localtime(time)); +} + +sub logger { + print HAWK strftime('%b %d %H:%M:%S', localtime(time)) . ' ' . $_[0] . "\n"; +} + +# clean the hashes +sub cleanh { + delete @ftp_faults{keys %ftp_faults}; + delete @ssh_faults{keys %ssh_faults}; + delete @pop3_faults{keys %pop3_faults}; + delete @imap_faults{keys %imap_faults}; + delete @cpanel_faults{keys %cpanel_faults}; + delete @notifications{keys %notifications}; + logger("hashes cleaned!"); +} + +# check for broots +sub check_broot { +# foreach ( keys %ssh_faults ) { +# if ( $ssh_faults{$_} > $max_attempts ) { +# notify('ssh', $_, $ssh_faults{$_}); +# } +# } +# foreach ( keys %pop3_faults ) { +# if ( $pop3_faults{$_} >= $max_attempts ) { +# notify('pop3', $_, $pop3_faults{$_}); +# } +# } +# foreach ( keys %imap_faults ) { +# if ( $imap_faults{$_} > $max_attempts ) { +# notify('imap', $_, $imap_faults{$_}); +# } +# } +# foreach ( keys %smtp_faults ) { +# if ( $smtp_faults{$_} > $max_attempts ) { +# notify('imap', $_, $smtp_faults{$_}); +# } +# } +# foreach ( keys %cpanel_faults ) { +# if ( $cpanel_faults{$_} > $max_attempts ) { +# notify('cPanel', $_, $cpanel_faults{$_}); +# } +# } + while ( my ($k,$v) = each (%ssh_faults) ) { + if ( $v > $max_attempts ) { + notify('ssh', $k, $ssh_faults{$k}); + } + } + while ( my ($k,$v) = each (%pop3_faults) ) { + if ( $v >= $max_attempts ) { + notify('pop3', $k, $v); + } + } + while ( my ($k,$v) = each (%imap_faults) ) { + if ( $v > $max_attempts ) { + notify('imap', $k, $imap_faults{$k}); + } + } + while ( my ($k,$v) = each (%smtp_faults) ) { + if ( $v > $max_attempts ) { + notify('imap', $k, $smtp_faults{$k}); + } + } + while ( my ($k,$v) = each (%cpanel_faults) ) { + if ( $v > $max_attempts ) { + notify('cPanel', $k, $cpanel_faults{$k}); + } + } + while ( my ($k,$v) = each (%ftp_faults) ) { + if ( $v > $max_attempts ) { + notify('ftp', $k, $ftp_faults{$k}); + } + } +} + +# Fork to background +defined(my $pid=fork) or die "DIE: Cannot fork process: $! \n"; +exit if $pid; +setsid or die "DIE: Unable to setsid: $!\n"; +umask 0; + +# redirect standart file descriptors to /dev/null +open STDIN, '>/dev/null' or die "DIE: Cannot write to stdout: $! \n"; +if (!$debug) { + open STDERR, '>>/dev/null' or die "DIE: Cannot write to stderr: $! \n"; +} + +# write the program pid to the $pidfile +open PIDFILE, '>', $pidfile or die "DIE: Unable to open pidfile $pidfile: $!\n"; +print PIDFILE $$; +close PIDFILE; + +# open logs +open LOGS, $log_list or die "DIE: Unable to open logs: $!\n"; + +# make the output unbuffered +select((select(HAWK), $| = 1)[0]); +select((select(LOGS), $| = 1)[0]); + +# prepare the connection +our $conn = DBI->connect_cached( $db, $user, $pass, { PrintError => 1, AutoCommit => 1 }) or die "DIE: Unable to connecto to DB: $!\n"; +our $log_me = $conn->prepare('INSERT INTO failed_log ( ip, "user", service ) VALUES ( ?, ?, ? ) ') + or die "DIE: Unable to prepare log query: $!\n"; +our $broot_me = $conn->prepare('INSERT INTO broots ( ip, service ) VALUES ( ?, ? ) ') + or die "DIE: Unable to prepare broot query: $!\n"; +my $get_failed = $conn->prepare('SELECT COUNT(id) AS id FROM failed_log') + or die "Unable to prepare log query: $!\n"; + +# $conn->{mysql_auto_reconnect} = 1; +# if ($conn->{mysql_auto_reconnect}) { +# print "Autoreconnect OK\n" if ($debug == 1); +# } else { +# die "Autoreconnect turned OFF!\n"; +# } + +# notification to admins +sub notify { + # 0 - SERVICE + # 1 - IP + # 2 - COUNT + my %services = ( + ftp => '21', + ssh => '22', + smtp => '25', + pop3 => '110', + imap => '143', + cPanel => '2083' + ); + # log to DB and file + if ( ! exists $notifications {$_[1]} ) { + $notifications{$_[1]}=1; + $broot_me->execute($_[1],$_[0]) or logger("Failed broot: $_[0], $_[1], $_[2] |$DBI::errstr"); + logger("!!! $_[0] $_[1] failed $_[2] times in $broot_time seconds"); + } + # Limit the offender + if ($do_limit) { +# variant 0 +# iptables -I in_hawk -s $_[0] -p tcp --dport $_[2] +# variant 1 +#iptables -A in_hawk -p tcp --dport $_[2] --syn -m recent --name --update --seconds 60 --hitcount 6 -m limit --limit 6/minute -j SSH_bruteforce +# variant 2 +#iptables -I in_hawk -i eth0 -p tcp --dport $_[2] -s $_[0] -m state --state NEW -m recent --set +#iptables -I in_hawk -i eth0 -p tcp --dport $_[2] -s $_[0] -m state --state NEW -m recent --update --seconds 120 --hitcount 1 -j DROP + if (system("/usr/local/sbin/iptables -I in_hawk -i eth0 -p tcp --dport $services{$_[2]} -s $_[0] -m state --state NEW -m recent --set && iptables -I in_hawk -i eth0 -p tcp --dport $_[2] -s $_[0] -m state --state NEW -m recent --update --seconds 120 --hitcount 1 -j DROP ")) { + logger("Unable to block: $_[0], $_[1], $_[2]"); + } else { + logger("Blocked: ip - $_[0], port - $_[2]"); + } + } +} + +sub send_fault() { + eval { + local $SIG{ALRM} = sub { die 'alarm'; }; + alarm 5; + use IO::Socket::INET; + if ( my $sock = new IO::Socket::INET ( PeerAddr => '64.246.15.53', PeerPort => '80', Proto => 'tcp', Timeout => '3') ) { + # Send the faulty drive! + print $sock "GET /~sentry/cgi-bin/ioerrors.pl?hdd=$_[0]\n\n"; + my @replay = <$sock>; + close $sock; + } else { + logger "Unable to connect to report IO error: $!"; + } + open IOERR, '>', $ioerrfile or logger('Unable to log I/O Error'); + print IOERR strftime('%b %d %H:%M:%S', localtime(time)) . " $_[0]\n"; + close IOERR; + alarm 0; + }; +} + +our @senders = (); +while () { + eval { + local $SIG{ALRM} = sub { die 'alarm'; }; + alarm 2; + # clean the childs(RAPER) + # I have to write the new(better) RAPER, using SIGCHLD + waitpid($_, 0) foreach(@senders); + alarm 0; + }; + # Feb 13 19:18:35 serv01 kernel: end_request: I/O error, dev sdb, sector 1405725148 + # Feb 13 19:18:58 serv01 kernel: end_request: I/O error, dev sdb, sector 1405727387 + if ( $_ =~ /I\/O error/i ) { + my @line = split /\s+/, $_; + # fork the checker + my $pid = fork(); + # add the child's pid to che fork array + push @senders, $pid; + defined $pid or print "Resources not avilable. Unable to fork checker.\n"; + setsid; + if ($pid == 0) { + # this is the child + $0="sending_hdd_fault_on-".$line[14]; + &send_fault($line[14]); + exit 0; + } + } elsif ( $_ =~ /ssh/ && $_ =~ /Failed/ ) { + #May 15 11:36:27 serv01 sshd[5448]: Failed password for support from ::ffff:67.15.243.7 port 47597 ssh2 + #May 16 03:27:24 serv01 sshd[25536]: Failed password for invalid user suport from ::ffff:85.14.6.2 port 52807 ssh2 + my @sshd = split /\s+/, $_; + my $ip = ''; + my $user = ''; + if ( $sshd[8] =~ /invalid/ ) { + $sshd[12] =~ s/::ffff://; + $ip = $sshd[12]; + $user = $sshd[10]; + } else { + $sshd[10] =~ s/::ffff://; + $ip = $sshd[10]; + $user = $sshd[8]; + } + next if ( $ip =~ /$myip/ ); # this is the local server + $log_me->execute($ip, $user, 'ssh'); + if ( exists $ssh_faults {$ip} ) { + $ssh_faults{$ip}++; + } else { + $ssh_faults{$ip} = 1; + } + logger(" IP $ip failed to identify to ssh.") if ($debug); + } elsif ( $_ =~ /cpanelpop/ && $_ =~ /totalxfer=102\s*$/ ) { + #May 16 02:37:31 serv01 cpanelpop[29746]: Session Closed host=67.15.172.8 ip=67.15.172.8 user=root realuser= totalxfer=102 + my @pop3 = split /\s+/, $_; + if ( defined($pop3[10]) && $pop3[10] =~ /realuser=/ ) { + $pop3[7] =~ s/host=//; + next if ( $pop3[7] =~ /$myip/ ); # this is the local server + $log_me->execute($pop3[7], '', 'pop3') or logger("Failed to insert: $pop3[7], $DBI::errstr"); + if ( exists $pop3_faults {$pop3[7]} ) { + $pop3_faults{$pop3[7]}++; + } else { + $pop3_faults{$pop3[7]} = 1; + } + logger("IP $pop3[7] faild to identify to cppop") if ($debug); + } + } elsif ( $_ =~ /pop3d:/ && $_ =~ /FAILED/ ) { + # Courier POP3 + #May 11 03:58:40 serv01 pop3d: LOGIN FAILED, user=kate, ip=[::ffff:72.43.28.210] + my @pop3 = split /\s+/, $_; + $pop3[8] =~ s/ip=\[(.*)\]/$1/; + $pop3[7] =~ s/user=(.*),/$1/; + $pop3[8] =~ s/.*:// if $pop3[8] =~ /ffff/; + next if ( $pop3[8] =~ /$myip/ ); # this is the local server + $log_me->execute($pop3[8], $pop3[7], 'pop3'); + if ( exists $pop3_faults {$pop3[8]} ) { + $pop3_faults{$pop3[8]}++; + } else { + $pop3_faults{$pop3[8]} = 1; + } + logger("IP $pop3[8]($pop3[7]) faild to identify to courier-pop3") if ($debug); + } elsif ( $_ =~ /imapd/ && $_ =~ /failed/ ) { + # cPanel IMAP + #May 17 17:06:44 serv01 imapd[32199]: Login failed user=dsada domain=(null) auth=dsada host=[85.14.6.2] + my @imap = split /\s+/, $_; + $imap[10] =~ s/host=\[(.*)\]/$1/; + $imap[7] =~ s/user=//; + next if ( $imap[10] =~ /$myip/ ); # this is the local server + $log_me->execute($imap[10], $imap[7], 'imap'); + if ( exists $imap_faults {$imap[10]} ) { + $imap_faults{$imap[10]}++; + } else { + $imap_faults{$imap[10]} = 1; + } + logger("IP $imap[10]($imap[7]) failed to identify to cpimap.") if ($debug); + } elsif ( $_ =~ /imapd/ && $_ =~ /FAILED/ ) { + # Courier IMAP + #May 15 05:26:16 serv01 imapd: LOGIN FAILED, user=admin, ip=[::ffff:67.15.243.20] + my @imap = split /\s+/, $_; + $imap[8] =~ s/host=\[::ffff:(.*)\]/$1/; + $imap[7] =~ s/user=//; + next if ( $imap[8] =~ /$myip/ ); # this is the local server + $log_me->execute($imap[8], $imap[7], 'imap'); + if ( exists $imap_faults {$imap[8]} ) { + $imap_faults{$imap[8]}++; + } else { + $imap_faults{$imap[8]} = 1; + } + logger(" IP $imap[8]($imap[7]) failed to identify to courier-imap.") if ($debug); + } elsif ( $_ =~ /pure-ftpd:/ && $_ =~ /failed/ ) { + #May 16 03:06:43 serv01 pure-ftpd: (?@85.14.6.2) [WARNING] Authentication failed for user [mamam] + #Mar 7 01:03:49 serv01 pure-ftpd: (?@68.4.142.211) [WARNING] Authentication failed for user [streetr1] + my @ftp = split /\s+/, $_; + $ftp[5] =~ s/\(.*\@(.*)\)/$1/; # get the IP + $ftp[11] =~ s/\[(.*)\]/$1/; # get the username + $log_me->execute($ftp[5], $ftp[11], 'ftp') or logger("Unable to execute query: $DBI::errstr"); + if ( exists $ftp_faults {$ftp[5]} ) { + $ftp_faults{$ftp[5]}++; + } else { + $ftp_faults{$ftp[5]} = 1; + } + logger("IP $ftp[5]($ftp[11]) failed to identify to Pure-FTPD.") if ($debug); +# } elsif ( $_ =~ / 401 / && $_ =~ /GET / ) { +# #85.14.6.2 - root [05/16/2007:07:58:28 -0000] "GET / HTTP/1." 401 0 "" "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20061208 Firefox/2.0.0.1" +# my @cpanel = split /\s+/; +# $cpanel[2] = '' if $cpanel[2] =~ /\[/; +# $log_me->execute($cpanel[0], $cpanel[2], 'cpanel'); +# if ( exists $cpanel_faults {$cpanel[0]} ) { +# $cpanel_faults{$cpanel[0]}++; +# } else { +# $cpanel_faults{$cpanel[0]} = 1; +# } +# logger("IP $cpanel[0]($cpanel[2])failed to identify to cPanel.") if ($debug); + } else { + next; + } + check_broot(); + my $passed_time = time() - $start_time; # get the pssed time + if ($passed_time > $broot_time) { # if the passed time is grater then $broot_time + cleanh(); # clean the hashes + $start_time = time(); # set the start_time to now + } +} +close LOGS; +close HAWK; +close STDIN; +close STDOUT; +close STDERR if (!$debug); +exit 0; diff --git a/machines b/machines new file mode 100644 index 0000000..6d39d9c --- /dev/null +++ b/machines @@ -0,0 +1,11 @@ +s122 +s123 +s124 +s125 +s126 +s127 +s128 +s129 +s130 +s131 +s132 diff --git a/main-master.tmpl b/main-master.tmpl new file mode 100644 index 0000000..69ab4b0 --- /dev/null +++ b/main-master.tmpl @@ -0,0 +1,104 @@ + + +Hawk Web Interface ver. __VER__ + + +

Hawk Web Interface ver. __VER__

+
+Menu : +See hawk statistics | + Update hawk statistics +
diff --git a/main.tmpl b/main.tmpl new file mode 100644 index 0000000..86ab805 --- /dev/null +++ b/main.tmpl @@ -0,0 +1,102 @@ + + +Hawk Web Interface ver. __VER__ + + +

Hawk Web Interface ver. __VER__

+
+Back to master interface
+Menu : +list broots | + list failed | +summary reports | +search | +blacklist +
diff --git a/menu.tmpl b/menu.tmpl new file mode 100644 index 0000000..36d6f76 --- /dev/null +++ b/menu.tmpl @@ -0,0 +1,4 @@ + +summery reports +list broots + list failed \ No newline at end of file diff --git a/mm.stat b/mm.stat new file mode 100644 index 0000000..3597316 --- /dev/null +++ b/mm.stat @@ -0,0 +1,26 @@ +pidfile='/var/run/hawk.pid' +check_status() { + check=0; + for i in `seq 1 5`; do + if [ -f $pidfile ]; then + if [ "$1" == 0 ]; then + if [ ! -d /proc/$pidfile ]; then + check=1; + continue 4; + fi + else + if [ -d /proc/$pidfile ]; then + check=1; + continue 4; + fi + fi + fi + sleep 1 + done + if [ $check == "1" ]; then + echo -e "OK" + else + echo -e "FAILED" + fi +} + diff --git a/search.tmpl b/search.tmpl new file mode 100644 index 0000000..fdc5ad6 --- /dev/null +++ b/search.tmpl @@ -0,0 +1,49 @@ +Search in the faild log:
+ + +
+ +
+ + +
+ + diff --git a/stats-list.tmpl b/stats-list.tmpl new file mode 100644 index 0000000..a01311d --- /dev/null +++ b/stats-list.tmpl @@ -0,0 +1,52 @@ +Statistics from all servers:
+ + + + + + + + + + + + + + + + + + + + + + +
Last 1 hourLast 1 dayLast 7 days
failedbrutesfailedbrutesfailedbrutes
__FAILED0____BRUTES0____FAILED1____BRUTES1____FAILED2____BRUTES2__
+
+Statistics per server: + + + + + + + + + + + + + + + + + + + + +__CONTENTS__ +
DateServerLast 1 hourBlocked activeBlocked removed
 failedbrutesLast hourLast dayLast hourLast day
\ No newline at end of file diff --git a/summary.tmpl b/summary.tmpl new file mode 100644 index 0000000..fced361 --- /dev/null +++ b/summary.tmpl @@ -0,0 +1,86 @@ + +Summary by date(for the last 7 days only):
+ + +
+ All failed attempts: graph
+ + + + + + + __TABLE0__ +
DateCount
+
+ All bruteforce attempts:graph
+ + + + + + + __TABLE1__ +
DateCount
+
+ All blacklisted IPs:
+ + + + + + __TABLE6__ +
DateCount
+
+ All blacklisted AND REMOVED IPs:
+ + + + + + __TABLE7__ +
DateCount
+
+Summary by service(for the last 1 hour):
+ + + + + + + +__TABLE2__ +
SSHFTPPOP3IMAPcPanel
+Summary IPs that have been detected to bruteforce services on the server(for the last 1 hour): + + + + +__TABLE3__ +
IP Address
+Summary IPs that have been detected to bruteforce services on the server(for the last 1 day): + + + + +__TABLE4__ +
IP Address
+Summary IPs that have been detected to bruteforce services on the server(for the last 7 days): + + + + +__TABLE5__ +
IP Address
+ diff --git a/templates.pl b/templates.pl new file mode 100644 index 0000000..c4d6519 --- /dev/null +++ b/templates.pl @@ -0,0 +1,44 @@ +

Hawk Web Interface ver. 0.1

+
+Menu: list broots | summery reports
+Menu: list failed | summery reports
+Menu: list broots | list failed
+
+ + +Failed attempts by hour(last 24 hours only):
+ + + + + + + + +
DateIP AddressServiceUser
01-01-2000 00:00IPSERVICEUSER
+ + + +Brootforce attempts by hour(last 56 hours only):
+ + + + + + + +
DateIP AddressService
01-01-2000 00:00IPSERVICE
+ + + +Summery by date(for the last 7 days only):
+ + + +
DateAll failed attemptsAll brootforce attempts
01-01-200001
+Summery by service:
+ + + +
SSHFTPPOP3IMAPcPanel
01111
+