# HG changeset patch # User Matti Hamalainen # Date 1250554890 -10800 # Node ID 42889eed0ce8943d8d1c3595e519746d3b58c343 # Parent d2e2b82dd2f2ea90b5ddca151151d7b832e9c865 Lots of cleanups, etc. Documentation updates. diff -r d2e2b82dd2f2 -r 42889eed0ce8 README --- a/README Tue Aug 18 00:43:10 2009 +0300 +++ b/README Tue Aug 18 03:21:30 2009 +0300 @@ -1,4 +1,4 @@ -Malicious Attack Livid Termination Filter daemon (maltfilter) v0.15.0 +Malicious Attack Livid Termination Filter daemon (maltfilter) v0.16.0 ===================================================================== Programmed by Matti 'ccr' Hämäläinen (C) Copyright 2009 Tecnic Software productions (TNSP) @@ -11,11 +11,14 @@ Maltfilter daemon script continuously scans various system logfiles including auth.log, httpd logs, etc. for signs of malicious connections, break-in and exploitation attempts. The originating IP addresses of -these connections are then blocked via Netfilter (iptables). +these connections can be then acted upon in following ways, each +being optional: + + * Insertion (and eventual deletion or "weeding") of Netfilter rules. + * Submitting entry to DroneBL DNSBL service. Additionally Maltfilter can generate status reports (either continuously in daemon mode, or as once-run report), in plaintext and HTML formats -and submit data to DroneBL DNSBL service. Since v0.14, there is also option for gathering "evidence" about certain PHP XSS exploit attempts into specified directory. These evidence files @@ -65,8 +68,8 @@ Configuration and usage ======================= -See example.conf for documentation about settings. -Start maltfilter either via the init script or through commandline: +See example.conf for documentation about settings. Start maltfilter +either via the init script or through commandline: $ maltfilter /var/run/maltfilter.pid /etc/maltfilter.conf diff -r d2e2b82dd2f2 -r 42889eed0ce8 example.conf --- a/example.conf Tue Aug 18 00:43:10 2009 +0300 +++ b/example.conf Tue Aug 18 03:21:30 2009 +0300 @@ -1,28 +1,41 @@ ############################################################################# ### Maltfilter configuration file. ### PLEASE READ THROUGH THIS FILE VERY CAREFULLY! +############################################################################# ############################################################################# ### General settings ############################################################################# -# Verbosity level (0 = quiet, bigger values add noise. valid range 0 - 4) -VERBOSITY = 4 +## Verbosity level (0 = quiet, bigger values add noise. valid range 0 - 4) +VERBOSITY = 3 -# Dry-run: 1 = disables daemonization/forking to background, disables -# modification of netfilter/iptables, printing the iptables commands to -# stdout instead. -# NOTICE! IF YOU DON'T CHANGE THIS TO 0, MALTFILTER WILL NOT DAEMONIZE! +## Dry-run: 1 = disables daemonization/forking to background, disables +## modification of netfilter via iptables, printing the iptables commands +## to stdout instead and DroneBL submissions will be disabled. +## NOTICE! IF YOU DON'T CHANGE THIS TO 0, MALTFILTER WILL NOT DAEMONIZE! DRY_RUN = 1 -# Full path to iptables binary -IPTABLES = "/sbin/iptables" - -# Maltfilter logfile path and name (set empty "" if you don't want logging) +## Maltfilter logfile path and name (set empty "" if you don't want logging) LOGFILE = "/var/log/maltfilter" -# System passwd file location (default is /etc/passwd), this file -# is checked to figure out system account names. See also SYSACCT_ -# settings below. +## IP addresses that should NOT be blocked under any circumstances. You should +## set this if you wish to have a surefire open channel from some host, even in +## the case someone tries to spoof IPs for denial of service. +## +## NOTICE! This setting supports only IPv4 addresses, no IPv6 or DNS names. +## You can have any number of NOACTION_IPS settings. +#NOACTION_IPS = "192.121.86.15" +#NOACTION_IPS = "74.125.45.100" + +## For how many hours to keep general information about IP. Affects from +## how long period statistics dump shows data. Also hitcount thresholds +## take the old data into account, meaning that if FILTER_MAX_AGE < GLOBAL_MAX_AGE +## hit data older than FILTER_MAX_AGE will be counted towards THRESHOLD. +#GLOBAL_MAX_AGE = 336 + +## System passwd file location (default is /etc/passwd), this file +## is checked to figure out system account names. See also SYSACCT_* +## settings below. #PASSWD = "/etc/passwd" ## Set range of system account UIDs here, default is 1-100. @@ -32,35 +45,26 @@ ############################################################################# -### Actions, etc. settings +### Netfilter actions ############################################################################# +## 0 = Netfilter handling disabled +FILTER = 0 + +## Full path to iptables binary +IPTABLES = "/sbin/iptables" + +## How many "hits" the IP needs until it is eligible to be filtered. +## (the "hits" can be from any check, e.g. sshd crack, httpd, etc.) +FILTER_THRESHOLD = 3 + ## Weeding threshold in hours. Entries older than this will be removed ## off from current netfilter settings. Also, entries older than this ## will not be added to netfilter to begin with. -#WEED_FILTER = 168 - -## For how many hours to keep general information about IP. Affects from -## how long period statistics dump shows data. Also hitcount thresholds -## take the old data into account, meaning that if WEED_FILTER < WEED_GLOBAL -## hit data older than WEED_FILTER will be counted towards THRESHOLD. -#WEED_GLOBAL = 336 - -## How many "hits" the IP needs until it is eligible to be blocked. -## (the "hits" can be from any "source", e.g. sshd crack, httpd, etc.) -#THRESHOLD = 3 +FILTER_MAX_AGE = 168 -## Target iptables action for added entries, default is DROP, but you +## Target iptables target for added entries, default is DROP, but you ## can use whatever rule chain name you want to here. -#ACTION = "DROP" - -## IP addresses that should NOT be blocked under any circumstances. You should -## set this if you wish to have a surefire open channel from some host, even in -## the case someone tries to spoof IPs for denial of service. -## -## NOTICE! This setting supports only IPv4 addresses, no IPv6 or DNS names. -## You can have any number of NOBLOCK_IPS settings. -#NOBLOCK_IPS = "192.121.86.15" -#NOBLOCK_IPS = "74.125.45.100" +FILTER_TARGET = "DROP" ############################################################################# @@ -97,7 +101,7 @@ ## ## NOTICE! Do not enable this setting, if you allow SSH root logins via ## password authentication! Mistyping password may get you blocked unless -## your host IP is defined in NOBLOCK_IPS. If you wish to enable this +## your host IP is defined in NOACTION_IPS. If you wish to enable this ## check, you should set "PermitRootLogin" to "without-password" or "no" ## in your sshd_config. CHK_ROOT_SSH_PWD = 0 @@ -106,12 +110,11 @@ ## Catches failed password logins for system accounts. ## ## NOTICE! If you enable this setting, make sure have defined safe -## host IPs in NOBLOCK_IPS, and that your system DOES NOT have passwords +## host IPs in NOACTION_IPS, and that your system DOES NOT have passwords ## for system accounts .. which would be stupid anyway. CHK_SYSACCT_SSH_PWD = 0 - # (2) Common/known vulnerable CGI/PHP software scans (like phpMyAdmin) # NOTICE! This matches ERRORLOG, thus it only works if you DO NOT have # any or some of these installed. Preferably none, or use uncommon @@ -145,11 +148,11 @@ ## every few minutes.) Leave empty ("") or commented if you do not want ## status reports. -## Plain ASCII text file rerpot +## Plain ASCII text file report #STATUS_FILE_PLAIN = "/var/www/maltstatus.txt" ## HTML file and optional CSS stylesheet URL for the HTML -## (if left empty, CSS is not used.) +## (if left empty/unset, CSS will not be linked from the HTML file.) #STATUS_FILE_HTML = "/var/www/maltstatus.html" #STATUS_FILE_CSS = "cool.css" @@ -169,12 +172,13 @@ ############################################################################# ## By enabling EVIDENCE=1 and setting EVIDENCE_DIR to existing directory ## writable by the effective UID which Maltfilter runs as, it will be -## populated by *.data and *.hosts files. If succesfully retrieved, .data -## files will have contents of the attempted XSS URI. *.hosts files -## list which hosts have attempted to exploit this specific URI. +## populated by *.info, *.data and *.hosts files. If succesfully retrieved, +## .data files will have contents of the attempted XSS URI. *.hosts files +## list which hosts have attempted to exploit this specific URI. *.info +## contain generic information and HTTP headers. -#EVIDENCE = 0 -#EVIDENCE_DIR = "/var/run/malt-evidence" +EVIDENCE = 0 +EVIDENCE_DIR = "/var/run/malt-evidence" ############################################################################# @@ -192,8 +196,8 @@ DRONEBL_THRESHOLD = 5 ## Maximum age of hits counted towards DroneBL submission threshold. -## There is currently no weeding of submissions. -DRONEBL_MAX_AGE = 30 +## NOTICE! Value this is in minutes! +DRONEBL_MAX_AGE = 60 ## Your personal RPC key. This _MUST_ be set to a valid value, if you ## have enabled submissions. To get a personal key, go to: diff -r d2e2b82dd2f2 -r 42889eed0ce8 maltfilter --- a/maltfilter Tue Aug 18 00:43:10 2009 +0300 +++ b/maltfilter Tue Aug 18 03:21:30 2009 +0300 @@ -12,29 +12,32 @@ use Net::DNS; use LWP::UserAgent; -my $progversion = "0.15.0"; +my $progversion = "0.16.0"; my $progbanner = "Malicious Attack Livid Termination Filter daemon (maltfilter) v$progversion\n". "Programmed by Matti 'ccr' Hamalainen \n". "(C) Copyright 2009 Tecnic Software productions (TNSP)\n"; + ############################################################################# ### Default settings and configuration ############################################################################# my %settings = ( - "VERBOSITY" => 3, - "DRY_RUN" => 1, - "WEED_FILTER" => 168, # in hours - "WEED_GLOBAL" => 336, # in hours - "THRESHOLD" => 3, - "ACTION" => "DROP", - "LOGFILE" => "", - "IPTABLES" => "/sbin/iptables", + "VERBOSITY" => 3, + "DRY_RUN" => 1, + "LOGFILE" => "", + "STATS_MAX_AGE" => 336, # in hours "PASSWD" => "/etc/passwd", "SYSACCT_MIN_UID" => 1, "SYSACCT_MAX_UID" => 100, + "FILTER" => 0, + "FILTER_THRESHOLD" => 3, + "FILTER_MAX_AGE" => 168, # in hours + "FILTER_TARGET" => "DROP", + "IPTABLES" => "/sbin/iptables", + "FULL_TIME" => 1, "STATUS_FILE_PLAIN" => "", "STATUS_FILE_HTML" => "", @@ -59,7 +62,7 @@ "DRONEBL_RPC_KEY" => "", ); -my @noblock_ips_def = ( +my @noaction_ips_def = ( "127.0.0.1", ); @@ -79,20 +82,21 @@ # (1.1) Generic login scan attempts if ($merr =~ /^Failed password for invalid user (\S+) from (\d+\.\d+\.\d+\.\d+)/) { - check_add_hit($2, $mdate, "SSH login scan", "", $settings{"CHK_SSHD"}); + check_add_hit($2, $mdate, "SSH login scan", "", 13, $settings{"CHK_SSHD"}); } # (1.2) Root account SSH login password bruteforcing attempts. elsif (/^Failed password for root from (\d+\.\d+\.\d+\.\d+)/) { - check_add_hit($1, $mdate, "Root SSH password bruteforce", "", $settings{"CHK_ROOT_SSH_PWD"}); + check_add_hit($1, $mdate, "Root SSH password bruteforce", "", 13, $settings{"CHK_ROOT_SSH_PWD"}); } # (1.3) System account SSH login password bruteforcing attempts. if ($merr =~ /^Failed password for (\S+) from (\d+\.\d+\.\d+\.\d+)/) { my $mip = $2; my $macct = $1; if (defined($systemacct{$macct})) { - check_add_hit($mip, $mdate, "SSH system account bruteforce", $macct, $settings{"CHK_SYSACCT_SSH_PWD"}); + check_add_hit($mip, $mdate, "SSH system account bruteforce", $macct, 13, $settings{"CHK_SYSACCT_SSH_PWD"}); } } } + # (2) Common/known vulnerable CGI/PHP software scans (like phpMyAdmin) elsif (/^\[(.+?)\]\s+\[error\]\s+\[client\s+(\d+\.\d+\.\d+\.\d+)\]\s+(.+)$/) { my $mdate = $1; @@ -101,10 +105,11 @@ if ($merr =~ /^File does not exist: (.+)$/) { my $tmp = $1; if ($tmp =~ /\/mss2|\/pma|admin|sql|\/roundcube|\/webmail|\/bin|\/mail|xampp|zen|mailto:|appserv|cube|round|_vti_bin|wiki/i) { - check_add_hit($mip, $mdate, "CGI vuln scan", $tmp, $settings{"CHK_KNOWN_CGI"}); + check_add_hit($mip, $mdate, "CGI vuln scan", $tmp, 2, $settings{"CHK_KNOWN_CGI"}); } } } + # (3) Apache common logging format checks elsif (/(\d+\.\d+\.\d+\.\d+)\s+-\s+-\s+\[(.+?)\]\s+\"GET (\S*?) HTTP\//) { my $mdate = $2; @@ -115,15 +120,15 @@ if ($merr =~ /\.php\?\S*?=http:\/\/([^\/]+)/) { if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) { if ($merr =~ /\.php\?\S*?=(http:\/\/[^\&\?]+\??)/) { - check_add_evidence($mip, $1, $merr); + evidence_queue($mip, $1, $merr); } - check_add_hit($mip, $mdate, "PHP XSS", $merr, $settings{"CHK_PHP_XSS"}); + check_add_hit($mip, $mdate, "PHP XSS", $merr, 2, $settings{"CHK_PHP_XSS"}); } } # (3.2) Try to match proxy scanning attempts elsif ($merr =~ /^http:\/\/([^\/]+)/) { if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) { - check_add_hit($mip, $mdate, "Proxy scan", $merr, $settings{"CHK_PROXY_SCAN"}); + check_add_hit($mip, $mdate, "Proxy scan", $merr, 2, $settings{"CHK_PROXY_SCAN"}); } } } @@ -136,15 +141,15 @@ my $reportmode = 0; # Full report mode my @scanfiles = (); # Files to scan my @scanfiles_once = (); # Files to scan only once during startup or HUP (e.g. not continuously followed) -my @noblock_ips = (); # IPs not to block +my @noaction_ips = (); # IPs not to block my %filehandles = (); # Global hash holding opened scanned log filehandles my $pid_file = ""; # Name of Maltfilter daemon pid file my @configfiles = (); # Array of configuration file names my $LOGFILE; # Maltfilter logfile handle my %dronebl = (); -# IPs currently blocked in Netfilter $blocklist{$ip} = date -my %blocklist = (); +# IPs currently blocked in Netfilter $filterlist{$ip} = date +my %filterlist = (); # Gathered information about hosts # $statlist{$ip}-> @@ -308,7 +313,7 @@ ); foreach my $mip (sort { $func->($table, $a, $b) } keys %{$keys}) { - my $blocked = defined($blocklist{$mip}) ? "blocked" : "unblocked"; + my $blocked = defined($filterlist{$mip}) ? "blocked" : "unblocked"; printElem($m, $f, " "); printTD($m, $f, sprintf(bb($m)."%-10d".eb($m), $table->{$mip}{"hits"})); printElem(!$m, $f, " | "); @@ -383,7 +388,7 @@ my @ncolor = (0, 0); my $printEntry = sub { - my $blocked = "class=\"".(defined($blocklist{$_[0]}) ? "blocked" : "unblocked")."\""; + my $blocked = "class=\"".(defined($filterlist{$_[0]}) ? "blocked" : "unblocked")."\""; if (test_ips($previp[$_[1]], $_[0]) < 3) { $ncolor[$_[1]]++; } @@ -477,7 +482,7 @@ "); printH($m, $f, 1, "Maltfilter v$progversion status report"); - my $period = get_period($settings{"WEED_GLOBAL"}); + my $period = get_period($settings{"STATS_MAX_AGE"}); printP($m, $f, "Generated ".bb($m).get_time_str(time()).eb($m).". Data computed from ". @@ -487,10 +492,10 @@ "blocked IP that was in Netfilter before Maltfilter was started.\n"); printH($m, $f, 2, "Currently filtered entries"); - $period = get_period($settings{"WEED_FILTER"}); + $period = get_period($settings{"FILTER_MAX_AGE"}); printP($m, $f, "List of IPs that are currently filtered (or would be, if this is\n". "a report-only mode). Data from period of $period.\n"); - print_table1($m, $f, \%statlist, \%blocklist, \&cmp_hits, "blocked"); + print_table1($m, $f, \%statlist, \%filterlist, \&cmp_hits, "blocked"); printH($m, $f, 2, "Summary of non-ignored entries"); printP($m, $f, "List of 'hits' of suspicious activity noticed by Maltfilter, but not\n". @@ -512,25 +517,26 @@ ############################################################################# sub dronebl_process { -# return if ($reportmode); return unless ($settings{"DRONEBL"} > 0); + return if ($settings{"DRY_RUN"}); # Create submission data my $xml = "\n\n"; my $entries = 0; while (my ($ip, $entry) = each(%dronebl)) { - if ($entry->{"sent"} == 0) { - $xml .= "{"type"}."\" />\n"; + if ($entry->{"sent"} == 0 && $entry->{"tries"} < 3) { + $xml .= "\n"; +# $xml .= "{"type"}."\" />\n"; $entries++; } } $xml .= "\n"; - mlog(1, "Trying to submit $entries entries to DroneBL.\n"); - print STDERR $xml; - return; - + # Bait out if no entries to submit return unless ($entries > 0); + mlog(1, "[DroneBL] Trying to submit $entries entries.\n"); + +return; # Submit via HTTP XML-RPC my $tmp = LWP::UserAgent->new; @@ -543,30 +549,46 @@ my $res = $tmp->request($req); if ($res->is_success) { - while (my ($ip, $entry) = each(%dronebl)) { - $entry->{"sent"} = 1; - } + mlog(2, "[DroneBL] [".$res->code."] ".$res->message."\n"); + print $res->content."\n"; +# while (my ($ip, $entry) = each(%dronebl)) { +# $entry->{"sent"} = 1; +# } } else { - mlog(-1, "DroneBL submission failed: [".$res->code."] ".$res->message."\n"); + mlog(-1, "[DroneBL] Submission failed: [".$res->code."] ".$res->message."\n"); } # Remove submitted expired entries while (my ($ip, $entry) = each(%dronebl)) { - print "$ip: ".$entry->{"sent"}."\n" unless check_time3($entry->{"date"}); + if (!check_time3($entry->{"date"})) { + mlog(1, "[DroneBL] $ip submission expired.\n") unless ($entry->{"sent"} > 0); + delete($dronebl{$ip}); + } } } +sub dronebl_queue($$$) +{ + my ($mip, $mdate, $mtype) = @_; + if (!defined($dronebl{$mip})) { + mlog(3, "[DroneBL] Queueing $mip \@ $mdate ($mtype)\n"); + $dronebl{$mip}{"type"} = $mtype; + $dronebl{$mip}{"date"} = $mdate; + $dronebl{$mip}{"sent"} = 0; + $dronebl{$mip}{"tries"} = 0; + } +} ############################################################################# ### Evidence gathering ############################################################################# my %evidence = (); -sub check_add_evidence($$$) +sub evidence_queue($$$) { my ($mip, $mdata, $mfull) = @_; - return unless ($settings{"EVIDENCE"}); + return unless ($settings{"EVIDENCE"} > 0); my $tmp = $mdata; $tmp =~ s/http:\/\///; @@ -578,7 +600,7 @@ $evidence{$mdata}{"full"}{$mfull} = 1; } -sub http_fetch($$) +sub evidence_fetch($$) { my $tmp = LWP::UserAgent->new; $tmp->agent("-"); @@ -588,12 +610,12 @@ return $tmp->request($req); } -sub gather_evidence +sub evidence_gather { my $dns = Net::DNS::Resolver->new; my $base = $settings{"EVIDENCE_DIR"}; - return unless ($settings{"EVIDENCE"}); + return unless ($settings{"EVIDENCE"} > 0); mdie("Evidence directory '$base' has disappeared.\n") unless (-e $base); @@ -607,7 +629,7 @@ if (! -e $filename) { $did_fetch = 1; mlog(1, "Fetching evidence for $url\n"); - my $res = http_fetch($url, ""); + my $res = evidence_fetch($url, ""); open(FILE, ">:raw", $filename) or mdie("Could not open '$filename' for writing.\n"); binmode(FILE, ":raw"); if ($res->is_success && $res->code >= 200 && $res->code <= 201) { @@ -697,6 +719,7 @@ ### Execute iptables sub exec_iptables(@) { + $ENV{"PATH"} = ""; my @args = ($settings{"IPTABLES"}, @_); if ($settings{"DRY_RUN"}) { mlog(3, ":: ".join(" ", @args)."\n"); @@ -706,12 +729,13 @@ } ### Get current Netfilter INPUT table entries that match -### entry types we manage, e.g. blocklist -sub update_blocklist($) +### entry types we manage, e.g. filterlist +sub update_filterlist($) { - # NOTICE: argument not used now + return unless ($settings{"FILTER"} > 0); my $first = $_[0]; - + mlog(0, "Updating initial filterlist from netfilter.\n") unless ($first > 0); + $ENV{"PATH"} = ""; open(STATUS, $settings{"IPTABLES"}." -v -n -L INPUT |") or mdie("Could not execute ".$settings{"IPTABLES"}."\n"); @@ -719,12 +743,12 @@ undef(%newlist); while () { chomp; - if (/^\s*(\d+)\s+\d+\s+$settings{"ACTION"}\s+all\s+--\s+\*\s+\*\s+(\d+\.\d+\.\d+\.\d+)\s+0\.0\.0\.0\/0\s*$/) { + if (/^\s*(\d+)\s+\d+\s+$settings{"FILTER_TARGET"}\s+all\s+--\s+\*\s+\*\s+(\d+\.\d+\.\d+\.\d+)\s+0\.0\.0\.0\/0\s*$/) { my $mip = $2; my $mdate = time(); - if (!defined($blocklist{$mip})) { - mlog(2, "* $mip appeared in iptables.\n") if ($first > 0); - $blocklist{$2} = $mdate; + if (!defined($filterlist{$mip})) { + mlog(2, "* $mip appeared in iptables.\n") unless ($first < 0); + $filterlist{$2} = $mdate; } $newlist{$2} = $mdate; update_entry(\%statlist, $mip, -1, "IPTABLES", "", 0); @@ -732,10 +756,10 @@ } close(STATUS); - foreach my $mip (keys %blocklist) { + foreach my $mip (keys %filterlist) { if (!defined($newlist{$mip})) { mlog(2, "* $mip removed from iptables.\n"); - delete($blocklist{$mip}); + delete($filterlist{$mip}); } } } @@ -744,12 +768,12 @@ ### Returns false if timestamp is over weed period, e.g. needs weeding. sub check_time1($) { - return ($_[0] > time() - ($settings{"WEED_FILTER"} * 60 * 60)); + return ($_[0] > time() - ($settings{"FILTER_MAX_AGE"} * 60 * 60)); } sub check_time2($) { - return ($_[0] > time() - ($settings{"WEED_GLOBAL"} * 60 * 60)); + return ($_[0] > time() - ($settings{"STATS_MAX_AGE"} * 60 * 60)); } sub check_time3($) @@ -760,10 +784,10 @@ ### Weed out old entries sub weed_do($) { - my $mtime = $blocklist{$_[0]}; + my $mtime = $filterlist{$_[0]}; mlog(2, "* Weeding $_[0] (".get_time_str($mtime).")\n"); - exec_iptables("-D", "INPUT", "-s", $_[0], "-d", "0.0.0.0/0", "-j", $settings{"ACTION"}); - delete($blocklist{$_[0]}); + exec_iptables("-D", "INPUT", "-s", $_[0], "-d", "0.0.0.0/0", "-j", $settings{"FILTER_TARGET"}); + delete($filterlist{$_[0]}); delete($statlist{$_[0]}); delete($ignorelist{$_[0]}); } @@ -771,14 +795,14 @@ sub weed_entries() { # Don't weed in report mode. - return if ($reportmode); + return unless ($settings{"FILTER"} > 0 && $reportmode == 0); # Weed blocked entries. - my @mips = keys %blocklist; + my @mips = keys %filterlist; foreach my $mip (@mips) { - if (defined($blocklist{$mip})) { - if ($blocklist{$mip} >= 0) { - weed_do($mip) unless check_time1($blocklist{$mip}); + if (defined($filterlist{$mip})) { + if ($filterlist{$mip} >= 0) { + weed_do($mip) unless check_time1($filterlist{$mip}); } else { weed_do($mip); } @@ -789,7 +813,7 @@ foreach my $mip (keys %statlist) { if (defined($statlist{$mip})) { my $mtime = $statlist{$mip}{"date2"}; - if (!check_time2($mtime) && !defined($blocklist{$mip})) { + if (!check_time2($mtime) && !defined($filterlist{$mip})) { mlog(3, "* Deleting stale $mip (".get_time_str($mtime).")\n"); delete($statlist{$mip}); } @@ -862,8 +886,8 @@ my $mcond = $_[5]; my $cnt; - if (check_hosts_array(\@noblock_ips, $mip)) { - mlog(3, "Hit to NOBLOCK_IPS($mip): [$mclass] $mreason\n"); + if (check_hosts_array(\@noaction_ips, $mip)) { + mlog(2, "Hit to NOACTION_IPS($mip): [$mclass] $mreason\n"); return; } @@ -877,21 +901,19 @@ } # Check if we have exceeded threshold etc. - if ($cnt >= $settings{"THRESHOLD"} && check_time1($mdate)) { - # Add to blocklist, unless already there. - if (!defined($blocklist{$mip})) { - mlog(1, "* Adding $mip ($mdate): [$mclass] $mreason\n"); - exec_iptables("-I", "INPUT", "1", "-s", $mip, "-j", $settings{"ACTION"}); + if ($settings{"FILTER"} > 0 && $cnt >= $settings{"FILTER_THRESHOLD"} && check_time1($mdate)) { + # Add to filterlist, unless already there. + if (!defined($filterlist{$mip})) { + mlog(1, "* Adding $mip \@ ".get_time_str($mdate).": [$mclass] $mreason\n"); + exec_iptables("-I", "INPUT", "1", "-s", $mip, "-j", $settings{"FILTER_TARGET"}); } # Update date of last hit - $blocklist{$mip} = $mdate; + $filterlist{$mip} = $mdate; } # Separate check for DroneBL if ($settings{"DRONEBL"} > 0 && $mtype > 0 && $cnt >= $settings{"DRONEBL_THRESHOLD"} && check_time3($mdate)) { - $dronebl{$mip}{"type"} = $mtype; - $dronebl{$mip}{"date"} = $mdate; - $dronebl{$mip}{"sent"} = 0 unless defined($dronebl{$mip}{"sent"}); + dronebl_queue($mip, $mdate, $mtype); } } @@ -925,8 +947,7 @@ undef(%statlist); %ignorelist = (); undef(%ignorelist); - mlog(0, "Updating initial blocklist from netfilter.\n"); - update_blocklist(-1); + update_filterlist(-1); foreach my $filename (@scanfiles_once) { mlog(0, "Parsing [ONCE] ".$filename." ...\n"); @@ -1001,6 +1022,16 @@ mlog(-1, "Reinitialization finished, resuming scanning.\n"); } +sub malt_maintenance +{ + update_filterlist(time()); + weed_entries(); + generate_status($settings{"STATUS_FILE_PLAIN"}, 0); + generate_status($settings{"STATUS_FILE_HTML"}, 1); + evidence_gather(); + dronebl_process(); +} + ### Main scanning function sub malt_scan { @@ -1017,15 +1048,9 @@ } } if ($counter < 0 || $counter++ >= 30) { - # Every once in a while, update known IP list from iptables - # (in case entries have appeared there from "outside") - # and perform weeding of old entries. + # Every once in a while, execute maintenance functions $counter = 0; - update_blocklist(time()); - weed_entries(); - generate_status($settings{"STATUS_FILE_PLAIN"}, 0); - generate_status($settings{"STATUS_FILE_HTML"}, 1); - gather_evidence(); + malt_maintenance(); } sleep(2); foreach my $filename (keys %filehandles) { @@ -1063,8 +1088,8 @@ push(@scanfiles, $value); } elsif ($key eq "SCANFILE_ONCE") { push(@scanfiles_once, $value); - } elsif ($key eq "NOBLOCK_IPS") { - push(@noblock_ips_def, $value); + } elsif ($key eq "NOACTION_IPS") { + push(@noaction_ips_def, $value); } elsif (defined($settings{$key})) { $settings{$key} = $value; } else { @@ -1107,10 +1132,10 @@ @scanfiles_once = grep(!$saw{$_}++, @scanfiles_once); %saw = (); - @noblock_ips = grep(!$saw{$_}++, @noblock_ips_def); + @noaction_ips = grep(!$saw{$_}++, @noaction_ips_def); undef(%saw); - mlog(-1, "Not blocking following IPs: ".join(", ", @noblock_ips)."\n"); + mlog(-1, "Not acting on IPs: ".join(", ", @noaction_ips)."\n"); # Check if we have anything to do if ($reportmode) { @@ -1119,13 +1144,26 @@ mdie("Nothing to do, no SCANFILE(s) defined in configuration.\n") unless ($#scanfiles > 0); } - # Test existence of iptables - if (! -e $settings{"IPTABLES"} || ! -x $settings{"IPTABLES"}) { - mdie("iptables binary does not exist or is not executable: ".$settings{"IPTABLES"}."\n"); + # General settings + my $val = $settings{"STATS_MAX_AGE"}; + mdie("Invalid STATS_MAX_AGE value $val, must be > 0.\n") unless ($val > 0); + + # Filtering + if ($settings{"FILTER"} > 0) { + $val = $settings{"FILTER_MAX_AGE"}; + mdie("Invalid FILTER_MAX_AGE value $val, must be > 0.\n") unless ($val > 0); + + $val = $settings{"FILTER_THRESHOLD"}; + mdie("Invalid FILTER_THRESHOLD value $val, must be >= 0.\n") unless ($val >= 0); + + $val = $settings{"IPTABLES"}; + mdie("iptables binary does not exist or is not executable: $val\n") unless (-e $val && -x $val); + } else { + mlog(1, "Netfilter handling disabled.\n"); } # Check evidence settings - if ($settings{"EVIDENCE"}) { + if ($settings{"EVIDENCE"} > 0) { my $base = $settings{"EVIDENCE_DIR"}; mdie("Evidence directory (EVIDENCE_DIR) not set in configuration.\n") if ($base eq ""); mdie("Evidence directory '$base' does not exist.\n") unless (-e $base); @@ -1133,7 +1171,12 @@ mdie("Evidence directory '$base' is not writable by euid.\n") unless (-w $base); } - # Check settings + # Sanitize DroneBL configuration + if ($settings{"DRONEBL"} > 0) { + mdie("DroneBL RPC key not set.\n") unless ($settings{"DRONEBL_RPC_KEY"} ne ""); + } + + # Check system account / passwd settings mdie("SYSACCT_MIN_UID must be >= 1.\n") unless ($settings{"SYSACCT_MIN_UID"} >= 1); mdie("SYSACCT_MAX_UID must be >= SYSACCT_MIN_UID.\n") unless ($settings{"SYSACCT_MAX_UID"} >= $settings{"SYSACCT_MIN_UID"}); @@ -1160,7 +1203,7 @@ # Print banner and help if no arguments my $argc = $#ARGV + 1; if ($argc < 1) { - print $progbanner. + print STDERR $progbanner. "\n". "Usage: maltfilter [config filename] [config filename...]\n". " maltfilter -f [config filename] [config filename...]\n". @@ -1172,6 +1215,7 @@ $pid_file = shift; if ($pid_file eq "-f") { $reportmode = 1; + print STDERR $progbanner; } else { mdie("'$pid_file' already exists, not starting.\n". "If the daemon is NOT running, remove the pid-file and re-start.\n") @@ -1187,11 +1231,10 @@ # Open logfile if ($settings{"DRY_RUN"}) { - print $progbanner. - "*********************************************\n". - "* NOTICE! DRY-RUN MODE ENABLED! No changes *\n". - "* will actually get committed to netfilter! *\n". - "*********************************************\n"; + print STDERR + "*********************************\n". + "* NOTICE! DRY-RUN MODE ENABLED! *\n". + "*********************************\n"; } elsif ($settings{"LOGFILE"} ne "") { open($LOGFILE, ">>", $settings{"LOGFILE"}) or die("Could not open logfile '".$settings{"LOGFILE"}."' for writing!\n"); select((select($LOGFILE), $| = 1)[0]); @@ -1204,10 +1247,7 @@ # Fork to background, unless dry-running if ($settings{"DRY_RUN"}) { if ($reportmode) { - mlog(-1, "Outputting report files.\n"); - generate_status($settings{"STATUS_FILE_PLAIN"}, 0); - generate_status($settings{"STATUS_FILE_HTML"}, 1); - gather_evidence(); + malt_maintenance(); malt_cleanup(); } else { malt_scan();