root/fwknop/tags/fwknop-1.8.2-pre6/fwknopd

Revision 731, 126.3 kB (checked in by mbr, 1 year ago)

fwknop-1.8.2-pre6

  • 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.2-pre6
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.2-pre6';
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 $ipt_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 $command_mode = 0;
103 my $access_mode  = 1;
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     = 16;
113
114 ### packet counters
115 my $tcp_ctr  = 0;
116 my $udp_ctr  = 0;
117 my $icmp_ctr = 0;
118
119 ### tcp option types
120 my $tcp_nop_type       = 1;
121 my $tcp_mss_type       = 2;
122 my $tcp_win_scale_type = 3;
123 my $tcp_sack_type      = 4;
124 my $tcp_timestamp_type = 8;
125
126 my %tcp_p0f_opt_types = (
127     'N' => $tcp_nop_type,
128     'M' => $tcp_mss_type,
129     'W' => $tcp_win_scale_type,
130     'S' => $tcp_sack_type,
131     'T' => $tcp_timestamp_type
132 );
133
134 my %access_keys = (
135     'SOURCE' => '',
136     'TYPE'   => '',
137     'KEY'    => '',
138     'OPEN_PORTS'     => '',
139     'GPG_REMOTE_ID'  => '',
140     'GPG_DECRYPT_ID' => '',
141     'GPG_DECRYPT_PW' => '',
142     'GPG_HOME_DIR'   => '',
143     'ULOG_PCAP'      => '',
144     'FILE_PCAP'      => '',
145     'DATA_COLLECT_MODE' => '',
146     'ENCRYPT_SEQUENCE'  => '',
147     'SHARED_SEQUENCE'   => '',
148     'PORT_OFFSET'       => '',
149     'REQUIRE_AUTH_METHOD' => '',
150     'SHADOW_FILE'    => '',
151     'KNOCK_INTERVAL' => '',
152     'KNOCK_LIMIT'    => '',
153     'PERMIT_CLIENT_PORTS' => '',
154     'ENABLE_CMD_EXEC'     => '',
155     'DISABLE_FW_ACCESS'   => '',
156     'REQUIRE_SOURCE_ADDRESS' => '',
157     'CMD_REGEX'         => '',
158     'FW_ACCESS_TIMEOUT' => '',
159     'REQUIRE_USERNAME'  => '',
160     'MIN_TIME_DIFF' => '',
161     'MAX_TIME_DIFF' => '',
162     'RESTRICT_INTF' => '',
163 );
164
165 my $ip_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|;
166
167 my @args_cp = @ARGV;
168
169 ### run GetOpt() to get comand line args
170 &handle_command_line();
171
172 &usage(0) if $print_help;
173
174 if ($print_version) {
175     print "[+] fwknopd v$version (file revision: $rev_num)\n",
176         "      by Michael Rash <mbr\@cipherdyne.org>\n";
177     exit 0;
178 }
179
180 if ($os_fprint_only) {
181     print "[+] Entering OS fingerprinting mode.\n";
182 }
183
184 print STDERR "[+] ** Starting fwknopd (debug mode) **\n" if $debug;
185
186 ### setup to run
187 &fwknop_init();
188
189 if ($config{'AUTH_MODE'} eq 'KNOCK' or $os_fprint_only) {
190
191     ### we are running in traditional port knocking mode
192     &knock_loop();
193
194 } elsif ($config{'AUTH_MODE'} eq 'FILE_PCAP'
195         or $config{'AUTH_MODE'} eq 'ULOG_PCAP'
196         or $config{'AUTH_MODE'} eq 'PCAP') {
197
198     ### we are parsing the pcap file created by the ulogd pcap
199     ### writer, or in sniffing mode against an interface
200     &pcap_loop();
201 }
202 exit 0;
203 #============================ end main ==============================
204
205 sub pcap_loop() {
206
207     ### we use both a size and an inode check in the FILE_PCAP and
208     ### ULOG_PCAP modes to check if the file has been rotated
209     my $pcap_file_size  = 0;
210     my $pcap_file_inode = 0;
211
212     ### get pcap opject
213     my $pcap_t = &get_pcap_obj();
214
215     if ($config{'AUTH_MODE'} eq 'FILE_PCAP'
216             or $config{'AUTH_MODE'} eq 'ULOG_PCAP') {
217         ### get file size (we don't need a -e check here because
218         ### this is handled in get_pcap_obj()).
219         $pcap_file_size = -s $config{'PCAP_PKT_FILE'};
220
221         ### get inode associated with the sniffing file
222         $pcap_file_inode = (stat($config{'PCAP_PKT_FILE'}))[1];
223     }
224     print STDERR "[+] pcap_loop()\n" if $debug;
225
226     my $check_file_ctr = 0;
227
228     for (;;) {
229
230         Net::Pcap::loop($pcap_t, 1, \&pcap_process_pkt, 'fwknop_tag');
231
232         if ($config{'AUTH_MODE'} eq 'FILE_PCAP'
233                 or $config{'AUTH_MODE'} eq 'ULOG_PCAP') {
234
235             ### check to see if the pcap file has been rotated (we need to
236             ### close and re-open)
237             if ($check_file_ctr == 10) {
238                 if (-e $config{'PCAP_PKT_FILE'}) {
239                     my $size_tmp  = -s $config{'PCAP_PKT_FILE'};
240                     my $inode_tmp = (stat($config{'PCAP_PKT_FILE'}))[1];
241                     if ($inode_tmp != $pcap_file_inode
242                             or $size_tmp < $pcap_file_size) {
243
244                         ### the file was rotated or shrank, so get new
245                         ### pcap_t object
246                         Net::Pcap::close($pcap_t);
247
248                         &logr('[+]', "pcap file $config{'PCAP_PKT_FILE'} " .
249                             "shrank or was rotated, so re-opening", 0);
250                         $pcap_t = &get_pcap_obj();
251
252                         ### set file size and inode
253                         $pcap_file_size  = $size_tmp;
254                         $pcap_file_inode = $inode_tmp;
255                     }
256                 } else {
257                     Net::Pcap::close($pcap_t);
258                     &logr('[+]', "pcap file $config{'PCAP_PKT_FILE'} " .
259                         "was rotated, so re-opening", 0);
260                     $pcap_t = &get_pcap_obj();
261
262                     ### set file size and inode
263                     $pcap_file_size  = -s $config{'PCAP_PKT_FILE'};
264                     $pcap_file_inode = (stat($config{'PCAP_PKT_FILE'}))[1];
265                 }
266                 $check_file_ctr = 0;
267             }
268             $check_file_ctr++;
269
270             ### always check to see if we need to timeout access for IPs
271             ### For AUTH_MODE set to PCAP, knoptm will timeout access
272             &timeout_access();
273         }
274
275         sleep $pcap_sleep_interval;
276     }
277
278     Net::Pcap::close($pcap_t);
279
280     return;
281 }
282
283 sub pcap_process_pkt() {
284     my ($tag, $hdr, $pkt) = @_;
285
286     return unless $tag eq 'fwknop_tag';
287     return unless defined $hdr;
288     return unless defined $pkt;
289
290     my $ether_data = '';
291     my $ip         = '';
292     my $src_ip     = '';
293     my $proto      = '';
294     my $transport_obj = '';
295
296     if ($config{'AUTH_MODE'} eq 'ULOG_PCAP') {
297         ### The ulogd pcap writer does not include link layer information
298         $ip = NetPacket::IP->decode($pkt) or return;
299     } else {
300         $ether_data = NetPacket::Ethernet::strip($pkt) or return;
301         $ip = NetPacket::IP->decode($ether_data) or return;
302     }
303
304     ### get the source IP address
305     $src_ip = $ip->{'src_ip'} or return;
306
307     ### get the protocol
308     $proto = $ip->{'proto'} or return;
309
310     if ($proto == 1) {
311         $transport_obj = NetPacket::ICMP->decode($ip->{'data'});
312     } elsif ($proto == 6) {
313         $transport_obj = NetPacket::TCP->decode($ip->{'data'});
314     } elsif ($proto == 17) {
315         $transport_obj = NetPacket::UDP->decode($ip->{'data'});
316     } else {
317         return;
318     }
319
320     print STDERR "[+] Received packet ***[" .
321         localtime() . "]*** (" if $debug;
322
323     ### make sure we have _some_ data in the packet; in practice
324     ### any valid SPA message will be longer than 10 bytes, but this
325     ### check is better than nothing
326     return unless defined $transport_obj->{'data'};
327
328     my $enc_msg_len = 0;
329     $enc_msg_len = length($transport_obj->{'data'});
330     if (10 < $enc_msg_len and $enc_msg_len < 1500) {
331         print STDERR "$enc_msg_len bytes)\n" if $debug;
332     } else {
333         print STDERR "$enc_msg_len bytes, not attempting decrypt)\n"
334             if $debug;
335         return;
336     }
337
338     if ($debug) {
339         ### make sure not to print non-printable stuff
340         my $data_tmp = $transport_obj->{'data'};
341         $data_tmp =~ s/[^\x20-\x7e]/NA/g;
342         print STDERR "[+] Received data: $data_tmp\n"
343             if $debug;
344     }
345
346     ### see if this packet is worthy of getting access through
347     ### the firewall
348     &SPA_check_grant_access($src_ip, $enc_msg_len, $transport_obj->{'data'});
349
350     return;
351 }
352
353 sub SPA_check_grant_access() {
354     my ($src_ip, $enc_msg_len, $pkt_data) = @_;
355
356     ### first check to see if we have any matching access directives
357     ### (in access.conf) for $src_ip, and if not we will do _nothing_
358     ### with this packet.
359     my $access_nums_aref = &check_src($src_ip);
360
361     unless ($access_nums_aref) {
362         print STDERR "[-] Packet from $src_ip did not match any ",
363               "SOURCE blocks in $config{'ACCESS_CONF'}\n" if $debug;
364         return;
365     }
366
367     ### at this point we have a non-replayed packet, so see if it qualifies
368     ### for any access
369     SOURCE: for my $num (@$access_nums_aref) {
370         my $access_hr = $access[$num];
371
372         next SOURCE unless $access_hr->{'DATA_COLLECT_MODE'} =~ /PCAP/;
373
374         &dump_access($access_hr, $num) if $debug and $verbose;
375
376         ### keep track of which source block we are dealing with from
377         ### access.conf
378         my $source_block_num = $access_hr->{'block_num'};
379
380         ### see if we can decrypt and base64-decode
381         my ($decrypt_rv, $decrypted_msg, $gpg_sign_id, $decrypt_algo)
382             = &SPA_decrypt($pkt_data, $enc_msg_len, $access_hr);
383         next SOURCE unless $decrypt_rv;
384
385         ### check for replay attacks
386         my ($md5sum_rv, $md5sum)
387             = &check_replay_attack($decrypted_msg, $src_ip);
388         return if $md5sum_rv;
389
390         ### see if we have a syntactically valid message
391         my ($validate_rv, $msg_href) = &pcap_validate_msg(
392             $decrypted_msg, $source_block_num, $access_hr);
393         next SOURCE unless $validate_rv;
394
395         ### check to see if client side time stamp is too old
396         my $time_check_rv = &SPA_check_packet_age($msg_href->{'remote_time'});
397         next SOURCE unless $time_check_rv;
398
399         ### dump packet to stderr for debugging purposes
400         &SPA_dump_packet($msg_href) if $debug;
401
402         ### check username
403         next SOURCE unless &SPA_check_user($access_hr, $src_ip, $msg_href);
404
405         ### check authentication method
406         next SOURCE unless &SPA_check_auth_method(
407             $access_hr, $src_ip, $msg_href);
408
409         if ($msg_href->{'action_type'} == $access_mode) {
410             if (&SPA_access($msg_href, $src_ip, $decrypt_algo,
411                     $gpg_sign_id, $md5sum, $access_hr)) {
412                 last SOURCE;
413             } else {
414                 next SOURCE;
415             }
416         } elsif ($msg_href->{'action_type'} == $command_mode) {
417             if (&SPA_cmd($msg_href, $src_ip, $decrypt_algo,
418                     $gpg_sign_id, $md5sum, $access_hr)) {
419                 last SOURCE;
420             } else {
421                 next SOURCE;
422             }
423         }
424     }
425     return;
426 }
427
428 sub SPA_decrypt() {
429     my ($pkt_data, $enc_msg_len, $access_hr) = @_;
430
431     my $decrypted_msg = '';
432     my $decrypt_algo  = 'Rijndael';
433     my $gpg_sign_id   = '';
434     my $decrypt_rv    = 1;
435
436     if ($enc_msg_len > 400
437             and defined $access_hr->{'GPG_REMOTE_ID'}) {
438         ### attempt GPG decrypt (only if the length of the encrypted
439         ### payload is greater than 500 bytes; even encrypting a single
440         ### byte of data with a 1024 bit GnuPG key results in 340 bytes
441         ### of encrypted payload).
442         ($decrypted_msg, $gpg_sign_id) =
443                 &pcap_GPG_decrypt_msg($pkt_data, $access_hr);
444         $decrypt_algo = 'GnuPG' if $decrypted_msg;
445
446     } elsif (defined $access_hr->{'KEY'}) {
447
448         $decrypted_msg = &pcap_Rijndael_decrypt_msg($pkt_data,
449                 $access_hr->{'KEY'});
450     }
451
452     if ($decrypted_msg) {
453         if ($debug) {
454             ### make sure not to print non-printable stuff
455             my $dec_tmp_msg = $decrypted_msg;
456             $dec_tmp_msg =~ s/[^\x20-\x7e]/NA/g;
457             print STDERR "[+] Decrypted message: $dec_tmp_msg\n";
458         }
459     } else {
460         print STDERR "[-] Failed decrypt for SOURCE block ",
461             "$access_hr->{'SOURCE'}\n" if $debug;
462         $decrypt_rv = 0;
463     }
464
465     return $decrypt_rv, $decrypted_msg, $gpg_sign_id, $decrypt_algo;
466 }
467
468 sub SPA_check_packet_age() {
469     my $remote_time = shift;
470
471     if ($config{'ENABLE_SPA_PACKET_AGING'} eq 'Y') {
472         if (abs((time() - $remote_time))
473                 > $config{'MAX_SPA_PACKET_AGE'}) {
474             &logr('[-]', "remote time stamp is older than " .
475                 "$config{'MAX_SPA_PACKET_AGE'} second max age.", 1);
476             return 0;
477         }
478     }
479     return 1;
480 }
481
482 sub SPA_dump_packet() {
483     my $msg_href = shift;
484
485     print STDERR "[+] Packet fields:\n",
486         "        Random data: $msg_href->{'random_number'}\n",
487         "        Username:    $msg_href->{'username'}\n",
488         "        Remote time: $msg_href->{'remote_time'}\n",
489         "        Remote ver:  $msg_href->{'remote_version'}\n",
490         "        Action type: $msg_href->{'action_type'}\n",
491         "        Action:      $msg_href->{'action'}\n",
492         "        MD5 sum:     $msg_href->{'md5sum'}\n";
493
494     if ($msg_href->{'server_auth'}) {
495         if ($msg_href->{'server_auth'} =~ /^\s*(\w+),(.*)/) {
496             my $server_auth_type = lc($1);
497             my $server_auth_crypt_pw = $2;
498             if ($debug) {
499                 print STDERR "        Server auth: $server_auth_type,";
500                 for (my $i=0; $i<length($server_auth_crypt_pw); $i++) {
501                     print STDERR '*';
502                 }
503                 print STDERR "\n";
504             }
505         }
506     }
507     return;
508 }
509
510 sub SPA_check_user() {
511     my ($access_hr, $src_ip, $msg_href) = @_;
512
513     if (defined $access_hr->{'REQUIRE_USERNAME'}) {
514         my $found = 0;
515         my $user  = '';
516         for my $valid_user (@{$access_hr->{'VALID_USERS'}}) {
517             if ($valid_user eq $msg_href->{'username'}) {
518                 $found = 1;
519                 $user  = $valid_user;
520             }
521         }
522         unless ($found) {
523             &logr('[-]', "username mismatch from $src_ip, expecting " .
524                 "$access_hr->{'REQUIRE_USERNAME'}, got " .
525                 "$msg_href->{'username'}", 1);
526             return 0;
527         }
528     }
529     return 1;
530 }
531
532 sub SPA_check_auth_method() {
533     my ($access_hr, $src_ip, $msg_href) = @_;
534
535     my $server_auth_type     = '';
536     my $server_auth_crypt_pw = '';
537     if ($msg_href->{'server_auth'}) {
538         if ($msg_href->{'server_auth'} =~ /^\s*(\w+),(.*)/) {
539             $server_auth_type = lc($1);
540             $server_auth_crypt_pw = $2;
541         }
542     }
543
544     if (defined $access_hr->{'REQUIRE_AUTH_METHOD'}) {
545         if ($server_auth_type
546                 eq $access_hr->{'REQUIRE_AUTH_METHOD'}) {
547             if ($server_auth_type eq 'crypt') {
548                 ### check the local UNIX crypt() password associated
549                 ### with the user
550                 unless (&server_auth_verify_crypt_pw(
551                             $msg_href->{'username'},
552                             $server_auth_crypt_pw,
553                             $access_hr->{'SHADOW_FILE'})) {
554                     &logr('[-]', "IP: $src_ip failed server-auth UNIX " .
555                         "crypt() password test", 0);
556                     return 0;
557                 }
558             }
559         } else {
560             &logr('[-]', "required server-auth method " .
561                 "\"$access_hr->{'REQUIRE_AUTH_METHOD'}\" " .
562                 "not supplied by $src_ip", 0);
563             return 0;
564         }
565     }
566     return 1;
567 }
568
569 sub SPA_access() {
570     my ($msg_href, $src_ip, $decrypt_algo, $gpg_sign_id,
571         $md5sum, $access_hr) = @_;
572
573     if ($access_hr->{'DISABLE_FW_ACCESS'}) {
574         &logr('[-]', "received fw access request from $src_ip, " .
575             "but DISABLE_FW_ACCESS is set to a true value", 0);
576         return 0;
577     }
578
579     my $allow_ip = '';
580     $allow_ip = $1 if $msg_href->{'action'} =~ /($ip_re)/;
581
582     unless ($allow_ip) {
583         &logr('[-]', "no valid IP address within action portion of SPA " .
584             "packet from $src_ip", 1);
585         return 0;
586     }
587
588     if ($allow_ip eq '0.0.0.0') {
589         if ($config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y'
590                 or (defined $access_hr->{'REQUIRE_SOURCE_ADDRESS'}
591                     and $access_hr->{'REQUIRE_SOURCE_ADDRESS'})) {
592             &logr('[-]', "IP: $src_ip sent SPA packet that " .
593                 "contained 0.0.0.0 (-s on the client side) " .
594                 "but REQUIRE_SOURCE_ADDRESS is enabled", 1);
595             return 0;
596         } else {
597             $allow_ip = $src_ip;
598         }
599     }
600
601     my %open_ports = ();
602
603     ### initialize to the OPEN_PORTS directives (if defined; we know that
604     ### either OPEN_PORTS or PERMIT_CLIENT_PORTS was specified in the
605     ### access.conf file)
606     %open_ports = %{$access_hr->{'OPEN_PORTS'}}
607         if defined $access_hr->{'OPEN_PORTS'};
608
609     if ($access_hr->{'PERMIT_CLIENT_PORTS'}) {
610         if ($msg_href->{'action'}
611                 =~ /($ip_re),(tcp|udp|icmp),(\d+)/i) {
612             ### single port access format (e.g. tcp,22)
613             my $allow_ip        = $1;
614             my $dec_allow_port  = $2;
615             my $dec_allow_proto = $3;
616
617             $open_ports{$dec_allow_proto}{$dec_allow_port} = '';
618
619         } elsif ($msg_href->{'action'}
620                  =~ /($ip_re),(\S+)/) {
621             ### multi-port access format (-A was specified by
622             ### the client)
623             my $allow_ip   = $1;
624             my $access_str = $2;
625
626             my @dec_allow_ports = split /,/, $access_str;
627
628             for my $port_str (@dec_allow_ports) {
629                 if ($port_str =~ m|(\D+)/(\d+)|) {
630                     my $proto = lc($1);
631                     my $port  = $2;
632
633                     next unless ($proto eq 'tcp'
634                         or $proto eq 'udp'
635                         or $proto eq 'icmp');
636                     $port = 0 if $proto eq 'icmp';
637
638                     $open_ports{$proto}{$port} = '';
639                 }
640             }
641         }
642     }
643
644     if ($decrypt_algo eq 'GnuPG') {
645         if ($access_hr->{'GPG_REMOTE_ID'} ne 'ANY') {
646             &logr('[+]', "received valid $decrypt_algo encrypted packet " .
647                 qq|(signed with required key ID: "$gpg_sign_id") from: | .
648                 "$src_ip, remote user: $msg_href->{'username'}", 0);
649         } else {
650             &logr('[+]', "received valid $decrypt_algo encrypted packet " .
651                 "from: $src_ip, remote user: $msg_href->{'username'}", 0);
652         }
653     } else {
654         &logr('[+]', "received valid $decrypt_algo encrypted " .
655             "packet from: $src_ip, remote user: $msg_href->{'username'}", 0);
656     }
657
658     ### cache the MD5 sum
659     $md5_msg_store{$md5sum} = '';
660
661     ### write MD5 sum to disk
662     &diskwrite_md5_sum($md5sum)
663         if $config{'ENABLE_MD5_PERSISTENCE'} eq 'Y';
664
665     ### grant access through the firewall
666     &grant_access($allow_ip, '', \%open_ports, $access_hr);
667
668     return 1;
669 }
670
671 sub SPA_cmd() {
672     my ($msg_href, $src_ip, $decrypt_algo, $gpg_sign_id,
673         $md5sum, $access_hr) = @_;
674
675     unless ($access_hr->{'ENABLE_CMD_EXEC'}) {
676         &logr('[-]', qq|received command "$msg_href->{'action'}" | .
677                 "but command mode not enabled for $src_ip", 1);
678         return 0;
679     }
680
681     if (defined $access_hr->{'CMD_REGEX'}) {
682         unless ($msg_href->{'action'} =~ m|$access_hr->{'CMD_REGEX'}|) {
683             &logr('[-]', qq|received command "$msg_href->{'action'}" | .
684                     "from $src_ip but CMD_REGEX did not match $src_ip", 1);
685             return 0;
686         }
687     }
688
689     my $cmd = $msg_href->{'action'};
690     my $run_cmd = '';
691     my $cmd_ip  = '';
692
693     if ($cmd =~ m|^\s*($ip_re),(.*)|) {
694         $cmd_ip  = $1;
695         $run_cmd = $2;
696     } else {
697         $run_cmd = $cmd;
698     }
699
700     ### pre-1.0 versions did not prepend command string with "<ip>,"
701     if ($cmd_ip and $cmd_ip eq '0.0.0.0'
702             and $config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y'
703             or (defined $access_hr->{'REQUIRE_SOURCE_ADDRESS'}
704                 and $access_hr->{'REQUIRE_SOURCE_ADDRESS'})) {
705         &logr('[-]', "IP: $src_ip sent SPA packet that " .
706             "contained 0.0.0.0 (-s on the client side) " .
707             "but REQUIRE_SOURCE_ADDRESS is enabled", 1);
708         return 0;
709     }
710
711     if ($decrypt_algo eq 'GnuPG') {
712         if ($access_hr->{'GPG_REMOTE_ID'} ne 'ANY') {
713             &logr('[+]', "received valid $decrypt_algo encrypted packet " .
714                 qq|(signed with required key ID: "$gpg_sign_id") from: | .
715                 "$src_ip, remote user: $msg_href->{'username'}", 0);
716         } else {
717             &logr('[+]', "received valid $decrypt_algo encrypted packet " .
718                 "from: $src_ip, remote user: $msg_href->{'username'}", 0);
719         }
720     } else {
721         &logr('[+]', "received valid $decrypt_algo encrypted " .
722             "packet from: $src_ip, remote user: $msg_href->{'username'}", 0);
723     }
724
725     &logr('[+]', qq|executing command "$msg_href->{'action'}" for $src_ip|, 1);
726
727     ### cache the MD5 sum
728     $md5_msg_store{$md5sum} = '';
729
730     ### write MD5 sum to disk
731     &diskwrite_md5_sum($md5sum)
732         if $config{'ENABLE_MD5_PERSISTENCE'} eq 'Y';
733
734     ### execute the command
735     &exec_command($run_cmd);
736
737     return 1;
738 }
739
740 sub check_replay_attack() {
741     my ($decrypted_data, $src_ip) = @_;
742
743     my $md5sum = md5_base64($decrypted_data);
744
745     if (defined $md5sum and $md5sum =~ /\S/) {
746         if (defined $md5_msg_store{$md5sum}) {
747             ### Bad!  Send warning email and return.
748             &logr('[-]', "attempted message replay from: $src_ip", 1);
749             return 1, '';
750         }
751     } else {
752         ### could not calculate the MD5 sum for some reason; don't
753         ### trust the packet
754         &logr('[-]', "could not calculate md5 sum for SPA " .
755             "packet from: $src_ip", 1);
756         return 1, '';
757     }
758     return 0, $md5sum;
759 }
760
761 sub server_auth_verify_crypt_pw() {
762     my ($username, $pw, $shadow_file) = @_;
763
764     unless (-e $shadow_file) {
765         &logr('[-]', "shadow file $shadow_file does not exist", 0);
766         return 0;
767     }
768
769     my $shadow_hash = '';
770     open S, "< $shadow_file" or die "[*] Could not open $shadow_file: $!";
771     while (<S>) {
772         my $line = $_;
773         if ($line =~ /^\s*$username:(\S+?):/) {
774             $shadow_hash = $1;
775         }
776     }
777     close S;
778
779     ### mbr:$1$nrU****************************:13108:0:99999:7:::
780     unless ($shadow_hash) {
781         &logr('[-]', "could not get password entry for $username " .
782             "from /etc/shadow", 0);
783         return 0;
784     }
785
786     return 1 if (crypt($pw, $shadow_hash) eq $shadow_hash);
787     return 0;
788 }
789
790 sub knock_loop() {
791     print STDERR "[+] Opening $config{'FW_DATA_FILE'}, and entering main loop.\n"
792         if $debug;
793
794     ### main server loop
795     open FWLOG, $config{'FW_DATA_FILE'} or die $!;
796     for (;;) {
797         my @fw_pkts = <FWLOG>;
798         if (@fw_pkts and