comparison maltfilter @ 65:d2e2b82dd2f2

Work on DroneBL support.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 18 Aug 2009 00:43:10 +0300
parents 6917de5b91be
children 42889eed0ce8
comparison
equal deleted inserted replaced
64:213e5204abea 65:d2e2b82dd2f2
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.14.0"; 15 my $progversion = "0.15.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
22 ### Default settings and configuration 22 ### Default settings and configuration
23 ############################################################################# 23 #############################################################################
24 my %settings = ( 24 my %settings = (
25 "VERBOSITY" => 3, 25 "VERBOSITY" => 3,
26 "DRY_RUN" => 1, 26 "DRY_RUN" => 1,
27 "WEED_BLOCK" => 168, 27 "WEED_FILTER" => 168, # in hours
28 "WEED_GLOBAL" => 336, 28 "WEED_GLOBAL" => 336, # in hours
29 "TRESHOLD" => 3, 29 "THRESHOLD" => 3,
30 "ACTION" => "DROP", 30 "ACTION" => "DROP",
31 "LOGFILE" => "", 31 "LOGFILE" => "",
32 "IPTABLES" => "/sbin/iptables", 32 "IPTABLES" => "/sbin/iptables",
33
34 "PASSWD" => "/etc/passwd",
35 "SYSACCT_MIN_UID" => 1,
36 "SYSACCT_MAX_UID" => 100,
33 37
34 "FULL_TIME" => 1, 38 "FULL_TIME" => 1,
35 "STATUS_FILE_PLAIN" => "", 39 "STATUS_FILE_PLAIN" => "",
36 "STATUS_FILE_HTML" => "", 40 "STATUS_FILE_HTML" => "",
37 "STATUS_FILE_CSS" => "", 41 "STATUS_FILE_CSS" => "",
43 "CHK_PROXY_SCAN" => 1, 47 "CHK_PROXY_SCAN" => 1,
44 "CHK_ROOT_SSH_PWD" => 0, 48 "CHK_ROOT_SSH_PWD" => 0,
45 "CHK_SYSACCT_SSH_PWD" => 0, 49 "CHK_SYSACCT_SSH_PWD" => 0,
46 "CHK_GOOD_HOSTS" => "", 50 "CHK_GOOD_HOSTS" => "",
47 51
48 "PASSWD" => "/etc/passwd",
49 "SYSACCT_MIN_UID" => 1,
50 "SYSACCT_MAX_UID" => 100,
51
52 "EVIDENCE" => 0, 52 "EVIDENCE" => 0,
53 "EVIDENCE_DIR" => "", 53 "EVIDENCE_DIR" => "",
54
55 "DRONEBL" => 0,
56 "DRONEBL_THRESHOLD" => 5,
57 "DRONEBL_MAX_AGE" => 30, # in minutes
58 "DRONEBL_RPC_URI" => "http://dronebl.org/RPC2",
59 "DRONEBL_RPC_KEY" => "",
54 ); 60 );
55 61
56 my @noblock_ips_def = ( 62 my @noblock_ips_def = (
57 "127.0.0.1", 63 "127.0.0.1",
58 ); 64 );
59 65
60 my %systemacct = (); 66 my %systemacct = ();
67 sub check_add_hit($$$$$$);
68
61 69
62 ############################################################################# 70 #############################################################################
63 ### Check given logfile line for matches 71 ### Check given logfile line for matches
64 ############################################################################# 72 #############################################################################
65 sub check_log_line($) 73 sub check_log_line($)
105 113
106 # (3.1) Simple match for generic PHP XSS vulnerability scans 114 # (3.1) Simple match for generic PHP XSS vulnerability scans
107 if ($merr =~ /\.php\?\S*?=http:\/\/([^\/]+)/) { 115 if ($merr =~ /\.php\?\S*?=http:\/\/([^\/]+)/) {
108 if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) { 116 if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) {
109 if ($merr =~ /\.php\?\S*?=(http:\/\/[^\&\?]+\??)/) { 117 if ($merr =~ /\.php\?\S*?=(http:\/\/[^\&\?]+\??)/) {
110 check_add_evidence($mip, $1); 118 check_add_evidence($mip, $1, $merr);
111 } 119 }
112 check_add_hit($mip, $mdate, "PHP XSS", $merr, $settings{"CHK_PHP_XSS"}); 120 check_add_hit($mip, $mdate, "PHP XSS", $merr, $settings{"CHK_PHP_XSS"});
113 } 121 }
114 } 122 }
115 # (3.2) Try to match proxy scanning attempts 123 # (3.2) Try to match proxy scanning attempts
131 my @noblock_ips = (); # IPs not to block 139 my @noblock_ips = (); # IPs not to block
132 my %filehandles = (); # Global hash holding opened scanned log filehandles 140 my %filehandles = (); # Global hash holding opened scanned log filehandles
133 my $pid_file = ""; # Name of Maltfilter daemon pid file 141 my $pid_file = ""; # Name of Maltfilter daemon pid file
134 my @configfiles = (); # Array of configuration file names 142 my @configfiles = (); # Array of configuration file names
135 my $LOGFILE; # Maltfilter logfile handle 143 my $LOGFILE; # Maltfilter logfile handle
144 my %dronebl = ();
136 145
137 # IPs currently blocked in Netfilter $blocklist{$ip} = date 146 # IPs currently blocked in Netfilter $blocklist{$ip} = date
138 my %blocklist = (); 147 my %blocklist = ();
139 148
140 # Gathered information about hosts 149 # Gathered information about hosts
475 ($reportmode ? "complete logfile scan" : "a period of last $period").".\n"); 484 ($reportmode ? "complete logfile scan" : "a period of last $period").".\n");
476 485
477 printP($m, $f, "The hit classes marked as 'IPTABLES' are a pseudo-class meaning an\n". 486 printP($m, $f, "The hit classes marked as 'IPTABLES' are a pseudo-class meaning an\n".
478 "blocked IP that was in Netfilter before Maltfilter was started.\n"); 487 "blocked IP that was in Netfilter before Maltfilter was started.\n");
479 488
480 printH($m, $f, 2, "Currently blocked entries"); 489 printH($m, $f, 2, "Currently filtered entries");
481 $period = get_period($settings{"WEED_BLOCK"}); 490 $period = get_period($settings{"WEED_FILTER"});
482 printP($m, $f, "List of IPs that are currently blocked (or would be, if this is\n". 491 printP($m, $f, "List of IPs that are currently filtered (or would be, if this is\n".
483 "a report-only mode). Data from period of $period.\n"); 492 "a report-only mode). Data from period of $period.\n");
484 print_table1($m, $f, \%statlist, \%blocklist, \&cmp_hits, "blocked"); 493 print_table1($m, $f, \%statlist, \%blocklist, \&cmp_hits, "blocked");
485 494
486 printH($m, $f, 2, "Summary of non-ignored entries"); 495 printH($m, $f, 2, "Summary of non-ignored entries");
487 printP($m, $f, "List of 'hits' of suspicious activity noticed by Maltfilter, but not\n". 496 printP($m, $f, "List of 'hits' of suspicious activity noticed by Maltfilter, but not\n".
497 close(STATUS); 506 close(STATUS);
498 } 507 }
499 508
500 509
501 ############################################################################# 510 #############################################################################
511 ### DroneBL submission support
512 #############################################################################
513 sub dronebl_process
514 {
515 # return if ($reportmode);
516 return unless ($settings{"DRONEBL"} > 0);
517
518 # Create submission data
519 my $xml = "<?xml version=\"1.0\"?>\n<request key=\"".$settings{"DRONEBL_RPC_KEY"}."\">\n";
520 my $entries = 0;
521 while (my ($ip, $entry) = each(%dronebl)) {
522 if ($entry->{"sent"} == 0) {
523 $xml .= "<add ip=\"".$ip."\" type=\"".$entry->{"type"}."\" />\n";
524 $entries++;
525 }
526 }
527 $xml .= "</request>\n";
528
529 mlog(1, "Trying to submit $entries entries to DroneBL.\n");
530 print STDERR $xml;
531 return;
532
533 return unless ($entries > 0);
534
535 # Submit via HTTP XML-RPC
536 my $tmp = LWP::UserAgent->new;
537 $tmp->agent("Maltfilter/".$progversion);
538 $tmp->timeout(10);
539 my $req = HTTP::Request->new(POST => $settings{"DRONEBL_RPC_URI"});
540 $req->content_type("text/xml");
541 $req->content($xml);
542 $req->user_agent("Maltfilter/".$progversion);
543 my $res = $tmp->request($req);
544
545 if ($res->is_success) {
546 while (my ($ip, $entry) = each(%dronebl)) {
547 $entry->{"sent"} = 1;
548 }
549 } else {
550 mlog(-1, "DroneBL submission failed: [".$res->code."] ".$res->message."\n");
551 }
552
553 # Remove submitted expired entries
554 while (my ($ip, $entry) = each(%dronebl)) {
555 print "$ip: ".$entry->{"sent"}."\n" unless check_time3($entry->{"date"});
556 }
557 }
558
559
560 #############################################################################
502 ### Evidence gathering 561 ### Evidence gathering
503 ############################################################################# 562 #############################################################################
504 my %evidence = (); 563 my %evidence = ();
505 564
506 sub check_add_evidence($$) 565 sub check_add_evidence($$$)
507 { 566 {
508 my ($mip, $mdata) = @_; 567 my ($mip, $mdata, $mfull) = @_;
509 568
510 return unless ($settings{"EVIDENCE"}); 569 return unless ($settings{"EVIDENCE"});
511 570
512 my $tmp = $mdata; 571 my $tmp = $mdata;
513 $tmp =~ s/http:\/\///; 572 $tmp =~ s/http:\/\///;
514 $tmp =~ s/^\.+/_/; 573 $tmp =~ s/^\.+/_/;
515 $tmp =~ s/[^A-Za-z0-9:\.]/_/g; 574 $tmp =~ s/[^A-Za-z0-9:\.]/_/g;
516 575
517 $evidence{$mdata}{"coll"} = $tmp; 576 $evidence{$mdata}{"coll"} = $tmp;
518 $evidence{$mdata}{"hosts"}{$mip} = 1; 577 $evidence{$mdata}{"hosts"}{$mip} = 1;
578 $evidence{$mdata}{"full"}{$mfull} = 1;
519 } 579 }
520 580
521 sub http_fetch($$) 581 sub http_fetch($$)
522 { 582 {
523 my $tmp = LWP::UserAgent->new; 583 my $tmp = LWP::UserAgent->new;
536 return unless ($settings{"EVIDENCE"}); 596 return unless ($settings{"EVIDENCE"});
537 597
538 mdie("Evidence directory '$base' has disappeared.\n") unless (-e $base); 598 mdie("Evidence directory '$base' has disappeared.\n") unless (-e $base);
539 599
540 foreach my $url (keys %evidence) { 600 foreach my $url (keys %evidence) {
541 if (!$evidence{$url}{"done"}) { 601 my $did_fetch = 0;
542 my $filename = $base."/".$evidence{$url}{"coll"}.".data"; 602 my $filename = $base."/".$evidence{$url}{"coll"}.".data";
543 my $filename2 = $base."/".$evidence{$url}{"coll"}.".hosts"; 603 my $filename2 = $base."/".$evidence{$url}{"coll"}.".hosts";
544 my $code = 0; 604 my $filename3 = $base."/".$evidence{$url}{"coll"}.".info";
545 my $message = ""; 605
546 606 # Get data contents only once
547 # Get data contents only once 607 if (! -e $filename) {
548 if (! -e $filename) { 608 $did_fetch = 1;
549 mlog(1, "Fetching evidence for $url\n"); 609 mlog(1, "Fetching evidence for $url\n");
550 my $res = http_fetch($url, ""); 610 my $res = http_fetch($url, "");
551 $code = $res->code; 611 open(FILE, ">:raw", $filename) or mdie("Could not open '$filename' for writing.\n");
552 $message = $res->message; 612 binmode(FILE, ":raw");
553 open(FILE, ">:raw", $filename) or mdie("Could not open '$filename' for writing.\n"); 613 if ($res->is_success && $res->code >= 200 && $res->code <= 201) {
554 binmode(FILE, ":raw"); 614 print FILE $res->content;
555 if ($res->code >= 200 && $res->code <= 201) { 615 }
556 print FILE $res->content; 616 close(FILE);
557 } else { 617
558 print FILE "[$code] $message\n"; 618 open(FILE, ">:raw", $filename3) or mdie("Could not open '$filename3' for writing.\n");
559 } 619 binmode(FILE, ":raw");
560 close(FILE); 620 print FILE "XSS URI : $url\n";
561 } 621 print FILE "Time of retrieval : ".get_time_str(time())."\n";
562 622 print FILE "HTTP return code : [".$res->code."] ".$res->message."\n";
563 # Check if we are appending hosts to existing data 623 print FILE "Content-Type : ".($res->content_type ? $res->content_type : "?")."\n";
564 if (-e $filename2) { 624 print FILE "Last modified : ".($res->last_modified ? $res->last_modified : "?")."\n";
565 open(FILE, "<", $filename2) or mdie("Could not open '$filename2' for reading.\n"); 625 print FILE "------ HTTP Headers ------\n".$res->headers_as_string."\n";
566 while (<FILE>) { 626 print FILE "------ Requests ------\n";
567 if (/^(\d+\.\d+\.\d+\.\d+) *\|/) { 627 print FILE $_."\n" foreach (keys %{$evidence{$url}{"full"}});
568 if (defined($evidence{$url}{"hosts"}{$1})) { 628 close(FILE);
569 delete($evidence{$url}{"hosts"}{$1}); 629 }
570 } 630
631 # Check if we are appending hosts to existing data
632 if (-e $filename2) {
633 open(FILE, "<", $filename2) or mdie("Could not open '$filename2' for reading.\n");
634 while (<FILE>) {
635 if (/^(\d+\.\d+\.\d+\.\d+) *\|/) {
636 if (defined($evidence{$url}{"hosts"}{$1})) {
637 delete($evidence{$url}{"hosts"}{$1});
571 } 638 }
572 } 639 }
573 close(FILE); 640 }
574 open(FILE, ">>", $filename2) or mdie("Could not open '$filename2' for appending.\n"); 641 close(FILE);
575 } else { 642 open(FILE, ">>", $filename2) or mdie("Could not open '$filename2' for appending.\n");
576 open(FILE, ">", $filename2) or mdie("Could not open '$filename2' for writing.\n"); 643 } else {
577 print FILE "# HTTP request result: [$code] $message\n"; 644 open(FILE, ">", $filename2) or mdie("Could not open '$filename2' for writing.\n");
578 print FILE "# Hosts which requested $url\n\n"; 645 }
579 } 646 foreach my $host (sort keys %{$evidence{$url}{"hosts"}}) {
580 647 my $query = $dns->search($host);
581 foreach my $host (sort keys %{$evidence{$url}{"hosts"}}) { 648 my @names = ();
582 print "lol: $host\n"; 649 undef(@names);
583 my $query = $dns->search($host); 650 if ($query) {
584 my @names = (); 651 foreach my $rr ($query->answer) {
585 undef(@names); 652 push(@names, $rr->{"ptrdname"}) if defined($rr->{"ptrdname"});
586 if ($query) {
587 foreach my $rr ($query->answer) {
588 push(@names, $rr->{"ptrdname"}) if defined($rr->{"ptrdname"});
589 }
590 } 653 }
591 printf FILE "%-15s | %s\n", $host, join(" | ", @names); 654 }
592 } 655 printf FILE "%-15s | %s\n", $host, join(" | ", @names);
593 close(FILE); 656 }
594 657 close(FILE);
595 delete($evidence{$url}); 658
596 659 # This entry has been handled, delete it
597 return unless $reportmode; 660 delete($evidence{$url});
598 } 661
662 # If not in report mode, handle only one fetched entry
663 return unless ($reportmode || !$did_fetch);
599 } 664 }
600 } 665 }
601 666
602 667
603 ############################################################################# 668 #############################################################################
677 742
678 ### Check if given timestamp is _newer_ than weedperiod threshold. 743 ### Check if given timestamp is _newer_ than weedperiod threshold.
679 ### Returns false if timestamp is over weed period, e.g. needs weeding. 744 ### Returns false if timestamp is over weed period, e.g. needs weeding.
680 sub check_time1($) 745 sub check_time1($)
681 { 746 {
682 return ($_[0] >= time() - ($settings{"WEED_BLOCK"} * 60 * 60)); 747 return ($_[0] > time() - ($settings{"WEED_FILTER"} * 60 * 60));
683 } 748 }
684 749
685 sub check_time2($) 750 sub check_time2($)
686 { 751 {
687 return ($_[0] >= time() - ($settings{"WEED_GLOBAL"} * 60 * 60)); 752 return ($_[0] > time() - ($settings{"WEED_GLOBAL"} * 60 * 60));
753 }
754
755 sub check_time3($)
756 {
757 return ($_[0] > time() - ($settings{"DRONEBL_MAX_AGE"} * 60));
688 } 758 }
689 759
690 ### Weed out old entries 760 ### Weed out old entries
691 sub weed_do($) 761 sub weed_do($)
692 { 762 {
778 update_date($reason, $mdate); 848 update_date($reason, $mdate);
779 849
780 return $entry->{"hits"}; 850 return $entry->{"hits"};
781 } 851 }
782 852
783 ### Check if given "try count" exceeds treshold and if entry 853 ### Check if given "try count" exceeds threshold and if entry
784 ### is NOT in Netfilter already, then add it if so. 854 ### is NOT in Netfilter already, then add it if so.
785 sub check_add_hit($$$$$) 855 sub check_add_hit($$$$$$)
786 { 856 {
787 my $mip = $_[0]; 857 my $mip = $_[0];
788 my $mdate = str2time($_[1]); 858 my $mdate = str2time($_[1]);
789 my $mclass = $_[2]; 859 my $mclass = $_[2];
790 my $mreason = $_[3]; 860 my $mreason = $_[3];
791 my $mcond = $_[4]; 861 my $mtype = $_[4];
862 my $mcond = $_[5];
792 my $cnt; 863 my $cnt;
793 864
794 if (check_hosts_array(\@noblock_ips, $mip)) { 865 if (check_hosts_array(\@noblock_ips, $mip)) {
795 mlog(3, "Hit to NOBLOCK_IPS($mip): [$mclass] $mreason\n"); 866 mlog(3, "Hit to NOBLOCK_IPS($mip): [$mclass] $mreason\n");
796 return; 867 return;
803 # This is an ignored hit (for disabled test), add to ignorelist 874 # This is an ignored hit (for disabled test), add to ignorelist
804 update_entry(\%ignorelist, $mip, $mdate, $mclass, $mreason, 1); 875 update_entry(\%ignorelist, $mip, $mdate, $mclass, $mreason, 1);
805 return; 876 return;
806 } 877 }
807 878
808 # Check if we have exceeded treshold etc. 879 # Check if we have exceeded threshold etc.
809 if ($cnt >= $settings{"TRESHOLD"} && check_time1($mdate)) { 880 if ($cnt >= $settings{"THRESHOLD"} && check_time1($mdate)) {
810 # Add to blocklist, unless already there. 881 # Add to blocklist, unless already there.
811 if (!defined($blocklist{$mip})) { 882 if (!defined($blocklist{$mip})) {
812 mlog(1, "* Adding $mip ($mdate): [$mclass] $mreason\n"); 883 mlog(1, "* Adding $mip ($mdate): [$mclass] $mreason\n");
813 exec_iptables("-I", "INPUT", "1", "-s", $mip, "-j", $settings{"ACTION"}); 884 exec_iptables("-I", "INPUT", "1", "-s", $mip, "-j", $settings{"ACTION"});
814 } 885 }
815 # Update date of last hit 886 # Update date of last hit
816 $blocklist{$mip} = $mdate; 887 $blocklist{$mip} = $mdate;
888 }
889
890 # Separate check for DroneBL
891 if ($settings{"DRONEBL"} > 0 && $mtype > 0 && $cnt >= $settings{"DRONEBL_THRESHOLD"} && check_time3($mdate)) {
892 $dronebl{$mip}{"type"} = $mtype;
893 $dronebl{$mip}{"date"} = $mdate;
894 $dronebl{$mip}{"sent"} = 0 unless defined($dronebl{$mip}{"sent"});
817 } 895 }
818 } 896 }
819 897
820 898
821 ############################################################################# 899 #############################################################################