root/psad/tags/psad-2.1.2/fwcheck_psad.pl

Revision 2026, 17.5 kB (checked in by mbr, 2 years ago)

copyright date update

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1 #!/usr/bin/perl -w
2 #
3 ###############################################################################
4 #
5 # File: fwcheck_psad.pl (/usr/sbin/fwcheck_psad)
6 #
7 # Purpose: To parse the iptables ruleset on the underlying system to see if
8 #          iptables has been configured to log and block unwanted packets by
9 #          default.  This program is called by psad, but can also be executed
10 #          manually from the command line.
11 #
12 # Author: Michael Rash (mbr@cipherdyne.org)
13 #
14 # Credits: (see the CREDITS file bundled with the psad sources.)
15 #
16 # Copyright (C) 1999-2007 Michael Rash (mbr@cipherdyne.org)
17 #
18 # License (GNU Public License):
19 #
20 #    This program is distributed in the hope that it will be useful,
21 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
22 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23 #    GNU General Public License for more details.
24 #
25 #    You should have received a copy of the GNU General Public License
26 #    along with this program; if not, write to the Free Software
27 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
28 #    USA
29 #
30 ###############################################################################
31 #
32 # $Id$
33 #
34
35 use Getopt::Long 'GetOptions';
36 use strict;
37
38 ### default psad config file.
39 my $config_file  = '/etc/psad/psad.conf';
40
41 ### config hash
42 my %config = ();
43
44 ### commands hash
45 my %cmds;
46
47 ### fw search string array
48 my @fw_search = ();
49
50 my $help = 0;
51 my $fw_analyze = 0;
52 my $fw_file    = '';
53 my $fw_search_all = 1;
54 my $no_fw_search_all = 0;
55 my $psad_lib_dir = '';
56
57 &usage(1) unless (GetOptions(
58     'config=s'    => \$config_file, # Specify path to configuration file.
59     'fw-file=s'   => \$fw_file,     # Analyze ruleset contained within
60                                     # $fw_file instead of a running
61                                     # policy.
62     'fw-analyze'  => \$fw_analyze,  # Analyze the local iptables ruleset
63                                     # and exit.
64     'no-fw-search-all' => \$no_fw_search_all, # looking for specific log
65                                               # prefixes
66     'Lib-dir=s'   => \$psad_lib_dir,# Specify path to psad lib directory.
67     'help'        => \$help,        # Display help.
68 ));
69 &usage(0) if $help;
70
71 $fw_search_all = 0 if $no_fw_search_all;
72
73 ### Everthing after this point must be executed as root.
74 $< == 0 && $> == 0 or
75     die '[*] fwcheck_psad.pl: You must be root (or equivalent ',
76         "UID 0 account) to execute fwcheck_psad.pl!  Exiting.\n";
77
78 if ($fw_file) {
79     die "[*] iptables dump file: $fw_file does not exist."
80         unless -e $fw_file;
81 }
82
83 ### import psad.conf
84 &import_config($config_file);
85
86 ### import FW_MSG_SEARCH strings
87 &import_fw_search($config_file);
88
89 ### expand any embedded vars within config values
90 &expand_vars();
91
92 ### check to make sure the commands specified in the config section
93 ### are in the right place, and attempt to correct automatically if not.
94 &check_commands({});
95
96 ### import psad perl modules
97 &import_psad_perl_modules();
98
99 open FWCHECK, "> $config{'FW_CHECK_FILE'}" or die "[*] Could not ",
100     "open $config{'FW_CHECK_FILE'}: $!";
101
102 unless ($fw_search_all) {
103     print FWCHECK "[+] Available search strings in $config_file:\n\n";
104     print FWCHECK "        $_\n" for @fw_search;
105     print FWCHECK
106 "\n[+] Additional search strings can be added be specifying more\n",
107     "    FW_MSG_SEARCH lines in $config_file\n\n";
108 }
109
110 ### check the iptables policy
111 my $rv = &fw_check();
112
113 close FWCHECK;
114
115 exit $rv;
116
117 #========================== end main =========================
118
119 sub fw_check() {
120
121     ### only send a firewall config alert if we really need to.
122     my $send_alert = 0;
123
124     my $forward_chain_rv = 1;
125     my $input_chain_rv = &ipt_chk_chain('INPUT');
126
127     unless ($input_chain_rv) {
128         &print_fw_help('INPUT');
129         $send_alert = 1;
130     }
131
132     ### we don't always have more than one interface or forwarding
133     ### turned on, so we only check the FORWARD iptables chain if we
134     ### do and we have multiple interfaces on the box.
135     if (&check_forwarding()) {
136         $forward_chain_rv = &ipt_chk_chain('FORWARD');
137         unless ($forward_chain_rv) {
138             &print_fw_help('FORWARD');
139             $send_alert = 1;
140         }
141     }
142
143     if ($send_alert) {
144         unless ($fw_search_all) {
145             print FWCHECK
146 "\n[+] NOTE: IPTables::Parse does not yet parse user defined chains and so\n",
147 "    it is possible your firewall config is compatible with psad anyway.\n";
148         }
149
150         unless ($config{'ALERTING_METHODS'} =~ /no.?e?mail/i) {
151             &send_mail("[psad-status] firewall setup warning on " .
152                 "$config{'HOSTNAME'}!", $config{'FW_CHECK_FILE'},
153                 $config{'EMAIL_ADDRESSES'},
154                 $cmds{'mail'}
155             );
156         }
157         if ($fw_analyze) {
158             print "[-] Errors found in firewall config.\n";
159             print "    emailed to ",
160                 "$config{'EMAIL_ADDRESSES'}\n";
161         }
162     } else {
163         print FWCHECK
164 "[+] The iptables ruleset on $config{'HOSTNAME'} will log and block unwanted\n",
165 "    packets in both the INPUT and FORWARD chains.  Firewall config success!\n";
166
167         if ($fw_analyze) {
168             print "[+] Firewall config looks good.\n",
169                 "[+] Completed check of firewall ruleset.\n";
170         }
171     }
172     if ($fw_analyze) {
173         print "[+] Results in $config{'FW_CHECK_FILE'}\n",
174             "[+] Exiting.\n";
175     }
176     return $forward_chain_rv && $input_chain_rv;
177 }
178
179 sub print_fw_help() {
180     my $chain = shift;
181     print FWCHECK
182 "[-] You may just need to add a default logging rule to the $chain chain on\n",
183 "    $config{'HOSTNAME'}.  For more information, see the file \"FW_HELP\" in\n",
184 "    the psad sources directory or visit:\n\n",
185 "    http://www.cipherdyne.org/psad/docs/fwconfig.html\n\n";
186     return;
187 }
188
189 sub check_forwarding() {
190     ### check to see if there are multiple interfaces on the
191     ### machine and return false if no since the machine will
192     ### not be able to forward packets anyway (e.g. desktop
193     ### machines).  Also return false if forwarding is turned
194     ### off (we have to trust the machine config is as the
195     ### admin wants it).
196     my $forwarding;
197     if (-e $config{'PROC_FORWARD_FILE'}) {
198         open F, "< $config{'PROC_FORWARD_FILE'}"
199             or die "[*] Could not open $config{'PROC_FORWARD_FILE'}: $!";
200         $forwarding = <F>;
201         close F;
202         chomp $forwarding;
203         return 0 if $forwarding == 0;
204     } else {
205         die "[*] Make sure the path to the IP forwarding file correct.\n",
206             "    The PROC_FORWARD_FILE in $config_file points to\n",
207             "    $config{'PROC_FORWARD_FILE'}";
208     }
209     open IFC, "$cmds{'ifconfig'} -a |" or die "[*] Could not ",
210         "execute: $cmds{'ifconfig'} -a: $!";
211     my @if_out = <IFC>;
212     close IFC;
213     my $num_intf = 0;
214     for my $line (@if_out) {
215         if ($line =~ /inet\s+/i && $line !~ /127\.0\.0\.1/) {
216             $num_intf++;
217         }
218     }
219     if ($num_intf < 2) {
220         return 0;
221     }
222     return 1;
223 }
224
225 sub ipt_chk_chain() {
226     my $chain = shift;
227     my $rv = 1;
228
229     my $ipt = new IPTables::Parse 'iptables' => $cmds{'iptables'}
230         or die "[*] Could not acquite IPTables::Parse object: $!";
231
232     if ($fw_analyze) {
233         print "[+] Parsing iptables $chain chain rules.\n";
234     }
235
236     if ($fw_search_all) {
237         ### we are not looking for specific log
238         ### prefixes, but we need _some_ logging rule
239         my $ipt_log = $ipt->default_log('filter', $chain, $fw_file);
240         return 0 unless $ipt_log;
241         if (defined $ipt_log->{'all'}) {
242             ### found real default logging rule (assuming it is above a default
243             ### drop rule, which we are not actually checking here).
244             return 1;
245         } else {
246             my $log_protos    = '';
247             my $no_log_protos = '';
248             for my $proto qw(tcp udp icmp) {
249                 if (defined $ipt_log->{$proto}) {
250                     $log_protos .= "$proto/";
251                 } else {
252                     $no_log_protos .= "$proto/";
253                 }
254             }
255             $log_protos =~ s|/$||;
256             $no_log_protos =~ s|/$||;
257
258             if ($log_protos) {
259                 print FWCHECK
260 "[-] Your firewall config on $config{'HOSTNAME'} includes logging rules for\n",
261 "    $log_protos but not for $no_log_protos in the $chain chain.\n\n";
262                 return 0;
263             } else {
264                 print FWCHECK
265 "[-] Could not determine whether the iptables $chain chain is configured with\n",
266 "    a default logging rule on $config{'HOSTNAME'}.\n\n";
267                 return 0;
268             }
269         }
270     } else {
271         ### we are looking for specific log prefixes.
272         ### for now we are only looking at the filter table, so if
273         ### the iptables ruleset includes the log and drop rules in
274         ### a user defined chain then psad will not see this.
275         my $ld_hr = $ipt->default_drop('filter', $chain, $fw_file);
276
277         my $num_keys = 0;
278         if (defined $ld_hr and keys %$ld_hr) {
279             $num_keys++;
280             my @protos;
281             if (defined $ld_hr->{'all'}) {
282                 @protos = qw(all);
283             } else {
284                 @protos = qw(tcp udp icmp);
285             }
286             for my $proto (@protos) {
287                 my $str1;
288                 my $str2;
289                 if (! defined $ld_hr->{$proto}->{'LOG'}) {
290                     if ($proto eq 'all') {
291                         $str1 = 'for all protocols';
292                         $str2 = 'scans';
293                     } else {
294                         $str1 = "for the $proto protocol";
295                         $str2 = "$proto scans";
296                     }
297                     print FWCHECK
298 "[-] The $chain chain in the iptables ruleset on $config{'HOSTNAME'} does not\n",
299 "    appear to include a default LOG rule $str1.  psad will not be able to\n",
300 "    detect $str2 without such a rule.\n\n";
301
302                     $rv = 0;
303                 }
304                 if (defined $ld_hr->{$proto}->{'LOG'}->{'prefix'}) {
305                     my $found = 0;
306                     for my $fwstr (@fw_search) {
307                         $found = 1
308                             if $ld_hr->{$proto}->{'LOG'}->{'prefix'} =~ /$fwstr/;
309                     }
310                     unless ($found) {
311                         if ($proto eq 'all') {
312                             $str1 = "[-] The $chain chain in the iptables ruleset " .
313                             "on $config{'HOSTNAME'} includes a default\n    LOG rule for " .
314                             "all protocols,";
315                             $str2 = 'scans';
316                         } else {
317                             $str1 = "[-] The $chain chain in the iptables ruleset " .
318                             "on $config{'HOSTNAME'} inclues a default\n    LOG rule for " .
319                             "the $proto protocol,";
320                             $str2 = "$proto scans";
321                         }
322                         print FWCHECK
323 "$str1\n",
324 "    but the rule does not include one of the log prefixes mentioned above.\n",
325 "    It appears as though the log prefix is set to \"$ld_hr->{$proto}->{'LOG'}->{'prefix'}\"\n",
326 "    psad will not be able to detect $str2 without adding one of the above\n",
327 "    logging prefixes to the rule.\n\n";
328                         $rv = 0;
329                     }
330                 }
331                 if (! defined $ld_hr->{$proto}->{'DROP'}) {
332                     if ($proto eq 'all') {
333                         $str1 = "for all protocols";
334                     } else {
335                         $str1 = "for the $proto protocol";
336                     }
337                     print FWCHECK
338 "[-] The $chain chain in the iptables ruleset on $config{'HOSTNAME'} does not\n",
339 "    appear to include a default DROP rule $str1.\n\n";
340                     $rv = 0;
341                 }
342             }
343         }
344         ### make sure there was _something_ returned from the IPTables::Parse
345         ### module.
346         return 0 unless $num_keys > 0;
347     }
348     return $rv;
349 }
350
351 sub import_psad_perl_modules() {
352
353     my $mod_paths_ar = &get_psad_mod_paths();
354
355     if ($#$mod_paths_ar > -1) {  ### /usr/lib/psad/ exists
356         push @$mod_paths_ar, @INC;
357         splice @INC, 0, $#$mod_paths_ar+1, @$mod_paths_ar;
358     }
359
360     require IPTables::Parse;
361
362     return;
363 }
364
365 sub get_psad_mod_paths() {
366
367     my @paths = ();
368
369     $config{'PSAD_LIBS_DIR'} = $psad_lib_dir if $psad_lib_dir;
370
371     unless (-d $config{'PSAD_LIBS_DIR'}) {
372         my $dir_tmp = $config{'PSAD_LIBS_DIR'};
373         $dir_tmp =~ s|lib/|lib64/|;
374         if (-d $dir_tmp) {
375             $config{'PSAD_LIBS_DIR'} = $dir_tmp;
376         } else {
377             return [];
378         }
379     }
380
381     opendir D, $config{'PSAD_LIBS_DIR'}
382         or die "[*] Could not open $config{'PSAD_LIBS_DIR'}: $!";
383     my @dirs = readdir D;
384     closedir D;
385
386     push @paths, $config{'PSAD_LIBS_DIR'};
387
388     for my $dir (@dirs) {
389         ### get directories like "/usr/lib/psad/x86_64-linux"
390         next unless -d "$config{'PSAD_LIBS_DIR'}/$dir";
391         push @paths, "$config{'PSAD_LIBS_DIR'}/$dir"
392             if $dir =~ m|linux| or $dir =~ m|thread|;
393     }
394     return \@paths;
395 }
396
397 sub import_fw_search() {
398     my $config_file = shift;
399
400     open F, "< $config_file" or die "[*] Could not open fw search ",
401         "string file $config_file: $!";
402     my @lines = <F>;
403     close F;
404     for my $line (@lines) {
405         next unless $line =~ /\S/;
406         next if $line =~ /^\s*#/;
407         if ($line =~ /^\s*FW_MSG_SEARCH\s+(.*?);/) {
408             push @fw_search, $1;
409         }
410     }
411     return;
412 }
413
414 ### send mail message to all addresses contained in the
415 ### EMAIL_ADDRESSES variable within psad.conf ($addr_str).
416 ### TODO:  Would it be better to use Net::SMTP here?
417 sub send_mail() {
418     my ($subject, $body_file, $addr_str, $mailCmd) = @_;
419     open MAIL, "| $mailCmd -s \"$subject\" $addr_str > /dev/null" or die
420         "[*] Could not send mail: $mailCmd -s \"$subject\" $addr_str: $!";
421     if ($body_file) {
422         open F, "< $body_file" or die "[*] Could not open mail file: ",
423             "$body_file: $!";
424         my @lines = <F>;
425         close F;
426         print MAIL for @lines;
427     }
428     close MAIL;
429     return;
430 }
431
432 sub import_config() {
433     my $conf_file = shift;
434
435     open C, "< $conf_file" or die "[*] Could not open " .
436         "config file $conf_file: $!";
437     my @lines = <C>;
438     close C;
439     for my $line (@lines) {
440         chomp $line;
441         next if ($line =~ /^\s*#/);
442         if ($line =~ /^\s*(\S+)\s+(.*?)\;/) {
443             my $varname = $1;
444             my $val     = $2;
445             if ($val =~ m|/.+| && $varname =~ /^\s*(\S+)Cmd$/) {
446                 ### found a command
447                 $cmds{$1} = $val;
448             } else {
449                 $config{$varname} = $val;
450             }
451         }
452     }
453     return;
454 }
455
456 sub expand_vars() {
457     for my $hr (\%config, \%cmds) {
458         for my $var (keys %$hr) {
459             my $val = $hr->{$var};
460             die "[*] Multiple variable expansion not supported yet ",
461                 "(var $var)." if $val =~ m|\$.+\$|;
462             if ($val =~ m|\$(\w+)|) {
463                 my $sub_var = $1;
464                 die "[*] sub-ver $sub_var not allowed within same ",
465                     "variable $var" if $sub_var eq $var;
466                 if (defined $config{$sub_var}) {
467                     $val =~ s|\$$sub_var|$config{$sub_var}|;
468                     $hr->{$var} = $val;
469                 } else {
470                     die "[*] sub-var \"$sub_var\" not defined in ",
471                         "config for var: $var."
472                 }
473             }
474         }
475     }
476     return;
477 }
478
479 ### check paths to commands and attempt to correct if any are wrong.
480 sub check_commands() {
481     my $exceptions_hr = shift;
482     my $caller = $0;
483     my @path = qw(
484         /bin
485         /sbin
486         /usr/bin
487         /usr/sbin
488         /usr/local/bin
489         /usr/local/sbin
490     );
491     CMD: for my $cmd (keys %cmds) {
492         ### both mail and sendmail are special cases, mail is not required
493         ### if "nomail" is set in REPORT_METHOD, and sendmail is only
494         ### required if DShield alerting is enabled and a DShield user
495         ### email is set.
496         next if $cmd =~ /mail/i;
497         next if $cmd eq 'wget'### only used in --sig-update mode
498         unless (-x $cmds{$cmd}) {
499             my $found = 0;
500             PATH: for my $dir (@path) {
501                 if (-x "${dir}/${cmd}") {
502                     $cmds{$cmd} = "${dir}/${cmd}";
503                     $found = 1;
504                     last PATH;
505                 }
506             }
507             unless ($found) {
508                 unless (defined $exceptions_hr->{$cmd}) {
509                     die "[*] ($caller): Could not find $cmd ",
510                         "anywhere!!!\n    Please edit the config section ",
511                          "to include the path to $cmd.";
512                 }
513             }
514         }
515         unless (-x $cmds{$cmd}) {
516             unless (defined $exceptions_hr->{$cmd}) {
517                 die "[*] ($caller): $cmd is located at ",
518                     "$cmds{$cmd}, but is not executable\n",
519                     "    by uid: $<";
520             }
521         }
522     }
523     return;
524 }
525
526 sub usage() {
527     my $exitcode = shift;
528     print <<_HELP_;
529
530 Options:
531     --config <config_file>            - Specify path to configuration
532                                         file.
533     --fw-file    <fw_file>            - Analyze ruleset contained within
534                                         fw_file instead of a running
535                                         policy.
536     --fw-analyze                      - Analyze the local iptables
537                                         ruleset and exit.
538     --no-fw-search-all                - looking for specific log
539                                         prefixes
540     --help                            - Display help.
541
542 _HELP_
543     exit $exitcode;
544 }
Note: See TracBrowser for help on using the browser.