comparison maltfilter @ 60:38885f5f34f6

Added evidence gathering functionality.
author Matti Hamalainen <ccr@tnsp.org>
date Mon, 17 Aug 2009 08:22:24 +0300
parents 69c39b5c6277
children 924720517cf9
comparison
equal deleted inserted replaced
59:69c39b5c6277 60:38885f5f34f6
7 # 7 #
8 ############################################################################# 8 #############################################################################
9 use strict; 9 use strict;
10 use Date::Parse; 10 use Date::Parse;
11 use Net::IP; 11 use Net::IP;
12 12 use Net::DNS;
13 my $progversion = "0.13.1"; 13 use LWP::UserAgent;
14
15 my $progversion = "0.14.0";
14 my $progbanner = 16 my $progbanner =
15 "Malicious Attack Livid Termination Filter daemon (maltfilter) v$progversion\n". 17 "Malicious Attack Livid Termination Filter daemon (maltfilter) v$progversion\n".
16 "Programmed by Matti 'ccr' Hamalainen <ccr\@tnsp.org>\n". 18 "Programmed by Matti 'ccr' Hamalainen <ccr\@tnsp.org>\n".
17 "(C) Copyright 2009 Tecnic Software productions (TNSP)\n"; 19 "(C) Copyright 2009 Tecnic Software productions (TNSP)\n";
18 20
27 "TRESHOLD" => 3, 29 "TRESHOLD" => 3,
28 "ACTION" => "DROP", 30 "ACTION" => "DROP",
29 "LOGFILE" => "", 31 "LOGFILE" => "",
30 "IPTABLES" => "/sbin/iptables", 32 "IPTABLES" => "/sbin/iptables",
31 33
32 "STATUS_FILE_PLAIN" => "", 34 "FULL_TIME" => 1,
33 "STATUS_FILE_HTML" => "", 35 "STATUS_FILE_PLAIN" => "",
34 "STATUS_FILE_CSS" => "", 36 "STATUS_FILE_HTML" => "",
35 37 "STATUS_FILE_CSS" => "",
36 "WHOIS_URL" => "http://whois.domaintools.com/", 38 "WHOIS_URL" => "http://whois.domaintools.com/",
37 39
38 "CHK_SSHD" => 1, 40 "CHK_SSHD" => 1,
39 "CHK_KNOWN_CGI" => 1, 41 "CHK_KNOWN_CGI" => 1,
40 "CHK_PHP_XSS" => 1, 42 "CHK_PHP_XSS" => 1,
41 "CHK_PROXY_SCAN" => 1, 43 "CHK_PROXY_SCAN" => 1,
42 "CHK_ROOT_SSH_PWD" => 0, 44 "CHK_ROOT_SSH_PWD" => 0,
43 "CHK_SYSACCT_SSH_PWD" => 0, 45 "CHK_SYSACCT_SSH_PWD" => 0,
44 "CHK_GOOD_HOSTS" => "", 46 "CHK_GOOD_HOSTS" => "",
45 47
48 "PASSWD" => "/etc/passwd",
46 "SYSACCT_MIN_UID" => 1, 49 "SYSACCT_MIN_UID" => 1,
47 "SYSACCT_MAX_UID" => 100, 50 "SYSACCT_MAX_UID" => 100,
48 51
49 "FULL_TIME" => 1, 52 "EVIDENCE" => 0,
50 53 "EVIDENCE_DIR" => "",
51 "PASSWD" => "/etc/passwd",
52 ); 54 );
53 55
54 my @noblock_ips_def = ( 56 my @noblock_ips_def = (
55 "127.0.0.1", 57 "127.0.0.1",
56 ); 58 );
102 my $merr = $3; 104 my $merr = $3;
103 105
104 # (3.1) Simple match for generic PHP XSS vulnerability scans 106 # (3.1) Simple match for generic PHP XSS vulnerability scans
105 if ($merr =~ /\.php\?\S*?=http:\/\/([^\/]+)/) { 107 if ($merr =~ /\.php\?\S*?=http:\/\/([^\/]+)/) {
106 if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) { 108 if (!check_hosts($settings{"CHK_GOOD_HOSTS"}, $1)) {
109 if ($merr =~ /\.php\?\S*?=(http:\/\/[^\&\?]+\??)/) {
110 check_add_evidence($mip, $1);
111 }
107 check_add_hit($mip, $mdate, "PHP XSS", $merr, $settings{"CHK_PHP_XSS"}); 112 check_add_hit($mip, $mdate, "PHP XSS", $merr, $settings{"CHK_PHP_XSS"});
108 } 113 }
109 } 114 }
110 # (3.2) Try to match proxy scanning attempts 115 # (3.2) Try to match proxy scanning attempts
111 elsif ($merr =~ /^http:\/\/([^\/]+)/) { 116 elsif ($merr =~ /^http:\/\/([^\/]+)/) {
407 printP($m, $f, bb($m).$nkeys.eb($m)." entries total, ".bb($m).$nhits.eb($m)." hits total.\n"); 412 printP($m, $f, bb($m).$nkeys.eb($m)." entries total, ".bb($m).$nhits.eb($m)." hits total.\n");
408 } 413 }
409 414
410 sub cmp_hits($$$) 415 sub cmp_hits($$$)
411 { 416 {
412 return $_[0]->{$_[2]}{"hits"} <=> $_[0]->{$_[1]}{"hits"}; 417 my $s1 = $_[0]->{$_[1]};
418 my $s2 = $_[0]->{$_[2]};
419
420 return -1 if ($s2->{"date2"} < $s1->{"date2"});
421 return 1 if ($s2->{"date2"} > $s1->{"date2"});
422 return $s2->{"hits"} <=> $s1->{"hits"};
413 } 423 }
414 424
415 sub get_period($) 425 sub get_period($)
416 { 426 {
417 my ($str, $r, $k); 427 my ($str, $r, $k);
487 close(STATUS); 497 close(STATUS);
488 } 498 }
489 499
490 500
491 ############################################################################# 501 #############################################################################
502 ### Evidence gathering
503 #############################################################################
504 my %evidence = ();
505
506 sub check_add_evidence($$)
507 {
508 my ($mip, $mdata) = @_;
509
510 return unless ($settings{"EVIDENCE"});
511
512 my $tmp = $mdata;
513 $tmp =~ s/http:\/\///;
514 $tmp =~ s/^\.+/_/;
515 $tmp =~ s/[^A-Za-z0-9:\.]/_/g;
516
517 $evidence{$mdata}{"coll"} = $tmp;
518 $evidence{$mdata}{"hosts"}{$mip} = 1;
519 }
520
521 sub http_fetch($$)
522 {
523 my $tmp = LWP::UserAgent->new;
524 $tmp->agent("-");
525 $tmp->timeout(10);
526 $tmp->default_headers->referer($_[1]);
527 my $req = HTTP::Request->new(GET => $_[0]);
528 return $tmp->request($req);
529 }
530
531 sub gather_evidence
532 {
533 my $dns = Net::DNS::Resolver->new;
534 my $base = $settings{"EVIDENCE_DIR"};
535
536 return unless ($settings{"EVIDENCE"});
537
538 mdie("Evidence directory '$base' has disappeared.\n") unless (-e $base);
539
540 foreach my $url (keys %evidence) {
541 if (!$evidence{$url}{"done"}) {
542 my $filename = $base."/".$evidence{$url}{"coll"}.".data";
543 my $filename2 = $base."/".$evidence{$url}{"coll"}.".hosts";
544 my $code = 0;
545 my $message = "";
546
547 # Get data contents only once
548 if (! -e $filename) {
549 mlog(1, "Fetching evidence for $url\n");
550 my $res = http_fetch($url, "");
551 $code = $res->code;
552 $message = $res->message;
553 open(FILE, ">:raw", $filename) or mdie("Could not open '$filename' for writing.\n");
554 binmode(FILE, ":raw");
555 if ($res->code >= 200 && $res->code <= 201) {
556 print FILE $res->content;
557 } else {
558 print FILE "[$code] $message\n";
559 }
560 close(FILE);
561 }
562
563 # Check if we are appending hosts to existing data
564 if (-e $filename2) {
565 open(FILE, "<", $filename2) or mdie("Could not open '$filename2' for reading.\n");
566 while (<FILE>) {
567 if (/^(\d+\.\d+\.\d+\.\d+) *\|/) {
568 if (defined($evidence{$url}{"hosts"}{$1})) {
569 delete($evidence{$url}{"hosts"}{$1});
570 }
571 }
572 }
573 close(FILE);
574 open(FILE, ">>", $filename2) or mdie("Could not open '$filename2' for appending.\n");
575 } else {
576 open(FILE, ">", $filename2) or mdie("Could not open '$filename2' for writing.\n");
577 print FILE "# HTTP request result: [$code] $message\n";
578 print FILE "# Hosts which requested $url\n\n";
579 }
580
581 foreach my $host (sort keys %{$evidence{$url}{"hosts"}}) {
582 print "lol: $host\n";
583 my $query = $dns->search($host);
584 my @names = ();
585 undef(@names);
586 if ($query) {
587 foreach my $rr ($query->answer) {
588 push(@names, $rr->{"ptrdname"}) if defined($rr->{"ptrdname"});
589 }
590 }
591 printf FILE "%-15s | %s\n", $host, join(" | ", @names);
592 }
593 close(FILE);
594
595 delete($evidence{$url});
596
597 return unless $reportmode;
598 }
599 }
600 }
601
602
603 #############################################################################
492 ### Entry management / handling functions 604 ### Entry management / handling functions
493 ############################################################################# 605 #############################################################################
494 ### Check if given IP or host exists in array 606 ### Check if given IP or host exists in array
495 sub check_hosts_array($$) 607 sub check_hosts_array($$)
496 { 608 {
531 ### Get current Netfilter INPUT table entries that match 643 ### Get current Netfilter INPUT table entries that match
532 ### entry types we manage, e.g. blocklist 644 ### entry types we manage, e.g. blocklist
533 sub update_blocklist($) 645 sub update_blocklist($)
534 { 646 {
535 # NOTICE: argument not used now 647 # NOTICE: argument not used now
648 my $first = $_[0];
536 649
537 $ENV{"PATH"} = ""; 650 $ENV{"PATH"} = "";
538 open(STATUS, $settings{"IPTABLES"}." -v -n -L INPUT |") or 651 open(STATUS, $settings{"IPTABLES"}." -v -n -L INPUT |") or
539 mdie("Could not execute ".$settings{"IPTABLES"}."\n"); 652 mdie("Could not execute ".$settings{"IPTABLES"}."\n");
540 my %newlist = (); 653 my %newlist = ();
543 chomp; 656 chomp;
544 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*$/) { 657 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*$/) {
545 my $mip = $2; 658 my $mip = $2;
546 my $mdate = time(); 659 my $mdate = time();
547 if (!defined($blocklist{$mip})) { 660 if (!defined($blocklist{$mip})) {
548 mlog(2, "* $mip appeared in iptables.\n"); 661 mlog(2, "* $mip appeared in iptables.\n") if ($first > 0);
549 $blocklist{$2} = $mdate; 662 $blocklist{$2} = $mdate;
550 } 663 }
551 $newlist{$2} = $mdate; 664 $newlist{$2} = $mdate;
552 update_entry(\%statlist, $mip, -1, "IPTABLES", "", 0); 665 update_entry(\%statlist, $mip, -1, "IPTABLES", "", 0);
553 } 666 }
832 $counter = 0; 945 $counter = 0;
833 update_blocklist(time()); 946 update_blocklist(time());
834 weed_entries(); 947 weed_entries();
835 generate_status($settings{"STATUS_FILE_PLAIN"}, 0); 948 generate_status($settings{"STATUS_FILE_PLAIN"}, 0);
836 generate_status($settings{"STATUS_FILE_HTML"}, 1); 949 generate_status($settings{"STATUS_FILE_HTML"}, 1);
837 } 950 gather_evidence();
838 sleep(5); 951 }
952 sleep(2);
839 foreach my $filename (keys %filehandles) { 953 foreach my $filename (keys %filehandles) {
840 seek($filehandles{$filename}, $filepos{$filename}, 0); 954 seek($filehandles{$filename}, $filepos{$filename}, 0);
841 } 955 }
842 } 956 }
843 } 957 }
930 # Test existence of iptables 1044 # Test existence of iptables
931 if (! -e $settings{"IPTABLES"} || ! -x $settings{"IPTABLES"}) { 1045 if (! -e $settings{"IPTABLES"} || ! -x $settings{"IPTABLES"}) {
932 mdie("iptables binary does not exist or is not executable: ".$settings{"IPTABLES"}."\n"); 1046 mdie("iptables binary does not exist or is not executable: ".$settings{"IPTABLES"}."\n");
933 } 1047 }
934 1048
1049 # Check evidence settings
1050 if ($settings{"EVIDENCE"}) {
1051 my $base = $settings{"EVIDENCE_DIR"};
1052 mdie("Evidence directory (EVIDENCE_DIR) not set in configuration.\n") if ($base eq "");
1053 mdie("Evidence directory '$base' does not exist.\n") unless (-e $base);
1054 mdie("Path '$base' is not a directory.\n") unless (-d $base);
1055 mdie("Evidence directory '$base' is not writable by euid.\n") unless (-w $base);
1056 }
1057
935 # Check settings 1058 # Check settings
936 mdie("SYSACCT_MIN_UID must be >= 1.\n") unless ($settings{"SYSACCT_MIN_UID"} >= 1); 1059 mdie("SYSACCT_MIN_UID must be >= 1.\n") unless ($settings{"SYSACCT_MIN_UID"} >= 1);
937 mdie("SYSACCT_MAX_UID must be >= SYSACCT_MIN_UID.\n") unless ($settings{"SYSACCT_MAX_UID"} >= $settings{"SYSACCT_MIN_UID"}); 1060 mdie("SYSACCT_MAX_UID must be >= SYSACCT_MIN_UID.\n") unless ($settings{"SYSACCT_MAX_UID"} >= $settings{"SYSACCT_MIN_UID"});
938 1061
939 open(PASSWD, "<", $settings{"PASSWD"}) or mdie("Could not open '".$settings{"PASSWD"}."' for reading!\n"); 1062 open(PASSWD, "<", $settings{"PASSWD"}) or mdie("Could not open '".$settings{"PASSWD"}."' for reading!\n");
1003 if ($settings{"DRY_RUN"}) { 1126 if ($settings{"DRY_RUN"}) {
1004 if ($reportmode) { 1127 if ($reportmode) {
1005 mlog(-1, "Outputting report files.\n"); 1128 mlog(-1, "Outputting report files.\n");
1006 generate_status($settings{"STATUS_FILE_PLAIN"}, 0); 1129 generate_status($settings{"STATUS_FILE_PLAIN"}, 0);
1007 generate_status($settings{"STATUS_FILE_HTML"}, 1); 1130 generate_status($settings{"STATUS_FILE_HTML"}, 1);
1131 gather_evidence();
1008 malt_cleanup(); 1132 malt_cleanup();
1009 } else { 1133 } else {
1010 malt_scan(); 1134 malt_scan();
1011 malt_cleanup(); 1135 malt_cleanup();
1012 } 1136 }