root/fwknop/tags/fwknop-1.8.3/fwknopd

Revision 809, 139.2 kB (checked in by mbr, 1 year ago)

fwknop-1.8.3 release

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Revision
Line 
1 #!/usr/bin/perl -w
2 #
3 #############################################################################
4 #
5 # File: fwknopd (/usr/sbin/fwknopd)
6 #
7 # URL: http://www.cipherdyne.org/fwknop
8 #
9 # Purpose: fwknopd implements the server portion of an authorization scheme
10 #          known as Single Packet Authorization (SPA) that requires only a
11 #          single encrypted packet to communicate various pieces of
12 #          information including desired access through an iptables policy
13 #          and/or specific commands to execute on the target system.  The
14 #          main application of this program is to protect services such as
15 #          SSH with an additional layer of security in order to make the
16 #          exploitation of vulnerabilities (both 0-day and unpatched code)
17 #          much more difficult.  For more information, see the fwknop(8) man
18 #          page.
19 #
20 # Author: Michael Rash (mbr@cipherdyne.org)
21 #
22 # Version: 1.8.3
23 #
24 # Copyright (C) 2004-2007 Michael Rash (mbr@cipherdyne.org)
25 #
26 # License (GNU Public License):
27 #
28 #    This program is distributed in the hope that it will be useful,
29 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
30 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
31 #    GNU General Public License for more details.
32 #
33 #    You should have received a copy of the GNU General Public License
34 #    along with this program; if not, write to the Free Software
35 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
36 #    USA
37 #
38 #############################################################################
39 #
40 # $Id: fwknopd 583 2006-11-04 20:43:01Z mbr $
41 #
42
43 use lib '/usr/lib/fwknop';
44 use Crypt::CBC;
45 use Unix::Syslog qw(:subs :macros);
46 use Net::IPv4Addr qw(ipv4_in_network);
47 use Net::Pcap;
48 use NetPacket::IP;
49 use NetPacket::UDP;
50 use NetPacket::TCP;
51 use NetPacket::ICMP;
52 use NetPacket::Ethernet;
53 use Digest::MD5 'md5_base64';
54 use IO::Socket;
55 use IO::Handle;
56 use MIME::Base64;
57 use Data::Dumper;
58 use POSIX;
59 use Getopt::Long;
60 use strict;
61
62 my $config_file = '/etc/fwknop/fwknop.conf';
63
64 my $version = '1.8.3';
65 my $revision_svn = '$Revision$';
66 my $rev_num = '1';
67 ($rev_num) = $revision_svn =~ m|\$Rev.*:\s+(\S+)|;
68
69 my %config     = ();
70 my %cmds       = ();
71 my %p0f_sigs   = ();
72 my %p0f        = ();
73 my @access     = ();
74 my @ipt_config = ();
75 my %fw_access  = ();
76 my %ip_sequences = ();
77 my %md5_msg_store = ();
78
79 my $os_fprint_only = 0;
80 my $print_version  = 0;
81 my $print_help     = 0;
82 my $kill           = 0;
83 my $restart        = 0;
84 my $status         = 0;
85 my $debug          = 0;
86 my $fw_list        = 0;
87 my $ipt_flush      = 0;
88 my $verbose        = 0;
89 my $use_gpg        = 0;
90 my $os_ipt_log     = '';
91 my $cmdline_intf   = '';
92 my $warn_msg       = '';
93 my $die_msg        = '';
94 my $err_wait_timer = 30;  ### seconds
95 my $gpg_agent_info = '';
96 my $skipped_first_loop = 0;
97 my $pcap_sleep_interval = 1;  ### seconds
98 my $imported_iptables_modules = 0;
99 my $include_all_config_data   = 0;
100
101 ### mode numbers
102 my $SPA_COMMAND_MODE = 0;
103 my $SPA_ACCESS_MODE  = 1;  ### default
104
105 ### default time values
106 my $knock_interval    = 60;
107 my $fw_access_timeout = 300;
108
109 my $enc_port_offset   = 61000;  ### default offset
110 my $enc_key           = '';
111 my $enc_alg           = 'Rijndael';
112 my $enc_blocksize     = 32;
113
114 ### there is a constant "RIJNDAEL_KEYSIZE" in the Crypt::Rijndael sources, but
115 ### it is not used; a 16 byte key size is fine.
116 my $enc_keysize       = 16;
117
118 my $ALG_RIJNDAEL = 1;
119 my $ALG_GNUPG    = 2;
120
121 my $PCAP      = 0;
122 my $FILE_PCAP = 1;
123 my $ULOG_PCAP = 2;
124 my $SHARED_SEQUENCE  = 3;
125 my $ENCRYPT_SEQUENCE = 4;
126
127 ### logr constants
128 my $SEND_MAIL = 1;
129 my $NO_MAIL   = 0;
130
131 ### packet counters
132 my $tcp_ctr  = 0;
133 my $udp_ctr  = 0;
134 my $icmp_ctr = 0;
135
136 ### tcp option types
137 my $tcp_nop_type       = 1;
138 my $tcp_mss_type       = 2;
139 my $tcp_win_scale_type = 3;
140 my $tcp_sack_type      = 4;
141 my $tcp_timestamp_type = 8;
142
143 my %tcp_p0f_opt_types = (
144     'N' => $tcp_nop_type,
145     'M' => $tcp_mss_type,
146     'W' => $tcp_win_scale_type,
147     'S' => $tcp_sack_type,
148     'T' => $tcp_timestamp_type
149 );
150
151 my %access_keys = (
152     'SOURCE' => '',
153     'TYPE'   => '',
154     'KEY'    => '',
155     'OPEN_PORTS'     => '',
156     'GPG_REMOTE_ID'  => '',
157     'GPG_DECRYPT_ID' => '',
158     'GPG_DECRYPT_PW' => '',
159     'GPG_HOME_DIR'   => '',
160     'ULOG_PCAP'      => '',
161     'FILE_PCAP'      => '',
162     'DATA_COLLECT_MODE' => '',
163     'ENCRYPT_SEQUENCE'  => '',
164     'SHARED_SEQUENCE'   => '',
165     'PORT_OFFSET'       => '',
166     'REQUIRE_AUTH_METHOD' => '',
167     'SHADOW_FILE'    => '',
168     'KNOCK_INTERVAL' => '',
169     'KNOCK_LIMIT'    => '',
170     'PERMIT_CLIENT_PORTS' => '',
171     'ENABLE_CMD_EXEC'     => '',
172     'DISABLE_FW_ACCESS'   => '',
173     'REQUIRE_SOURCE_ADDRESS' => '',
174     'CMD_REGEX'         => '',
175     'FW_ACCESS_TIMEOUT' => '',
176     'REQUIRE_USERNAME'  => '',
177     'MIN_TIME_DIFF' => '',
178     'MAX_TIME_DIFF' => '',
179     'RESTRICT_INTF' => '',
180 );
181
182 my $ip_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|;
183
184 my @args_cp = @ARGV;
185
186 ### run GetOpt() to get comand line args
187 &handle_command_line();
188
189 &usage(0) if $print_help;
190
191 if ($print_version) {
192     print "[+] fwknopd v$version (file revision: $rev_num)\n",
193         "      by Michael Rash <mbr\@cipherdyne.org>\n";
194     exit 0;
195 }
196
197 if ($os_fprint_only) {
198     print "[+] Entering OS fingerprinting mode.\n";
199 }
200
201 print STDERR "[+] ** Starting fwknopd (debug mode) **\n" if $debug;
202
203 ### setup to run
204 &fwknop_init();
205
206 if ($config{'AUTH_MODE'} eq 'KNOCK' or $os_fprint_only) {
207
208     ### we are running in traditional port knocking mode
209     &knock_loop();
210
211 } elsif ($config{'AUTH_MODE'} eq 'FILE_PCAP'
212         or $config{'AUTH_MODE'} eq 'ULOG_PCAP'
213         or $config{'AUTH_MODE'} eq 'PCAP') {
214
215     ### we are parsing the pcap file created by the ulogd pcap
216     ### writer, or in sniffing mode against an interface
217     &pcap_loop();
218 }
219 exit 0;
220 #============================ end main ==============================
221
222 sub pcap_loop() {
223
224     ### we use both a size and an inode check in the FILE_PCAP and
225     ### ULOG_PCAP modes to check if the file has been rotated
226     my $pcap_file_size  = 0;
227     my $pcap_file_inode = 0;
228
229     ### get pcap opject
230     my $pcap_t = &get_pcap_obj();
231
232     if ($config{'AUTH_MODE'} eq 'FILE_PCAP'
233             or $config{'AUTH_MODE'} eq 'ULOG_PCAP') {
234         ### get file size (we don't need a -e check here because
235         ### this is handled in get_pcap_obj()).
236         $pcap_file_size = -s $config{'PCAP_PKT_FILE'};
237
238         ### get inode associated with the sniffing file
239         $pcap_file_inode = (stat($config{'PCAP_PKT_FILE'}))[1];
240     }
241     print STDERR "[+] pcap_loop()\n" if $debug;
242
243     my $check_file_ctr = 0;
244
245     for (;;) {
246
247         Net::Pcap::loop($pcap_t, 1, \&pcap_process_pkt, 'fwknop_tag');
248
249         if ($config{'AUTH_MODE'} eq 'FILE_PCAP'
250                 or $config{'AUTH_MODE'} eq 'ULOG_PCAP') {
251
252             ### check to see if the pcap file has been rotated (we need to
253             ### close and re-open)
254             if ($check_file_ctr == 10) {
255                 if (-e $config{'PCAP_PKT_FILE'}) {
256                     my $size_tmp  = -s $config{'PCAP_PKT_FILE'};
257                     my $inode_tmp = (stat($config{'PCAP_PKT_FILE'}))[1];
258                     if ($inode_tmp != $pcap_file_inode
259                             or $size_tmp < $pcap_file_size) {
260
261                         ### the file was rotated or shrank, so get new
262                         ### pcap_t object
263                         Net::Pcap::close($pcap_t);
264
265                         &logr('[+]', "pcap file $config{'PCAP_PKT_FILE'} " .
266                             "shrank or was rotated, so re-opening", $NO_MAIL);
267                         $pcap_t = &get_pcap_obj();
268
269                         ### set file size and inode
270                         $pcap_file_size  = $size_tmp;
271                         $pcap_file_inode = $inode_tmp;
272                     }
273                 } else {
274                     Net::Pcap::close($pcap_t);
275                     &logr('[+]', "pcap file $config{'PCAP_PKT_FILE'} " .
276                         "was rotated, so re-opening", $NO_MAIL);
277                     $pcap_t = &get_pcap_obj();
278
279                     ### set file size and inode
280                     $pcap_file_size  = -s $config{'PCAP_PKT_FILE'};
281                     $pcap_file_inode = (stat($config{'PCAP_PKT_FILE'}))[1];
282                 }
283                 $check_file_ctr = 0;
284             }
285             $check_file_ctr++;
286
287             ### always check to see if we need to timeout access for IPs
288             ### (note that AUTH_MODE set to PCAP, knoptm will timeout access;
289             ### knoptm is not run in either the FILE_PCAP or ULOG_PCAP
290             ### modes).
291             &timeout_access();
292         }
293
294         sleep $pcap_sleep_interval;
295     }
296
297     Net::Pcap::close($pcap_t);
298
299     return;
300 }
301
302 sub pcap_process_pkt() {
303     my ($tag, $hdr, $pkt) = @_;
304
305     &write_die_msg() if $die_msg;
306     &write_warn_msg() if $warn_msg;
307
308     return unless $tag eq 'fwknop_tag';
309     return unless defined $hdr;
310     return unless defined $pkt;
311
312     my $ether_data = '';
313     my $ip         = '';
314     my $src_ip     = '';
315     my $proto      = '';
316     my $transport_obj = '';
317
318     if ($config{'AUTH_MODE'} eq 'ULOG_PCAP') {
319         ### The ulogd pcap writer does not include link layer information
320         $ip = NetPacket::IP->decode($pkt) or return;
321     } else {
322         $ether_data = NetPacket::Ethernet::strip($pkt) or return;
323         $ip = NetPacket::IP->decode($ether_data) or return;
324     }
325
326     ### get the source IP address
327     $src_ip = $ip->{'src_ip'} or return;
328
329     ### get the protocol
330     $proto = $ip->{'proto'} or return;
331
332     if ($proto == 1) {
333         $transport_obj = NetPacket::ICMP->decode($ip->{'data'});
334     } elsif ($proto == 6) {
335         $transport_obj = NetPacket::TCP->decode($ip->{'data'});
336     } elsif ($proto == 17) {
337         $transport_obj = NetPacket::UDP->decode($ip->{'data'});
338     } else {
339         return;
340     }
341
342     print STDERR "[+] Received packet ***[" .
343         localtime() . "]*** (" if $debug;
344
345     ### make sure we have _some_ data in the packet; in practice
346     ### any valid SPA message will be longer than 10 bytes, but this
347     ### check is better than nothing
348     return unless defined $transport_obj->{'data'};
349
350     my $enc_msg_len = 0;
351     $enc_msg_len = length($transport_obj->{'data'});
352     if (10 < $enc_msg_len and $enc_msg_len < 1500) {
353         print STDERR "$enc_msg_len bytes)\n" if $debug;
354     } else {
355         print STDERR "$enc_msg_len bytes, not attempting decrypt)\n"
356             if $debug;
357         return;
358     }
359
360     if ($debug) {
361         ### make sure not to print non-printable stuff
362         my $data_tmp = $transport_obj->{'data'};
363         $data_tmp =~ s/[^\x20-\x7e]/NA/g;
364         print STDERR "[+] Raw packet data (single line): $data_tmp\n";
365
366         ### print packet data out in tcpdump -X format
367         print STDERR "    Raw packet data (hex dump, minus packet headers):\n";
368         &hex_dump($transport_obj->{'data'}) if $verbose;
369     }
370
371     ### see if this packet is worthy of getting access through
372     ### the firewall
373     &SPA_check_grant_access($src_ip, $enc_msg_len, $transport_obj->{'data'});
374
375     &write_die_msg() if $die_msg;
376     &write_warn_msg() if $warn_msg;
377
378     return;
379 }
380
381 sub SPA_check_grant_access() {
382     my ($src_ip, $enc_msg_len, $pkt_data) = @_;
383
384     ### first check to see if we have any matching access directives
385     ### (in access.conf) for $src_ip, and if not we will do _nothing_
386     ### with this packet.
387     my $access_nums_aref = &check_src($src_ip);
388
389     unless ($access_nums_aref) {
390         print STDERR "[-] Packet from $src_ip did not match any ",
391               "SOURCE blocks in $config{'ACCESS_CONF'}\n" if $debug;
392         return;
393     }
394
395     ### See if the packet qualifies for any access
396     SOURCE: for my $num (@$access_nums_aref) {
397         my $access_hr = $access[$num];
398
399         next SOURCE unless $access_hr->{'DATA_COLLECT_MODE'} == $PCAP
400             or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP
401             or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP;
402
403         &dump_access($access_hr, $num) if $debug and $verbose;
404
405         ### keep track of which source block we are dealing with from
406         ### access.conf
407         my $source_block_num = $access_hr->{'block_num'};
408
409         ### see if we can decrypt and base64-decode
410         my ($decrypt_rv, $decrypted_msg, $gpg_sign_id, $decrypt_algo)
411             = &SPA_decrypt($pkt_data, $enc_msg_len, $access_hr);
412         next SOURCE unless $decrypt_rv;
413
414         ### check for replay attacks
415         my ($md5sum_rv, $md5sum)
416             = &check_replay_attack($decrypted_msg, $src_ip);
417         return if $md5sum_rv;
418
419         ### see if we have a syntactically valid message
420         my ($validate_rv, $msg_href) = &pcap_validate_msg(
421             $decrypted_msg, $source_block_num, $access_hr);
422         if ($debug and not $validate_rv) {
423             print STDERR "[-] Decrypted message does not ",
424                 "conform to a valid SPA packet.\n";
425         }
426         next SOURCE unless $validate_rv;
427
428         ### check to see if client side time stamp is too old
429         my $time_check_rv = &SPA_check_packet_age($msg_href->{'remote_time'});
430         next SOURCE unless $time_check_rv;
431
432         ### dump packet to stderr for debugging purposes
433         &SPA_dump_packet($msg_href) if $debug;
434
435         ### check username
436         next SOURCE unless &SPA_check_user($access_hr, $src_ip, $msg_href);
437
438         ### check authentication method
439         next SOURCE unless &SPA_check_auth_method(
440             $access_hr, $src_ip, $msg_href);
441
442         if ($msg_href->{'action_type'} == $SPA_ACCESS_MODE) {
443             if (&SPA_access($msg_href, $src_ip, $decrypt_algo,
444                     $gpg_sign_id, $md5sum, $access_hr)) {
445                 last SOURCE;
446             } else {
447                 next SOURCE;
448             }
449         } elsif ($msg_href->{'action_type'} == $SPA_COMMAND_MODE) {
450             if (&SPA_cmd($msg_href, $src_ip, $decrypt_algo,
451                     $gpg_sign_id, $md5sum, $access_hr)) {
452                 last SOURCE;
453             } else {
454                 next SOURCE;
455             }
456         }
457     }
458     return;
459 }
460
461 sub SPA_decrypt() {
462     my ($pkt_data, $enc_msg_len, $access_hr) = @_;
463
464     my $decrypted_msg = '';
465     my $decrypt_algo  = $ALG_RIJNDAEL;
466     my $gpg_sign_id   = '';
467     my $decrypt_rv    = 0;
468
469     if ($enc_msg_len > $config{'MIN_GNUPG_MSG_SIZE'}
470             and defined $access_hr->{'GPG_REMOTE_ID'}) {
471         ### attempt GPG decrypt (only if the length of the encrypted
472         ### payload is greater than the minimum size for an SPA message
473         ### encrypted with GnuPG; even encrypting a single byte of data
474         ### with a 1024 bit GnuPG key results in 340 bytes of encrypted
475         ### payload in my testing).
476         ($decrypt_rv, $decrypted_msg, $gpg_sign_id) =
477                 &pcap_GPG_decrypt_msg($pkt_data, $access_hr);
478
479         $decrypt_algo = $ALG_GNUPG if $decrypt_rv;
480     }
481
482     ### fall back to Rijndael if the GnuPG decrypt was not successful
483     ### (and note that the GnuPG decryption is only attempted if the
484     ### packet size is large enough).
485     if (defined $access_hr->{'KEY'} and not $decrypt_rv) {
486
487         ($decrypt_rv, $decrypted_msg) = &pcap_Rijndael_decrypt_msg(
488                             $pkt_data, $access_hr->{'KEY'});
489     }
490
491     if ($decrypt_rv) {
492         if ($debug) {
493             ### make sure not to print non-printable stuff
494             my $dec_tmp_msg = $decrypted_msg;
495             $dec_tmp_msg =~ s/[^\x20-\x7e]/NA/g;
496             print STDERR "[+] Decrypted ",
497                 "message: $dec_tmp_msg\n";
498             if ($verbose) {
499                 print STDERR "    Decrypted message (hex dump):\n";
500                 &hex_dump($decrypted_msg);
501             }
502         }
503     } else {
504         print STDERR "[-] Failed decrypt for SOURCE block ",
505             "$access_hr->{'SOURCE'}\n" if $debug;
506     }
507
508     return $decrypt_rv, $decrypted_msg, $gpg_sign_id, $decrypt_algo;
509 }
510
511 sub SPA_check_packet_age() {
512     my $remote_time = shift;
513
514     if ($config{'ENABLE_SPA_PACKET_AGING'} eq 'Y') {
515         if (abs((time() - $remote_time))
516                 > $config{'MAX_SPA_PACKET_AGE'}) {
517             &logr('[-]', "remote time stamp is older than " .
518                 "$config{'MAX_SPA_PACKET_AGE'} second max age.", $SEND_MAIL);
519             return 0;
520         }
521     }
522     return 1;
523 }
524
525 sub SPA_dump_packet() {
526     my $msg_href = shift;
527
528     print STDERR "[+] Packet fields:\n",
529         "        Random data: $msg_href->{'random_number'}\n",
530         "        Username:    $msg_href->{'username'}\n",
531         "        Remote time: $msg_href->{'remote_time'}\n",
532         "        Remote ver:  $msg_href->{'remote_version'}\n",
533         "        Action type: $msg_href->{'action_type'}\n",
534         "        Action:      $msg_href->{'action'}\n",
535         "        MD5 sum:     $msg_href->{'md5sum'}\n";
536
537     if ($msg_href->{'server_auth'}) {
538         if ($msg_href->{'server_auth'} =~ /^\s*(\w+),(.*)/) {
539             my $server_auth_type = lc($1);
540             my $server_auth_crypt_pw = $2;
541             if ($debug) {
542                 print STDERR "        Server auth: $server_auth_type,";
543                 for (my $i=0; $i<length($server_auth_crypt_pw); $i++) {
544                     print STDERR '*';
545                 }
546                 print STDERR "\n";
547             }
548         }
549     }
550     return;
551 }
552
553 sub SPA_check_user() {
554     my ($access_hr, $src_ip, $msg_href) = @_;
555
556     if (defined $access_hr->{'REQUIRE_USERNAME'}) {
557         my $found = 0;
558         my $user  = '';
559         for my $valid_user (@{$access_hr->{'VALID_USERS'}}) {
560             if ($valid_user eq $msg_href->{'username'}) {
561                 $found = 1;
562                 $user  = $valid_user;
563             }
564         }
565         unless ($found) {
566             &logr('[-]', "username mismatch from $src_ip, expecting " .
567                 "$access_hr->{'REQUIRE_USERNAME'}, got " .
568                 "$msg_href->{'username'}", $SEND_MAIL);
569             return 0;
570         }
571     }
572     return 1;
573 }
574
575 sub SPA_check_auth_method() {
576     my ($access_hr, $src_ip, $msg_href) = @_;
577
578     my $server_auth_type     = '';
579     my $server_auth_crypt_pw = '';
580     if ($msg_href->{'server_auth'}) {
581         if ($msg_href->{'server_auth'} =~ /^\s*(\w+),(.*)/) {
582             $server_auth_type = lc($1);
583             $server_auth_crypt_pw = $2;
584         }
585     }
586
587     if (defined $access_hr->{'REQUIRE_AUTH_METHOD'}) {
588         if ($server_auth_type
589                 eq $access_hr->{'REQUIRE_AUTH_METHOD'}) {
590             if ($server_auth_type eq 'crypt') {
591                 ### check the local UNIX crypt() password associated
592                 ### with the user
593                 unless (&server_auth_verify_crypt_pw(
594                             $msg_href->{'username'},
595                             $server_auth_crypt_pw,
596                             $access_hr->{'SHADOW_FILE'})) {
597                     &logr('[-]', "IP: $src_ip failed server-auth UNIX " .
598                         "crypt() password test", $NO_MAIL);
599                     return 0;
600                 }
601             }
602         } else {
603             &logr('[-]', "required server-auth method " .
604                 "\"$access_hr->{'REQUIRE_AUTH_METHOD'}\" " .
605                 "not supplied by $src_ip", $NO_MAIL);
606             return 0;
607         }
608     }
609     return 1;
610 }
611
612 sub SPA_access() {
613     my ($msg_href, $src_ip, $decrypt_algo, $gpg_sign_id,
614         $md5sum, $access_hr) = @_;
615
616     if ($access_hr->{'DISABLE_FW_ACCESS'}) {
617         &logr('[-]', "received fw access request from $src_ip, " .
618             "but DISABLE_FW_ACCESS is set to a true value", $NO_MAIL);
619         return 0;
620     }
621
622     my $allow_ip = '';
623     $allow_ip = $1 if $msg_href->{'action'} =~ /($ip_re)/;
624
625     unless ($allow_ip) {
626         &logr('[-]', "no valid IP address within action portion of SPA " .
627             "packet from $src_ip", $SEND_MAIL);
628         return 0;
629     }
630
631     if ($allow_ip eq '0.0.0.0') {
632         if ($config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y'
633                 or (defined $access_hr->{'REQUIRE_SOURCE_ADDRESS'}
634                     and $access_hr->{'REQUIRE_SOURCE_ADDRESS'})) {
635             &logr('[-]', "IP: $src_ip sent SPA packet that " .
636                 "contained 0.0.0.0 (-s on the client side) " .
637                 "but REQUIRE_SOURCE_ADDRESS is enabled", $SEND_MAIL);
638             return 0;
639         } else {
640             $allow_ip = $src_ip;
641         }
642     }
643
644     my %open_ports = ();
645
646     ### initialize to the OPEN_PORTS directives (if defined; we know that
647     ### either OPEN_PORTS or PERMIT_CLIENT_PORTS was specified in the
648     ### access.conf file)
649     %open_ports = %{$access_hr->{'OPEN_PORTS'}}
650         if defined $access_hr->{'OPEN_PORTS'};
651
652     if ($access_hr->{'PERMIT_CLIENT_PORTS'}) {
653         if ($msg_href->{'action'}
654                 =~ /($ip_re),(tcp|udp|icmp),(\d+)/i) {
655             ### single port access format (e.g. tcp,22)
656             my $allow_ip        = $1;
657             my $dec_allow_port  = $2;
658             my $dec_allow_proto = $3;
659
660             $open_ports{$dec_allow_proto}{$dec_allow_port} = '';
661
662         } elsif ($msg_href->{'action'}
663                  =~ /($ip_re),(\S+)/) {
664             ### multi-port access format (-A was specified by
665             ### the client)
666             my $allow_ip   = $1;
667             my $access_str = $2;
668
669             my @dec_allow_ports = split /,/, $access_str;
670
671             for my $port_str (@dec_allow_ports) {
672                 if ($port_str =~ m|(\D+)/(\d+)|) {
673                     my $proto = lc($1);
674                     my $port  = $2;
675
676                     next unless ($proto eq 'tcp'
677                         or $proto eq 'udp'
678                         or $proto eq 'icmp');
679                     $port = 0 if $proto eq 'icmp';
680
681                     $open_ports{$proto}{$port} = '';
682                 }
683             }
684         }
685     }
686
687     if ($decrypt_algo == $ALG_GNUPG) {
688         if ($access_hr->{'GPG_REMOTE_ID'} ne 'ANY') {
689             &logr('[+]', "received valid GnuPG encrypted packet " .
690                 qq|(signed with required key ID: "$gpg_sign_id") from: | .
691                 "$src_ip, remote user: $msg_href->{'username'}", $NO_MAIL);
692         } else {
693             &logr('[+]', "received valid GnuPG encrypted packet " .
694                 "from: $src_ip, remote user: $msg_href->{'username'}",
695                 $NO_MAIL);
696         }
697     } else {
698         &logr('[+]', "received valid Rijndael encrypted " .
699             "packet from: $src_ip, remote user: $msg_href->{'username'}",
700             $NO_MAIL);
701     }
702
703     ### cache the MD5 sum
704     $md5_msg_store{$md5sum} = $src_ip;
705
706     ### write MD5 sum to disk
707     &diskwrite_md5_sum($md5sum, $src_ip)
708         if $config{'ENABLE_MD5_PERSISTENCE'} eq 'Y';
709
710     ### grant access through the firewall
711     &grant_access($allow_ip, '', \%open_ports, $access_hr);
712
713     return 1;
714 }
715
716 sub SPA_cmd() {
717     my ($msg_href, $src_ip, $decrypt_algo, $gpg_sign_id,
718         $md5sum, $access_hr) = @_;
719
720     unless ($access_hr->{'ENABLE_CMD_EXEC'}) {
721         &logr('[-]', qq|received command "$msg_href->{'action'}" | .
722                 "but command mode not enabled for $src_ip", $SEND_MAIL);
723         return 0;
724     }
725
726     if (defined $access_hr->{'CMD_REGEX'}) {
727         unless ($msg_href->{'action'} =~ m|$access_hr->{'CMD_REGEX'}|) {
728             &logr('[-]', qq|received command "$msg_href->{'action'}" | .
729                     "from $src_ip but CMD_REGEX did not match $src_ip",
730                     $SEND_MAIL);
731             return 0;
732         }
733     }
734
735     my $cmd = $msg_href->{'action'};
736     my $run_cmd = '';
737     my $cmd_ip  = '';
738
739     if ($cmd =~ m|^\s*($ip_re),(.*)|) {
740         $cmd_ip  = $1;
741         $run_cmd = $2;
742     } else {
743         $run_cmd = $cmd;
744     }
745
746     ### pre-1.0 versions did not prepend command string with "<ip>,"
747     if ($cmd_ip and $cmd_ip eq '0.0.0.0'
748             and $config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y'
749             or (defined $access_hr->{'REQUIRE_SOURCE_ADDRESS'}
750                 and $access_hr->{'REQUIRE_SOURCE_ADDRESS'})) {
751         &logr('[-]', "IP: $src_ip sent SPA packet that " .
752             "contained 0.0.0.0 (-s on the client side) " .
753             "but REQUIRE_SOURCE_ADDRESS is enabled", $SEND_MAIL);
754         return 0;
755     }
756
757     if ($decrypt_algo == $ALG_GNUPG) {
758         if ($access_hr->{'GPG_REMOTE_ID'} ne 'ANY') {
759             &logr('[+]', "received valid GnuPG encrypted packet " .
760                 qq|(signed with required key ID: "$gpg_sign_id") from: | .
761                 "$src_ip, remote user: $msg_href->{'username'}",
762                 $NO_MAIL);
763         } else {
764             &logr('[+]', "received valid GnuPG encrypted packet " .
765                 "from: $src_ip, remote user: $msg_href->{'username'}",
766                 $NO_MAIL);
767         }
768     } else {
769         &logr('[+]', "received valid Rijndael encrypted " .
770             "packet from: $src_ip, remote user: $msg_href->{'username'}",
771             $NO_MAIL);
772     }
773
774     &logr('[+]', qq|executing command "$run_cmd" for $src_ip|, $SEND_MAIL);
775
776     ### cache the MD5 sum
777     $md5_msg_store{$md5sum} = '';
778
779     ### write MD5 sum to disk
780     &diskwrite_md5_sum($md5sum)
781         if $config{'ENABLE_MD5_PERSISTENCE'} eq 'Y';
782
783     ### execute the command
784     &exec_command($run_cmd);
785
786     return 1;
787 }
788
789 sub check_replay_attack() {
790     my ($decrypted_data, $src_ip) = @_;
791
792     my $rv = 0;
793
794     my $md5sum = md5_base64($decrypted_data);
795
796     if (defined $md5sum and $md5sum =~ /\S/) {
797         if (defined $md5_msg_store{$md5sum}) {
798   &nbs