comparison maltfilter @ 66:42889eed0ce8

Lots of cleanups, etc. Documentation updates.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 18 Aug 2009 03:21:30 +0300
parents d2e2b82dd2f2
children 8df5d52436a1
comparison
equal deleted inserted replaced
65:d2e2b82dd2f2 66:42889eed0ce8
10 use Date::Parse; 10 use Date::Parse;
11 use Net::IP; 11 use Net::IP;
12 use Net::DNS; 12 use Net::DNS;
13 use LWP::UserAgent; 13 use LWP::UserAgent;
14 14
15 my $progversion = "0.15.0"; 15 my $progversion = "0.16.0";
16 my $progbanner = 16 my $progbanner =
17 "Malicious Attack Livid Termination Filter daemon (maltfilter) v$progversion\n". 17 "Malicious Attack Livid Termination Filter daemon (maltfilter) v$progversion\n".
18 "Programmed by Matti 'ccr' Hamalainen <ccr\@tnsp.org>\n". 18 "Programmed by Matti 'ccr' Hamalainen <ccr\@tnsp.org>\n".
19 "(C) Copyright 2009 Tecnic Software productions (TNSP)\n"; 19 "(C) Copyright 2009 Tecnic Software productions (TNSP)\n";
20 20
21
21 ############################################################################# 22 #############################################################################
22 ### Default settings and configuration 23 ### Default settings and configuration
23 ############################################################################# 24 #############################################################################
24 my %settings = ( 25 my %settings = (
25 "VERBOSITY" => 3, 26 "VERBOSITY" => 3,
26 "DRY_RUN" => 1, 27 "DRY_RUN" => 1,
27 "WEED_FILTER" => 168, # in hours 28 "LOGFILE" => "",
28 "WEED_GLOBAL" => 336, # in hours 29 "STATS_MAX_AGE" => 336, # in hours
29 "THRESHOLD" => 3,
30 "ACTION" => "DROP",
31 "LOGFILE" => "",
32 "IPTABLES" => "/sbin/iptables",
33 30
34 "PASSWD" => "/etc/passwd", 31 "PASSWD" => "/etc/passwd",
35 "SYSACCT_MIN_UID" => 1, 32 "SYSACCT_MIN_UID" => 1,
36 "SYSACCT_MAX_UID" => 100, 33 "SYSACCT_MAX_UID" => 100,
34
35 "FILTER" => 0,
36 "FILTER_THRESHOLD" => 3,
37 "FILTER_MAX_AGE" => 168, # in hours
38 "FILTER_TARGET" => "DROP",
39 "IPTABLES" => "/sbin/iptables",
37 40
38 "FULL_TIME" => 1, 41 "FULL_TIME" => 1,
39 "STATUS_FILE_PLAIN" => "", 42 "STATUS_FILE_PLAIN" => "",
40 "STATUS_FILE_HTML" => "", 43 "STATUS_FILE_HTML" => "",
41 "STATUS_FILE_CSS" => "", 44 "STATUS_FILE_CSS" => "",
57 "DRONEBL_MAX_AGE" => 30, # in minutes 60 "DRONEBL_MAX_AGE" => 30, # in minutes
58 "DRONEBL_RPC_URI" => "http://dronebl.org/RPC2", 61 "DRONEBL_RPC_URI" => "http://dronebl.org/RPC2",
59 "DRONEBL_RPC_KEY" => "", 62 "DRONEBL_RPC_KEY" => "",
60 ); 63 );
61 64
62 my @noblock_ips_def = ( 65 my @noaction_ips_def = (
63 "127.0.0.1", 66 "127.0.0.1",
64 ); 67 );
65 68
66 my %systemacct = (); 69 my %systemacct = ();
67 sub check_add_hit($$$$$$); 70 sub check_add_hit($$$$$$);
77 my $mdate = $1; 80 my $mdate = $1;
78 my $merr = $2; 81 my $merr = $2;
79 82
80 # (1.1) Generic login scan attempts 83 # (1.1) Generic login scan attempts
81 if ($merr =~ /^Failed password for invalid user (\S+) from (\d+\.\d+\.\d+\.\d+)/) { 84 if ($merr =~ /^Failed password for invalid user (\S+) from (\d+\.\d+\.\d+\.\d+)/) {
82 check_add_hit($2, $mdate, "SSH login scan", "", $settings{"CHK_SSHD"}); 85 check_add_hit($2, $mdate, "SSH login scan", "", 13, $settings{"CHK_SSHD"});
83 } 86 }
84 # (1.2) Root account SSH login password bruteforcing attempts. 87 # (1.2) Root account SSH login password bruteforcing attempts.
85 elsif (/^Failed password for root from (\d+\.\d+\.\d+\.\d+)/) { 88 elsif (/^Failed password for root from (\d+\.\d+\.\d+\.\d+)/) {
86 check_add_hit($1, $mdate, "Root SSH password bruteforce", "", $settings{"CHK_ROOT_SSH_PWD"}); 89 check_add_hit($1, $mdate, "Root SSH password bruteforce", "", 13, $settings{"CHK_ROOT_SSH_PWD"});
87 } 90 }
88 # (1.3) System account SSH login password bruteforcing attempts. 91 # (1.3) System account SSH login password bruteforcing attempts.
89 if ($merr =~ /^Failed password for (\S+) from (\d+\.\d+\.\d+\.\d+)/) { 92 if ($merr =~ /^Failed password for (\S+) from (\d+\.\d+\.\d+\.\d+)/) {
90 my $mip = $2; my $macct = $1; 93 my $mip = $2; my $macct = $1;
91 if (defined($systemacct{$macct})) { 94 if (defined($systemacct{$macct})) {
92 check_add_hit($mip, $mdate, "SSH system account bruteforce", $macct, $settings{"CHK_SYSACCT_SSH_PWD"}); 95 check_add_hit($mip, $mdate, "SSH system account bruteforce", $macct, 13, $settings{"CHK_SYSACCT_SSH_PWD"});
93 } 96 }
94 } 97 }
95 } 98 }
99
96 # (2) Common/known vulnerable CGI/PHP software scans (like phpMyAdmin) 100 # (2) Common/known vulnerable CGI/PHP software scans (like phpMyAdmin)
97 elsif (/^\[(.+?)\]\s+\[error\]\s+\[client\s+(\d+\.\d+\.\d+\.\d+)\]\s+(.+)$/) { 101 elsif (/^\[(.+?)\]\s+\[error\]\s+\[client\s+(\d+\.\d+\.\d+\.\d+)\]\s+(.+)$/) {
98 my $mdate = $1; 102 my $mdate = $1;
99 my $mip = $2; 103 my $mip = $2;
100 my $merr = $3; 104 my $merr = $3;
101 if ($merr =~ /^File does not exist: (.+)$/) { 105 if ($merr =~ /^File does not exist: (.+)$/) {
102 my $tmp = $1; 106 my $tmp = $1;
103 if ($tmp =~ /\/mss2|\/pma|admin|sql|\/roundcube|\/webmail|\/bin|\/mail|xampp|zen|mailto:|appserv|cube|round|_vti_bin|wiki/i) { 107 if ($tmp =~ /\/mss2|\/pma|admin|sql|\/roundcube|\/webmail|\/bin|\/mail|xampp|zen|mailto:|appserv|cube|round|_vti_bin|wiki/i) {
104 check_add_hit($mip, $mdate, "CGI vuln scan", $tmp, $settings{"CHK_KNOWN_CGI"}); 108 check_add_hit($mip, $mdate, "CGI vuln scan", $tmp, 2, $settings{"CHK_KNOWN_CGI"});
105 } 109 }
106 } 110 }
107 } 111 }
112
108 # (3) Apache common logging format checks 113 # (3) Apache common logging format checks
109 elsif (/(\d+\.\d+\.\d+\.\d+)\s+-\s+-\s+\[(.+?)\]\s+\"GET (\S*?) HTTP\//) { 114 elsif (/(\d+\.\d+\.\d+\.\d+)\s+-\s+-\s+\[(.+?)\]\s+\"GET (\S*?) HTTP\//) {
110 my $mdate = $2; 115 my $mdate = $2;
111 my $mip = $1; 116 my $mip = $1;
112 my $merr = $3; 117 my $merr = $3;
113 118
114 # (3.1) Simple match for generic PHP XSS vulnerability scans 119 # (3.1) Simple match for generic PHP XSS vulnerability scans
115 if ($merr =~ /\.php\?\S*?=http:\/\/([^\/]+)/) { 120 if ($merr =~ /\.php\?\S*?=http:\/\/([^\/]+)/) {
116 if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) { 121 if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) {
117 if ($merr =~ /\.php\?\S*?=(http:\/\/[^\&\?]+\??)/) { 122 if ($merr =~ /\.php\?\S*?=(http:\/\/[^\&\?]+\??)/) {
118 check_add_evidence($mip, $1, $merr); 123 evidence_queue($mip, $1, $merr);
119 } 124 }
120 check_add_hit($mip, $mdate, "PHP XSS", $merr, $settings{"CHK_PHP_XSS"}); 125 check_add_hit($mip, $mdate, "PHP XSS", $merr, 2, $settings{"CHK_PHP_XSS"});
121 } 126 }
122 } 127 }
123 # (3.2) Try to match proxy scanning attempts 128 # (3.2) Try to match proxy scanning attempts
124 elsif ($merr =~ /^http:\/\/([^\/]+)/) { 129 elsif ($merr =~ /^http:\/\/([^\/]+)/) {
125 if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) { 130 if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) {
126 check_add_hit($mip, $mdate, "Proxy scan", $merr, $settings{"CHK_PROXY_SCAN"}); 131 check_add_hit($mip, $mdate, "Proxy scan", $merr, 2, $settings{"CHK_PROXY_SCAN"});
127 } 132 }
128 } 133 }
129 } 134 }
130 } 135 }
131 136
134 ### Global variables 139 ### Global variables
135 ############################################################################# 140 #############################################################################
136 my $reportmode = 0; # Full report mode 141 my $reportmode = 0; # Full report mode
137 my @scanfiles = (); # Files to scan 142 my @scanfiles = (); # Files to scan
138 my @scanfiles_once = (); # Files to scan only once during startup or HUP (e.g. not continuously followed) 143 my @scanfiles_once = (); # Files to scan only once during startup or HUP (e.g. not continuously followed)
139 my @noblock_ips = (); # IPs not to block 144 my @noaction_ips = (); # IPs not to block
140 my %filehandles = (); # Global hash holding opened scanned log filehandles 145 my %filehandles = (); # Global hash holding opened scanned log filehandles
141 my $pid_file = ""; # Name of Maltfilter daemon pid file 146 my $pid_file = ""; # Name of Maltfilter daemon pid file
142 my @configfiles = (); # Array of configuration file names 147 my @configfiles = (); # Array of configuration file names
143 my $LOGFILE; # Maltfilter logfile handle 148 my $LOGFILE; # Maltfilter logfile handle
144 my %dronebl = (); 149 my %dronebl = ();
145 150
146 # IPs currently blocked in Netfilter $blocklist{$ip} = date 151 # IPs currently blocked in Netfilter $filterlist{$ip} = date
147 my %blocklist = (); 152 my %filterlist = ();
148 153
149 # Gathered information about hosts 154 # Gathered information about hosts
150 # $statlist{$ip}-> 155 # $statlist{$ip}->
151 # "date1" = timestamp of first hit 156 # "date1" = timestamp of first hit
152 # "date2" = timestamp of latest hit 157 # "date2" = timestamp of latest hit
306 311
307 "Hits | IP-address | First hit | Latest hit | Reason(s)\n" 312 "Hits | IP-address | First hit | Latest hit | Reason(s)\n"
308 ); 313 );
309 314
310 foreach my $mip (sort { $func->($table, $a, $b) } keys %{$keys}) { 315 foreach my $mip (sort { $func->($table, $a, $b) } keys %{$keys}) {
311 my $blocked = defined($blocklist{$mip}) ? "blocked" : "unblocked"; 316 my $blocked = defined($filterlist{$mip}) ? "blocked" : "unblocked";
312 printElem($m, $f, " <tr class=\"$blocked\">"); 317 printElem($m, $f, " <tr class=\"$blocked\">");
313 printTD($m, $f, sprintf(bb($m)."%-10d".eb($m), $table->{$mip}{"hits"})); 318 printTD($m, $f, sprintf(bb($m)."%-10d".eb($m), $table->{$mip}{"hits"}));
314 printElem(!$m, $f, " | "); 319 printElem(!$m, $f, " | ");
315 printTD($m, $f, sprintf("%-15s", get_link($m, $mip))); 320 printTD($m, $f, sprintf("%-15s", get_link($m, $mip)));
316 printElem(!$m, $f, " | "); 321 printElem(!$m, $f, " | ");
381 386
382 my @previp = ("0.0.0.0", "0.0.0.0"); 387 my @previp = ("0.0.0.0", "0.0.0.0");
383 my @ncolor = (0, 0); 388 my @ncolor = (0, 0);
384 389
385 my $printEntry = sub { 390 my $printEntry = sub {
386 my $blocked = "class=\"".(defined($blocklist{$_[0]}) ? "blocked" : "unblocked")."\""; 391 my $blocked = "class=\"".(defined($filterlist{$_[0]}) ? "blocked" : "unblocked")."\"";
387 if (test_ips($previp[$_[1]], $_[0]) < 3) { 392 if (test_ips($previp[$_[1]], $_[0]) < 3) {
388 $ncolor[$_[1]]++; 393 $ncolor[$_[1]]++;
389 } 394 }
390 $previp[$_[1]] = $_[0]; 395 $previp[$_[1]] = $_[0];
391 my $str = "style=\"background: ".$ipcolors[$ncolor[$_[1]] % scalar @ipcolors].";\""; 396 my $str = "style=\"background: ".$ipcolors[$ncolor[$_[1]] % scalar @ipcolors].";\"";
475 </head> 480 </head>
476 <body> 481 <body>
477 "); 482 ");
478 483
479 printH($m, $f, 1, "Maltfilter v$progversion status report"); 484 printH($m, $f, 1, "Maltfilter v$progversion status report");
480 my $period = get_period($settings{"WEED_GLOBAL"}); 485 my $period = get_period($settings{"STATS_MAX_AGE"});
481 486
482 printP($m, $f, 487 printP($m, $f,
483 "Generated ".bb($m).get_time_str(time()).eb($m).". Data computed from ". 488 "Generated ".bb($m).get_time_str(time()).eb($m).". Data computed from ".
484 ($reportmode ? "complete logfile scan" : "a period of last $period").".\n"); 489 ($reportmode ? "complete logfile scan" : "a period of last $period").".\n");
485 490
486 printP($m, $f, "The hit classes marked as 'IPTABLES' are a pseudo-class meaning an\n". 491 printP($m, $f, "The hit classes marked as 'IPTABLES' are a pseudo-class meaning an\n".
487 "blocked IP that was in Netfilter before Maltfilter was started.\n"); 492 "blocked IP that was in Netfilter before Maltfilter was started.\n");
488 493
489 printH($m, $f, 2, "Currently filtered entries"); 494 printH($m, $f, 2, "Currently filtered entries");
490 $period = get_period($settings{"WEED_FILTER"}); 495 $period = get_period($settings{"FILTER_MAX_AGE"});
491 printP($m, $f, "List of IPs that are currently filtered (or would be, if this is\n". 496 printP($m, $f, "List of IPs that are currently filtered (or would be, if this is\n".
492 "a report-only mode). Data from period of $period.\n"); 497 "a report-only mode). Data from period of $period.\n");
493 print_table1($m, $f, \%statlist, \%blocklist, \&cmp_hits, "blocked"); 498 print_table1($m, $f, \%statlist, \%filterlist, \&cmp_hits, "blocked");
494 499
495 printH($m, $f, 2, "Summary of non-ignored entries"); 500 printH($m, $f, 2, "Summary of non-ignored entries");
496 printP($m, $f, "List of 'hits' of suspicious activity noticed by Maltfilter, but not\n". 501 printP($m, $f, "List of 'hits' of suspicious activity noticed by Maltfilter, but not\n".
497 "necessarily acted upon. Sorted by descending IP address.\n"); 502 "necessarily acted upon. Sorted by descending IP address.\n");
498 print_table2($m, $f, \%statlist, \%statlist, \&cmp_ips, "global"); 503 print_table2($m, $f, \%statlist, \%statlist, \&cmp_ips, "global");
510 ############################################################################# 515 #############################################################################
511 ### DroneBL submission support 516 ### DroneBL submission support
512 ############################################################################# 517 #############################################################################
513 sub dronebl_process 518 sub dronebl_process
514 { 519 {
515 # return if ($reportmode);
516 return unless ($settings{"DRONEBL"} > 0); 520 return unless ($settings{"DRONEBL"} > 0);
521 return if ($settings{"DRY_RUN"});
517 522
518 # Create submission data 523 # Create submission data
519 my $xml = "<?xml version=\"1.0\"?>\n<request key=\"".$settings{"DRONEBL_RPC_KEY"}."\">\n"; 524 my $xml = "<?xml version=\"1.0\"?>\n<request key=\"".$settings{"DRONEBL_RPC_KEY"}."\">\n";
520 my $entries = 0; 525 my $entries = 0;
521 while (my ($ip, $entry) = each(%dronebl)) { 526 while (my ($ip, $entry) = each(%dronebl)) {
522 if ($entry->{"sent"} == 0) { 527 if ($entry->{"sent"} == 0 && $entry->{"tries"} < 3) {
523 $xml .= "<add ip=\"".$ip."\" type=\"".$entry->{"type"}."\" />\n"; 528 $xml .= "<add ip=\"".$ip."\" type=\"1\" />\n";
529 # $xml .= "<add ip=\"".$ip."\" type=\"".$entry->{"type"}."\" />\n";
524 $entries++; 530 $entries++;
525 } 531 }
526 } 532 }
527 $xml .= "</request>\n"; 533 $xml .= "</request>\n";
528 534
529 mlog(1, "Trying to submit $entries entries to DroneBL.\n"); 535 # Bait out if no entries to submit
530 print STDERR $xml;
531 return;
532
533 return unless ($entries > 0); 536 return unless ($entries > 0);
537 mlog(1, "[DroneBL] Trying to submit $entries entries.\n");
538
539 return;
534 540
535 # Submit via HTTP XML-RPC 541 # Submit via HTTP XML-RPC
536 my $tmp = LWP::UserAgent->new; 542 my $tmp = LWP::UserAgent->new;
537 $tmp->agent("Maltfilter/".$progversion); 543 $tmp->agent("Maltfilter/".$progversion);
538 $tmp->timeout(10); 544 $tmp->timeout(10);
541 $req->content($xml); 547 $req->content($xml);
542 $req->user_agent("Maltfilter/".$progversion); 548 $req->user_agent("Maltfilter/".$progversion);
543 my $res = $tmp->request($req); 549 my $res = $tmp->request($req);
544 550
545 if ($res->is_success) { 551 if ($res->is_success) {
546 while (my ($ip, $entry) = each(%dronebl)) { 552 mlog(2, "[DroneBL] [".$res->code."] ".$res->message."\n");
547 $entry->{"sent"} = 1; 553 print $res->content."\n";
548 } 554 # while (my ($ip, $entry) = each(%dronebl)) {
549 } else { 555 # $entry->{"sent"} = 1;
550 mlog(-1, "DroneBL submission failed: [".$res->code."] ".$res->message."\n"); 556 # }
557 } else {
558 mlog(-1, "[DroneBL] Submission failed: [".$res->code."] ".$res->message."\n");
551 } 559 }
552 560
553 # Remove submitted expired entries 561 # Remove submitted expired entries
554 while (my ($ip, $entry) = each(%dronebl)) { 562 while (my ($ip, $entry) = each(%dronebl)) {
555 print "$ip: ".$entry->{"sent"}."\n" unless check_time3($entry->{"date"}); 563 if (!check_time3($entry->{"date"})) {
556 } 564 mlog(1, "[DroneBL] $ip submission expired.\n") unless ($entry->{"sent"} > 0);
557 } 565 delete($dronebl{$ip});
558 566 }
567 }
568 }
569
570 sub dronebl_queue($$$)
571 {
572 my ($mip, $mdate, $mtype) = @_;
573 if (!defined($dronebl{$mip})) {
574 mlog(3, "[DroneBL] Queueing $mip \@ $mdate ($mtype)\n");
575 $dronebl{$mip}{"type"} = $mtype;
576 $dronebl{$mip}{"date"} = $mdate;
577 $dronebl{$mip}{"sent"} = 0;
578 $dronebl{$mip}{"tries"} = 0;
579 }
580 }
559 581
560 ############################################################################# 582 #############################################################################
561 ### Evidence gathering 583 ### Evidence gathering
562 ############################################################################# 584 #############################################################################
563 my %evidence = (); 585 my %evidence = ();
564 586
565 sub check_add_evidence($$$) 587 sub evidence_queue($$$)
566 { 588 {
567 my ($mip, $mdata, $mfull) = @_; 589 my ($mip, $mdata, $mfull) = @_;
568 590
569 return unless ($settings{"EVIDENCE"}); 591 return unless ($settings{"EVIDENCE"} > 0);
570 592
571 my $tmp = $mdata; 593 my $tmp = $mdata;
572 $tmp =~ s/http:\/\///; 594 $tmp =~ s/http:\/\///;
573 $tmp =~ s/^\.+/_/; 595 $tmp =~ s/^\.+/_/;
574 $tmp =~ s/[^A-Za-z0-9:\.]/_/g; 596 $tmp =~ s/[^A-Za-z0-9:\.]/_/g;
576 $evidence{$mdata}{"coll"} = $tmp; 598 $evidence{$mdata}{"coll"} = $tmp;
577 $evidence{$mdata}{"hosts"}{$mip} = 1; 599 $evidence{$mdata}{"hosts"}{$mip} = 1;
578 $evidence{$mdata}{"full"}{$mfull} = 1; 600 $evidence{$mdata}{"full"}{$mfull} = 1;
579 } 601 }
580 602
581 sub http_fetch($$) 603 sub evidence_fetch($$)
582 { 604 {
583 my $tmp = LWP::UserAgent->new; 605 my $tmp = LWP::UserAgent->new;
584 $tmp->agent("-"); 606 $tmp->agent("-");
585 $tmp->timeout(10); 607 $tmp->timeout(10);
586 $tmp->default_headers->referer($_[1]); 608 $tmp->default_headers->referer($_[1]);
587 my $req = HTTP::Request->new(GET => $_[0]); 609 my $req = HTTP::Request->new(GET => $_[0]);
588 return $tmp->request($req); 610 return $tmp->request($req);
589 } 611 }
590 612
591 sub gather_evidence 613 sub evidence_gather
592 { 614 {
593 my $dns = Net::DNS::Resolver->new; 615 my $dns = Net::DNS::Resolver->new;
594 my $base = $settings{"EVIDENCE_DIR"}; 616 my $base = $settings{"EVIDENCE_DIR"};
595 617
596 return unless ($settings{"EVIDENCE"}); 618 return unless ($settings{"EVIDENCE"} > 0);
597 619
598 mdie("Evidence directory '$base' has disappeared.\n") unless (-e $base); 620 mdie("Evidence directory '$base' has disappeared.\n") unless (-e $base);
599 621
600 foreach my $url (keys %evidence) { 622 foreach my $url (keys %evidence) {
601 my $did_fetch = 0; 623 my $did_fetch = 0;
605 627
606 # Get data contents only once 628 # Get data contents only once
607 if (! -e $filename) { 629 if (! -e $filename) {
608 $did_fetch = 1; 630 $did_fetch = 1;
609 mlog(1, "Fetching evidence for $url\n"); 631 mlog(1, "Fetching evidence for $url\n");
610 my $res = http_fetch($url, ""); 632 my $res = evidence_fetch($url, "");
611 open(FILE, ">:raw", $filename) or mdie("Could not open '$filename' for writing.\n"); 633 open(FILE, ">:raw", $filename) or mdie("Could not open '$filename' for writing.\n");
612 binmode(FILE, ":raw"); 634 binmode(FILE, ":raw");
613 if ($res->is_success && $res->code >= 200 && $res->code <= 201) { 635 if ($res->is_success && $res->code >= 200 && $res->code <= 201) {
614 print FILE $res->content; 636 print FILE $res->content;
615 } 637 }
695 } 717 }
696 718
697 ### Execute iptables 719 ### Execute iptables
698 sub exec_iptables(@) 720 sub exec_iptables(@)
699 { 721 {
722 $ENV{"PATH"} = "";
700 my @args = ($settings{"IPTABLES"}, @_); 723 my @args = ($settings{"IPTABLES"}, @_);
701 if ($settings{"DRY_RUN"}) { 724 if ($settings{"DRY_RUN"}) {
702 mlog(3, ":: ".join(" ", @args)."\n"); 725 mlog(3, ":: ".join(" ", @args)."\n");
703 } else { 726 } else {
704 system(@args) == 0 or print join(" ", @args)." failed: $?\n"; 727 system(@args) == 0 or print join(" ", @args)." failed: $?\n";
705 } 728 }
706 } 729 }
707 730
708 ### Get current Netfilter INPUT table entries that match 731 ### Get current Netfilter INPUT table entries that match
709 ### entry types we manage, e.g. blocklist 732 ### entry types we manage, e.g. filterlist
710 sub update_blocklist($) 733 sub update_filterlist($)
711 { 734 {
712 # NOTICE: argument not used now 735 return unless ($settings{"FILTER"} > 0);
713 my $first = $_[0]; 736 my $first = $_[0];
714 737 mlog(0, "Updating initial filterlist from netfilter.\n") unless ($first > 0);
738
715 $ENV{"PATH"} = ""; 739 $ENV{"PATH"} = "";
716 open(STATUS, $settings{"IPTABLES"}." -v -n -L INPUT |") or 740 open(STATUS, $settings{"IPTABLES"}." -v -n -L INPUT |") or
717 mdie("Could not execute ".$settings{"IPTABLES"}."\n"); 741 mdie("Could not execute ".$settings{"IPTABLES"}."\n");
718 my %newlist = (); 742 my %newlist = ();
719 undef(%newlist); 743 undef(%newlist);
720 while (<STATUS>) { 744 while (<STATUS>) {
721 chomp; 745 chomp;
722 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*$/) { 746 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*$/) {
723 my $mip = $2; 747 my $mip = $2;
724 my $mdate = time(); 748 my $mdate = time();
725 if (!defined($blocklist{$mip})) { 749 if (!defined($filterlist{$mip})) {
726 mlog(2, "* $mip appeared in iptables.\n") if ($first > 0); 750 mlog(2, "* $mip appeared in iptables.\n") unless ($first < 0);
727 $blocklist{$2} = $mdate; 751 $filterlist{$2} = $mdate;
728 } 752 }
729 $newlist{$2} = $mdate; 753 $newlist{$2} = $mdate;
730 update_entry(\%statlist, $mip, -1, "IPTABLES", "", 0); 754 update_entry(\%statlist, $mip, -1, "IPTABLES", "", 0);
731 } 755 }
732 } 756 }
733 close(STATUS); 757 close(STATUS);
734 758
735 foreach my $mip (keys %blocklist) { 759 foreach my $mip (keys %filterlist) {
736 if (!defined($newlist{$mip})) { 760 if (!defined($newlist{$mip})) {
737 mlog(2, "* $mip removed from iptables.\n"); 761 mlog(2, "* $mip removed from iptables.\n");
738 delete($blocklist{$mip}); 762 delete($filterlist{$mip});
739 } 763 }
740 } 764 }
741 } 765 }
742 766
743 ### Check if given timestamp is _newer_ than weedperiod threshold. 767 ### Check if given timestamp is _newer_ than weedperiod threshold.
744 ### Returns false if timestamp is over weed period, e.g. needs weeding. 768 ### Returns false if timestamp is over weed period, e.g. needs weeding.
745 sub check_time1($) 769 sub check_time1($)
746 { 770 {
747 return ($_[0] > time() - ($settings{"WEED_FILTER"} * 60 * 60)); 771 return ($_[0] > time() - ($settings{"FILTER_MAX_AGE"} * 60 * 60));
748 } 772 }
749 773
750 sub check_time2($) 774 sub check_time2($)
751 { 775 {
752 return ($_[0] > time() - ($settings{"WEED_GLOBAL"} * 60 * 60)); 776 return ($_[0] > time() - ($settings{"STATS_MAX_AGE"} * 60 * 60));
753 } 777 }
754 778
755 sub check_time3($) 779 sub check_time3($)
756 { 780 {
757 return ($_[0] > time() - ($settings{"DRONEBL_MAX_AGE"} * 60)); 781 return ($_[0] > time() - ($settings{"DRONEBL_MAX_AGE"} * 60));
758 } 782 }
759 783
760 ### Weed out old entries 784 ### Weed out old entries
761 sub weed_do($) 785 sub weed_do($)
762 { 786 {
763 my $mtime = $blocklist{$_[0]}; 787 my $mtime = $filterlist{$_[0]};
764 mlog(2, "* Weeding $_[0] (".get_time_str($mtime).")\n"); 788 mlog(2, "* Weeding $_[0] (".get_time_str($mtime).")\n");
765 exec_iptables("-D", "INPUT", "-s", $_[0], "-d", "0.0.0.0/0", "-j", $settings{"ACTION"}); 789 exec_iptables("-D", "INPUT", "-s", $_[0], "-d", "0.0.0.0/0", "-j", $settings{"FILTER_TARGET"});
766 delete($blocklist{$_[0]}); 790 delete($filterlist{$_[0]});
767 delete($statlist{$_[0]}); 791 delete($statlist{$_[0]});
768 delete($ignorelist{$_[0]}); 792 delete($ignorelist{$_[0]});
769 } 793 }
770 794
771 sub weed_entries() 795 sub weed_entries()
772 { 796 {
773 # Don't weed in report mode. 797 # Don't weed in report mode.
774 return if ($reportmode); 798 return unless ($settings{"FILTER"} > 0 && $reportmode == 0);
775 799
776 # Weed blocked entries. 800 # Weed blocked entries.
777 my @mips = keys %blocklist; 801 my @mips = keys %filterlist;
778 foreach my $mip (@mips) { 802 foreach my $mip (@mips) {
779 if (defined($blocklist{$mip})) { 803 if (defined($filterlist{$mip})) {
780 if ($blocklist{$mip} >= 0) { 804 if ($filterlist{$mip} >= 0) {
781 weed_do($mip) unless check_time1($blocklist{$mip}); 805 weed_do($mip) unless check_time1($filterlist{$mip});
782 } else { 806 } else {
783 weed_do($mip); 807 weed_do($mip);
784 } 808 }
785 } 809 }
786 } 810 }
787 811
788 # Clean up old entries from other lists 812 # Clean up old entries from other lists
789 foreach my $mip (keys %statlist) { 813 foreach my $mip (keys %statlist) {
790 if (defined($statlist{$mip})) { 814 if (defined($statlist{$mip})) {
791 my $mtime = $statlist{$mip}{"date2"}; 815 my $mtime = $statlist{$mip}{"date2"};
792 if (!check_time2($mtime) && !defined($blocklist{$mip})) { 816 if (!check_time2($mtime) && !defined($filterlist{$mip})) {
793 mlog(3, "* Deleting stale $mip (".get_time_str($mtime).")\n"); 817 mlog(3, "* Deleting stale $mip (".get_time_str($mtime).")\n");
794 delete($statlist{$mip}); 818 delete($statlist{$mip});
795 } 819 }
796 } 820 }
797 } 821 }
860 my $mreason = $_[3]; 884 my $mreason = $_[3];
861 my $mtype = $_[4]; 885 my $mtype = $_[4];
862 my $mcond = $_[5]; 886 my $mcond = $_[5];
863 my $cnt; 887 my $cnt;
864 888
865 if (check_hosts_array(\@noblock_ips, $mip)) { 889 if (check_hosts_array(\@noaction_ips, $mip)) {
866 mlog(3, "Hit to NOBLOCK_IPS($mip): [$mclass] $mreason\n"); 890 mlog(2, "Hit to NOACTION_IPS($mip): [$mclass] $mreason\n");
867 return; 891 return;
868 } 892 }
869 893
870 # If condition is true, we add to regular statlist 894 # If condition is true, we add to regular statlist
871 if ($mcond) { 895 if ($mcond) {
875 update_entry(\%ignorelist, $mip, $mdate, $mclass, $mreason, 1); 899 update_entry(\%ignorelist, $mip, $mdate, $mclass, $mreason, 1);
876 return; 900 return;
877 } 901 }
878 902
879 # Check if we have exceeded threshold etc. 903 # Check if we have exceeded threshold etc.
880 if ($cnt >= $settings{"THRESHOLD"} && check_time1($mdate)) { 904 if ($settings{"FILTER"} > 0 && $cnt >= $settings{"FILTER_THRESHOLD"} && check_time1($mdate)) {
881 # Add to blocklist, unless already there. 905 # Add to filterlist, unless already there.
882 if (!defined($blocklist{$mip})) { 906 if (!defined($filterlist{$mip})) {
883 mlog(1, "* Adding $mip ($mdate): [$mclass] $mreason\n"); 907 mlog(1, "* Adding $mip \@ ".get_time_str($mdate).": [$mclass] $mreason\n");
884 exec_iptables("-I", "INPUT", "1", "-s", $mip, "-j", $settings{"ACTION"}); 908 exec_iptables("-I", "INPUT", "1", "-s", $mip, "-j", $settings{"FILTER_TARGET"});
885 } 909 }
886 # Update date of last hit 910 # Update date of last hit
887 $blocklist{$mip} = $mdate; 911 $filterlist{$mip} = $mdate;
888 } 912 }
889 913
890 # Separate check for DroneBL 914 # Separate check for DroneBL
891 if ($settings{"DRONEBL"} > 0 && $mtype > 0 && $cnt >= $settings{"DRONEBL_THRESHOLD"} && check_time3($mdate)) { 915 if ($settings{"DRONEBL"} > 0 && $mtype > 0 && $cnt >= $settings{"DRONEBL_THRESHOLD"} && check_time3($mdate)) {
892 $dronebl{$mip}{"type"} = $mtype; 916 dronebl_queue($mip, $mdate, $mtype);
893 $dronebl{$mip}{"date"} = $mdate;
894 $dronebl{$mip}{"sent"} = 0 unless defined($dronebl{$mip}{"sent"});
895 } 917 }
896 } 918 }
897 919
898 920
899 ############################################################################# 921 #############################################################################
923 { 945 {
924 %statlist = (); 946 %statlist = ();
925 undef(%statlist); 947 undef(%statlist);
926 %ignorelist = (); 948 %ignorelist = ();
927 undef(%ignorelist); 949 undef(%ignorelist);
928 mlog(0, "Updating initial blocklist from netfilter.\n"); 950 update_filterlist(-1);
929 update_blocklist(-1);
930 951
931 foreach my $filename (@scanfiles_once) { 952 foreach my $filename (@scanfiles_once) {
932 mlog(0, "Parsing [ONCE] ".$filename." ...\n"); 953 mlog(0, "Parsing [ONCE] ".$filename." ...\n");
933 if (open(INFILE, "<", $filename)) { 954 if (open(INFILE, "<", $filename)) {
934 while (<INFILE>) { 955 while (<INFILE>) {
997 mlog(-1, "Received HUP, reinitializing.\n"); 1018 mlog(-1, "Received HUP, reinitializing.\n");
998 malt_cleanup(); 1019 malt_cleanup();
999 malt_configure(); 1020 malt_configure();
1000 malt_init(); 1021 malt_init();
1001 mlog(-1, "Reinitialization finished, resuming scanning.\n"); 1022 mlog(-1, "Reinitialization finished, resuming scanning.\n");
1023 }
1024
1025 sub malt_maintenance
1026 {
1027 update_filterlist(time());
1028 weed_entries();
1029 generate_status($settings{"STATUS_FILE_PLAIN"}, 0);
1030 generate_status($settings{"STATUS_FILE_HTML"}, 1);
1031 evidence_gather();
1032 dronebl_process();
1002 } 1033 }
1003 1034
1004 ### Main scanning function 1035 ### Main scanning function
1005 sub malt_scan 1036 sub malt_scan
1006 { 1037 {
1015 chomp; 1046 chomp;
1016 check_log_line($_); 1047 check_log_line($_);
1017 } 1048 }
1018 } 1049 }
1019 if ($counter < 0 || $counter++ >= 30) { 1050 if ($counter < 0 || $counter++ >= 30) {
1020 # Every once in a while, update known IP list from iptables 1051 # Every once in a while, execute maintenance functions
1021 # (in case entries have appeared there from "outside")
1022 # and perform weeding of old entries.
1023 $counter = 0; 1052 $counter = 0;
1024 update_blocklist(time()); 1053 malt_maintenance();
1025 weed_entries();
1026 generate_status($settings{"STATUS_FILE_PLAIN"}, 0);
1027 generate_status($settings{"STATUS_FILE_HTML"}, 1);
1028 gather_evidence();
1029 } 1054 }
1030 sleep(2); 1055 sleep(2);
1031 foreach my $filename (keys %filehandles) { 1056 foreach my $filename (keys %filehandles) {
1032 seek($filehandles{$filename}, $filepos{$filename}, 0); 1057 seek($filehandles{$filename}, $filepos{$filename}, 0);
1033 } 1058 }
1061 my $value = $2; 1086 my $value = $2;
1062 if ($key eq "SCANFILE") { 1087 if ($key eq "SCANFILE") {
1063 push(@scanfiles, $value); 1088 push(@scanfiles, $value);
1064 } elsif ($key eq "SCANFILE_ONCE") { 1089 } elsif ($key eq "SCANFILE_ONCE") {
1065 push(@scanfiles_once, $value); 1090 push(@scanfiles_once, $value);
1066 } elsif ($key eq "NOBLOCK_IPS") { 1091 } elsif ($key eq "NOACTION_IPS") {
1067 push(@noblock_ips_def, $value); 1092 push(@noaction_ips_def, $value);
1068 } elsif (defined($settings{$key})) { 1093 } elsif (defined($settings{$key})) {
1069 $settings{$key} = $value; 1094 $settings{$key} = $value;
1070 } else { 1095 } else {
1071 mlog(-1, "[$filename:$line] Unknown setting '$key' = '$value'\n"); 1096 mlog(-1, "[$filename:$line] Unknown setting '$key' = '$value'\n");
1072 $errors = 1; 1097 $errors = 1;
1105 1130
1106 %saw = (); 1131 %saw = ();
1107 @scanfiles_once = grep(!$saw{$_}++, @scanfiles_once); 1132 @scanfiles_once = grep(!$saw{$_}++, @scanfiles_once);
1108 1133
1109 %saw = (); 1134 %saw = ();
1110 @noblock_ips = grep(!$saw{$_}++, @noblock_ips_def); 1135 @noaction_ips = grep(!$saw{$_}++, @noaction_ips_def);
1111 undef(%saw); 1136 undef(%saw);
1112 1137
1113 mlog(-1, "Not blocking following IPs: ".join(", ", @noblock_ips)."\n"); 1138 mlog(-1, "Not acting on IPs: ".join(", ", @noaction_ips)."\n");
1114 1139
1115 # Check if we have anything to do 1140 # Check if we have anything to do
1116 if ($reportmode) { 1141 if ($reportmode) {
1117 mdie("Nothing to do, no SCANFILE(s) or SCANFILE_ONCE(s) defined in configuration.\n") unless ($#scanfiles > 0 || $#scanfiles_once > 0); 1142 mdie("Nothing to do, no SCANFILE(s) or SCANFILE_ONCE(s) defined in configuration.\n") unless ($#scanfiles > 0 || $#scanfiles_once > 0);
1118 } else { 1143 } else {
1119 mdie("Nothing to do, no SCANFILE(s) defined in configuration.\n") unless ($#scanfiles > 0); 1144 mdie("Nothing to do, no SCANFILE(s) defined in configuration.\n") unless ($#scanfiles > 0);
1120 } 1145 }
1121 1146
1122 # Test existence of iptables 1147 # General settings
1123 if (! -e $settings{"IPTABLES"} || ! -x $settings{"IPTABLES"}) { 1148 my $val = $settings{"STATS_MAX_AGE"};
1124 mdie("iptables binary does not exist or is not executable: ".$settings{"IPTABLES"}."\n"); 1149 mdie("Invalid STATS_MAX_AGE value $val, must be > 0.\n") unless ($val > 0);
1150
1151 # Filtering
1152 if ($settings{"FILTER"} > 0) {
1153 $val = $settings{"FILTER_MAX_AGE"};
1154 mdie("Invalid FILTER_MAX_AGE value $val, must be > 0.\n") unless ($val > 0);
1155
1156 $val = $settings{"FILTER_THRESHOLD"};
1157 mdie("Invalid FILTER_THRESHOLD value $val, must be >= 0.\n") unless ($val >= 0);
1158
1159 $val = $settings{"IPTABLES"};
1160 mdie("iptables binary does not exist or is not executable: $val\n") unless (-e $val && -x $val);
1161 } else {
1162 mlog(1, "Netfilter handling disabled.\n");
1125 } 1163 }
1126 1164
1127 # Check evidence settings 1165 # Check evidence settings
1128 if ($settings{"EVIDENCE"}) { 1166 if ($settings{"EVIDENCE"} > 0) {
1129 my $base = $settings{"EVIDENCE_DIR"}; 1167 my $base = $settings{"EVIDENCE_DIR"};
1130 mdie("Evidence directory (EVIDENCE_DIR) not set in configuration.\n") if ($base eq ""); 1168 mdie("Evidence directory (EVIDENCE_DIR) not set in configuration.\n") if ($base eq "");
1131 mdie("Evidence directory '$base' does not exist.\n") unless (-e $base); 1169 mdie("Evidence directory '$base' does not exist.\n") unless (-e $base);
1132 mdie("Path '$base' is not a directory.\n") unless (-d $base); 1170 mdie("Path '$base' is not a directory.\n") unless (-d $base);
1133 mdie("Evidence directory '$base' is not writable by euid.\n") unless (-w $base); 1171 mdie("Evidence directory '$base' is not writable by euid.\n") unless (-w $base);
1134 } 1172 }
1135 1173
1136 # Check settings 1174 # Sanitize DroneBL configuration
1175 if ($settings{"DRONEBL"} > 0) {
1176 mdie("DroneBL RPC key not set.\n") unless ($settings{"DRONEBL_RPC_KEY"} ne "");
1177 }
1178
1179 # Check system account / passwd settings
1137 mdie("SYSACCT_MIN_UID must be >= 1.\n") unless ($settings{"SYSACCT_MIN_UID"} >= 1); 1180 mdie("SYSACCT_MIN_UID must be >= 1.\n") unless ($settings{"SYSACCT_MIN_UID"} >= 1);
1138 mdie("SYSACCT_MAX_UID must be >= SYSACCT_MIN_UID.\n") unless ($settings{"SYSACCT_MAX_UID"} >= $settings{"SYSACCT_MIN_UID"}); 1181 mdie("SYSACCT_MAX_UID must be >= SYSACCT_MIN_UID.\n") unless ($settings{"SYSACCT_MAX_UID"} >= $settings{"SYSACCT_MIN_UID"});
1139 1182
1140 open(PASSWD, "<", $settings{"PASSWD"}) or mdie("Could not open '".$settings{"PASSWD"}."' for reading!\n"); 1183 open(PASSWD, "<", $settings{"PASSWD"}) or mdie("Could not open '".$settings{"PASSWD"}."' for reading!\n");
1141 while (<PASSWD>) { 1184 while (<PASSWD>) {
1158 $SIG{'HUP'} = 'malt_hup'; 1201 $SIG{'HUP'} = 'malt_hup';
1159 1202
1160 # Print banner and help if no arguments 1203 # Print banner and help if no arguments
1161 my $argc = $#ARGV + 1; 1204 my $argc = $#ARGV + 1;
1162 if ($argc < 1) { 1205 if ($argc < 1) {
1163 print $progbanner. 1206 print STDERR $progbanner.
1164 "\n". 1207 "\n".
1165 "Usage: maltfilter <pid filename> [config filename] [config filename...]\n". 1208 "Usage: maltfilter <pid filename> [config filename] [config filename...]\n".
1166 " maltfilter -f [config filename] [config filename...]\n". 1209 " maltfilter -f [config filename] [config filename...]\n".
1167 "-f turns on the full report mode.\n"; 1210 "-f turns on the full report mode.\n";
1168 exit; 1211 exit;
1170 1213
1171 # Test pid file existence unless report mode 1214 # Test pid file existence unless report mode
1172 $pid_file = shift; 1215 $pid_file = shift;
1173 if ($pid_file eq "-f") { 1216 if ($pid_file eq "-f") {
1174 $reportmode = 1; 1217 $reportmode = 1;
1218 print STDERR $progbanner;
1175 } else { 1219 } else {
1176 mdie("'$pid_file' already exists, not starting.\n". 1220 mdie("'$pid_file' already exists, not starting.\n".
1177 "If the daemon is NOT running, remove the pid-file and re-start.\n") 1221 "If the daemon is NOT running, remove the pid-file and re-start.\n")
1178 if (-e $pid_file); 1222 if (-e $pid_file);
1179 } 1223 }
1185 1229
1186 malt_configure(); 1230 malt_configure();
1187 1231
1188 # Open logfile 1232 # Open logfile
1189 if ($settings{"DRY_RUN"}) { 1233 if ($settings{"DRY_RUN"}) {
1190 print $progbanner. 1234 print STDERR
1191 "*********************************************\n". 1235 "*********************************\n".
1192 "* NOTICE! DRY-RUN MODE ENABLED! No changes *\n". 1236 "* NOTICE! DRY-RUN MODE ENABLED! *\n".
1193 "* will actually get committed to netfilter! *\n". 1237 "*********************************\n";
1194 "*********************************************\n";
1195 } elsif ($settings{"LOGFILE"} ne "") { 1238 } elsif ($settings{"LOGFILE"} ne "") {
1196 open($LOGFILE, ">>", $settings{"LOGFILE"}) or die("Could not open logfile '".$settings{"LOGFILE"}."' for writing!\n"); 1239 open($LOGFILE, ">>", $settings{"LOGFILE"}) or die("Could not open logfile '".$settings{"LOGFILE"}."' for writing!\n");
1197 select((select($LOGFILE), $| = 1)[0]); 1240 select((select($LOGFILE), $| = 1)[0]);
1198 mlog(-1, "Log started\n"); 1241 mlog(-1, "Log started\n");
1199 } 1242 }
1202 malt_init(); 1245 malt_init();
1203 1246
1204 # Fork to background, unless dry-running 1247 # Fork to background, unless dry-running
1205 if ($settings{"DRY_RUN"}) { 1248 if ($settings{"DRY_RUN"}) {
1206 if ($reportmode) { 1249 if ($reportmode) {
1207 mlog(-1, "Outputting report files.\n"); 1250 malt_maintenance();
1208 generate_status($settings{"STATUS_FILE_PLAIN"}, 0);
1209 generate_status($settings{"STATUS_FILE_HTML"}, 1);
1210 gather_evidence();
1211 malt_cleanup(); 1251 malt_cleanup();
1212 } else { 1252 } else {
1213 malt_scan(); 1253 malt_scan();
1214 malt_cleanup(); 1254 malt_cleanup();
1215 } 1255 }