3 #############################################################################
5 # File: fwknopd (/usr/sbin/fwknopd)
7 # URL: http://www.cipherdyne.org/fwknop/
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
20 # More information can be found in the fwknop(8) and fwknopd(8) man
21 # pages, and also online here:
23 # http://www.cipherdyne.org/fwknop/docs/
25 # Author: Michael Rash (mbr@cipherdyne.org)
29 # Copyright (C) 2004-2009 Michael Rash (mbr@cipherdyne.org)
31 # License - GNU Public License version 2 (GPLv2):
33 # This program is distributed in the hope that it will be useful,
34 # but WITHOUT ANY WARRANTY; without even the implied warranty of
35 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
36 # GNU General Public License for more details.
38 # You should have received a copy of the GNU General Public License
39 # along with this program; if not, write to the Free Software
40 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
43 #############################################################################
45 # $Id: fwknopd 1533 2009-09-08 02:44:02Z mbr $
52 use POSIX ':sys_wait_h';
56 my $config_file = '/etc/fwknop/fwknop.conf';
57 my $access_conf_file = '';
59 my $version = '1.9.12';
60 my $revision_svn = '$Revision: 1533 $';
62 ($rev_num) = $revision_svn =~ m|\$Rev.*:\s+(\S+)|;
65 my $override_config_str = '';
69 my $blacklist_ar = [];
70 my $blacklist_exclude_ar = [];
73 my %ip_sequences = ();
74 my %digest_store = ();
77 my %ipt_prerouting = ();
78 my %ipt_postrouting = ();
79 my %ipt_output = (); ### optional
82 my $ipfw_is_dynamic = 0;
84 my $os_fprint_only = 0;
85 my $print_version = 0;
97 my $ipt_del_chains = 0;
101 my $imported_gpg = 0;
103 my $use_sendmail = 0;
104 my $cmdline_intf = '';
107 my $cmdline_knoptm = '';
108 my $skip_fko_module = 0;
109 my $use_fko_module = 0;
110 my $test_fko_exists = 0;
111 my $fko_incoming_digest_type = 0;
113 my $cmdl_disable_gpg = 0;
114 my $cmdline_fwknop_serv = '';
115 my $knoptm_debug_file = '';
116 my $knoptm_include_pidname = 0;
117 my $fwkserv_debug_file = '';
118 my $fwkserv_include_pidname = 0;
119 my $err_wait_timer = 30; ### seconds
120 my $gpg_agent_info = '';
121 my $gpg_no_options = 0;
122 my $gpg_use_options = 0;
123 my $gpg_default_prefix = 'hQ'; ### base64 encoded 0x8502
124 my $build_ipt_config = 0;
125 my $skipped_first_loop = 0;
126 my $imported_crypt_cbc = 0;
127 my $pcap_sleep_interval = 1; ### seconds
128 my $imported_iptables_modules = 0;
129 my $include_all_config_data = 0;
130 my $voluntary_exit_timestamp = 0;
131 my $fw_data_file = ''; ### legacy port knocking mode
133 my $spa_dump_packets = '';
135 my $cmdline_locale = '';
138 ### SPA message types from fwknop clients
141 ### random data : user : client_timestamp : client_version : \
142 ### type (0) : command : digest
143 my $SPA_COMMAND_MODE = 0;
145 ### ACCESS message (this type is used most often):
146 ### random data : user : client_timestamp : client_version : \
147 ### type (1) : access_request : digest
148 my $SPA_ACCESS_MODE = 1; ### default
150 ### NAT ACCESS message:
151 ### random data : user : client_timestamp : client_version : \
152 ### type (2) : access_request : NAT_info : digest
153 my $SPA_NAT_ACCESS_MODE = 2;
155 ### ACCESS message with client-defined firewall timeout:
156 ### random data : user : client_timestamp : client_version : \
157 ### type (3) : access_request : timeout : digest
158 my $SPA_CLIENT_TIMEOUT_ACCESS_MODE = 3;
160 ### NAT ACCESS message with client-defined firewall timeout:
161 ### random data : user : client_timestamp : client_version : \
162 ### type (4) : access_request : NAT_info : timeout : digest
163 my $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE = 4;
165 ### local NAT ACCESS message:
166 ### random data : user : client_timestamp : client_version : \
167 ### type (5) : access_request : NAT_info : message digest
168 my $SPA_LOCAL_NAT_ACCESS_MODE = 5;
170 ### local NAT ACCESS message with client-defined firewall timeout:
171 ### random data : user : client_timestamp : client_version : \
172 ### type (6) : access_request : NAT_info : timeout : message digest
173 my $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE = 6;
175 my %spa_mode_strings = (
176 $SPA_COMMAND_MODE => 'SPA_COMMAND_MODE',
177 $SPA_ACCESS_MODE => 'SPA_ACCESS_MODE',
178 $SPA_NAT_ACCESS_MODE => 'SPA_NAT_ACCESS_MODE',
179 $SPA_CLIENT_TIMEOUT_ACCESS_MODE => 'SPA_CLIENT_TIMEOUT_ACCESS_MODE',
180 $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE => 'SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE',
181 $SPA_LOCAL_NAT_ACCESS_MODE => 'SPA_LOCAL_NAT_ACCESS_MODE',
182 $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE => 'SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE'
185 ### limits on nummber of fields within a decrypted SPA packet
186 my $SPA_MIN_PACKET_FIELDS = 7;
187 my $SPA_MAX_PACKET_FIELDS = 9;
189 ### default time values
190 my $knock_interval = 60;
191 my $default_access_timeout = 300;
193 my $enc_port_offset = 61000; ### default offset
195 my $enc_alg = 'Rijndael';
196 my $enc_blocksize = 32;
198 ### there is a constant "RIJNDAEL_KEYSIZE" in the Crypt::Rijndael sources, but
199 ### it is not used; a 16 byte key size is fine.
200 my $enc_keysize = 16;
202 my $ALG_RIJNDAEL = 1;
208 my $SHARED_SEQUENCE = 3;
209 my $ENCRYPT_SEQUENCE = 4;
211 ### Bool to detect Linux "Cooked" datalink layers
212 my $PCAP_COOKED_INTF = 0;
215 my $SHA256_DIGEST_LEN = 43;
216 my $SHA1_DIGEST_LEN = 27;
217 my $MD5_DIGEST_LEN = 22;
218 my $FKO_RECOMPUTE = 1;
236 my $IPPROTO_ICMP = 1;
238 my $IPPROTO_UDP = 17;
241 my $tcp_nop_type = 1;
242 my $tcp_mss_type = 2;
243 my $tcp_win_scale_type = 3;
244 my $tcp_sack_type = 4;
245 my $tcp_timestamp_type = 8;
247 my %tcp_p0f_opt_types = (
248 'N' => $tcp_nop_type,
249 'M' => $tcp_mss_type,
250 'W' => $tcp_win_scale_type,
251 'S' => $tcp_sack_type,
252 'T' => $tcp_timestamp_type
255 my $ETH_HDR_LEN = 14;
256 my $MIN_IP_HDR_LEN = 20;
257 my $MIN_ICMP_HDR_LEN = 8; ### most practical for SPA packets over ICMP
259 my $MIN_TCP_HDR_LEN = 20;
261 my $EXTERNAL_CMD_ALARM = 30; ### default for external commands
267 'GPG_REMOTE_ID' => '',
268 'GPG_DECRYPT_ID' => '',
269 'GPG_DECRYPT_PW' => '',
270 'GPG_HOME_DIR' => '',
271 'GPG_NO_OPTIONS' => 0,
272 'GPG_USE_OPTIONS' => 0,
273 'GPG_NO_REQUIRE_PREFIX' => 0,
278 'DATA_COLLECT_MODE' => '',
279 'ENCRYPT_SEQUENCE' => '',
280 'SHARED_SEQUENCE' => '',
282 'REQUIRE_AUTH_METHOD' => '',
284 'KNOCK_INTERVAL' => '',
286 'PERMIT_CLIENT_PORTS' => '',
287 'PERMIT_CLIENT_TIMEOUT' => '',
288 'ENABLE_FORWARD_ACCESS' => 0,
289 'ENABLE_CMD_EXEC' => '',
290 'DISABLE_FW_ACCESS' => '',
291 'REQUIRE_SOURCE_ADDRESS' => [],
292 'require_src_addr_exceptions' => [],
293 'INTERNAL_NET_ACCESS' => [], ### for --Forward-access IP restrictions
294 'internal_net_exceptions' => [],
296 'FW_ACCESS_TIMEOUT' => '',
297 'REQUIRE_USERNAME' => '',
298 'MIN_TIME_DIFF' => '',
299 'MAX_TIME_DIFF' => '',
300 'RESTRICT_INTF' => '',
301 'ENABLE_EXTERNAL_CMDS' => 0,
302 'EXTERNAL_CMD_OPEN' => '',
303 'EXTERNAL_CMD_CLOSE' => '',
304 'EXTERNAL_CMD_ALARM' => '',
307 my $ip_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|;
311 ### run GetOpt() to get comand line args
312 &handle_command_line();
314 &usage(0) if $print_help;
316 if ($print_version) {
317 print "[+] fwknopd v$version (file revision: $rev_num)\n",
318 " by Michael Rash <mbr\@cipherdyne.org>\n";
322 if ($os_fprint_only) {
323 print "[+] Entering OS fingerprinting mode.\n";
326 print STDERR localtime() . " [+] ** Starting fwknopd (debug mode) **\n",
327 " fwknopd Command line: @args_cp\n" if $debug;
332 if ($config{'AUTH_MODE'} eq 'KNOCK' or $os_fprint_only) {
334 ### we are running in traditional port knocking mode
337 } elsif ($config{'AUTH_MODE'} eq 'FILE_PCAP'
338 or $config{'AUTH_MODE'} eq 'ULOG_PCAP'
339 or $config{'AUTH_MODE'} eq 'PCAP') {
341 ### we are parsing the pcap file created by the ulogd pcap
342 ### writer, or in sniffing mode against an interface
347 print STDERR "[+] Net::Pcap::VERSION $Net::Pcap::VERSION\n";
352 } elsif ($config{'AUTH_MODE'} eq 'SOCKET') {
354 ### we are going to acquire SPA packet data from the fwknop_serv
355 ### process via a domain socket. fwknop_serv itself will listen
356 ### on a tcp or udp port for an incoming SPA packet, so libpcap
357 ### does not become involved in this mode.
363 #============================ end main ==============================
367 ### we use both a size and an inode check in the FILE_PCAP and
368 ### ULOG_PCAP modes to check if the file has been rotated
369 my $pcap_file_size = 0;
370 my $pcap_file_inode = 0;
373 my $pcap_t = &get_pcap_obj();
375 ### Check for "cooked" Linux datalink layers (i.e. rp-pppoe)
377 if (not $PCAP_COOKED_INTF and $Net::Pcap::VERSION > 0.05) {
378 if (Net::Pcap::datalink_val_to_name(
379 Net::Pcap::datalink($pcap_t)) eq 'LINUX_SLL') {
380 print STDERR "[+] Detected Linux Cooked Interface.\n" if $debug;
381 $PCAP_COOKED_INTF = 1;
385 &collect_warn_die_msgs() if $@;
387 if ($config{'AUTH_MODE'} eq 'FILE_PCAP'
388 or $config{'AUTH_MODE'} eq 'ULOG_PCAP') {
389 ### get file size (we don't need a -e check here because
390 ### this is handled in get_pcap_obj()).
391 $pcap_file_size = -s $config{'PCAP_PKT_FILE'};
393 ### get inode associated with the sniffing file
394 $pcap_file_inode = (stat($config{'PCAP_PKT_FILE'}))[1];
396 print STDERR localtime() . " [+] pcap_loop()\n" if $debug;
398 my $check_file_ctr = 0;
400 &collect_warn_die_msgs();
404 my @tmpcbargs; my $cbcalled = 0;
409 @tmpcbargs = ($tag, \%hdr, $pkt);
413 Net::Pcap::loop($pcap_t, 1, $tmpcb, 'fwknop_tag');
414 pcap_process_pkt(@tmpcbargs) if($cbcalled);
416 if ($config{'AUTH_MODE'} eq 'FILE_PCAP'
417 or $config{'AUTH_MODE'} eq 'ULOG_PCAP') {
419 ### check to see if the pcap file has been rotated (we need to
420 ### close and re-open)
421 if ($check_file_ctr >= 10) {
422 if (-e $config{'PCAP_PKT_FILE'}) {
423 my $size_tmp = -s $config{'PCAP_PKT_FILE'};
424 my $inode_tmp = (stat($config{'PCAP_PKT_FILE'}))[1];
425 if ($inode_tmp != $pcap_file_inode
426 or $size_tmp < $pcap_file_size) {
428 ### the file was rotated or shrank, so get new
430 Net::Pcap::close($pcap_t);
432 &logr('[+]', "pcap file $config{'PCAP_PKT_FILE'} " .
433 "shrank or was rotated, so re-opening", $NO_MAIL);
434 $pcap_t = &get_pcap_obj();
436 ### set file size and inode
437 $pcap_file_size = $size_tmp;
438 $pcap_file_inode = $inode_tmp;
441 Net::Pcap::close($pcap_t);
442 &logr('[+]', "pcap file $config{'PCAP_PKT_FILE'} " .
443 "was rotated, so re-opening", $NO_MAIL);
444 $pcap_t = &get_pcap_obj();
446 ### set file size and inode
447 $pcap_file_size = -s $config{'PCAP_PKT_FILE'};
448 $pcap_file_inode = (stat($config{'PCAP_PKT_FILE'}))[1];
455 &collect_warn_die_msgs();
456 sleep $pcap_sleep_interval;
459 Net::Pcap::close($pcap_t);
464 sub pcap_process_pkt() {
465 my ($tag, $hdr, $pkt) = @_;
467 &collect_warn_die_msgs();
469 return unless $tag eq 'fwknop_tag';
470 return unless defined $hdr;
471 return unless defined $pkt;
477 my $transport_data = '';
480 print STDERR localtime() . " [+] Received packet ***[" .
481 localtime() . "]***\n";
483 print STDERR localtime() .
484 " Complete raw packet data (hex dump, including ",
485 "packet headers):\n";
490 ### check the length of the packet; if it is not at least
491 ### 160 bytes long (this is the default MIN_SPA_PKT_LEN value, and
492 ### this is conservative) then it cannot be an SPA packet
493 my $pkt_len = length($pkt);
494 if (length($pkt) < $config{'MIN_SPA_PKT_LEN'}) {
495 if ($debug and $verbose) {
497 "[-] Packet length ($pkt_len bytes) less than $config{'MIN_SPA_PKT_LEN'}\n",
498 " minimum, so is not an SPA packet; skipping.\n";
503 if ($config{'AUTH_MODE'} eq 'ULOG_PCAP') {
504 ### The ulogd pcap writer does not include link layer information
505 $ip = &ip_decode($pkt) or return;
507 if ($config{'FIREWALL_TYPE'} eq 'ipfw'
508 and $config{'PCAP_INTF'} eq 'lo0') {
510 ### it seems that FreeBSD does not include an Ethernet header
511 ### over loopback but puts a different set of four bytes
512 $pkt =~ s/^.{4}// if $pkt =~ /^[^\x45].{3}\x45/;
514 $ip = &ip_decode($pkt) or return;
516 if ($PCAP_COOKED_INTF) {
517 $ether_data = unpack("x16a*", $pkt);
519 $ether_data = ðernet_strip($pkt) or return;
521 $ip = &ip_decode($ether_data) or return;
525 ### get the source IP address from the IP header
526 $src_ip = $ip->{'src_ip'} or return;
529 $proto = $ip->{'proto'} or return;
531 if ($proto == $IPPROTO_ICMP) {
532 $transport_data = &icmp_decode_data($ip->{'data'});
533 } elsif ($proto == $IPPROTO_TCP) {
534 $transport_data = &tcp_decode_data($ip->{'data'});
535 } elsif ($proto == $IPPROTO_UDP) {
536 $transport_data = &udp_decode_data($ip->{'data'});
541 &decode_SPA_data($transport_data, $src_ip, $proto);
546 sub decode_SPA_data() {
547 my ($transport_data, $src_ip, $proto) = @_;
549 ### make sure we have _some_ data in the packet; in practice
550 ### any valid SPA message will be longer than 10 bytes, but this
551 ### check is better than nothing
552 return if $transport_data eq '';
555 $enc_msg_len = length($transport_data);
556 if (10 < $enc_msg_len and $enc_msg_len < $config{'MAX_SNIFF_BYTES'}) {
557 print STDERR localtime() . " [+] Data len: $enc_msg_len bytes\n"
560 print STDERR localtime() . " [-] $enc_msg_len bytes, not ",
561 "attempting decrypt)\n" if $debug;
566 ### make sure not to print non-printable stuff
567 my $data_tmp = $transport_data;
568 $data_tmp =~ s/[^\x20-\x7e]/NA/g;
569 print STDERR localtime() .
570 " [+] Raw packet data (single line): $data_tmp\n";
572 ### print packet data out in tcpdump -X format
574 print STDERR localtime() .
575 " Raw packet data (hex dump, minus packet headers):\n";
576 &hex_dump($transport_data);
580 my $candidate_spa_data = '';
582 if ($proto == $IPPROTO_TCP and $config{'ENABLE_SPA_OVER_HTTP'} eq 'Y') {
583 if ($transport_data =~ m|GET\s+(\S+)\s+HTTP/\d|) {
584 $candidate_spa_data = $1;
586 $candidate_spa_data =~ s/\.html// if $candidate_spa_data =~ /\.html/;
587 $candidate_spa_data =~ s|^/|| if $candidate_spa_data =~ m|^/|;
588 $candidate_spa_data =~ s|^http://\S+/||
589 if $candidate_spa_data =~ m|^http://\S+/|;
591 unless (&is_url_base64($candidate_spa_data)) {
593 print STDERR localtime() . " [+] Packet contains non-base64 ",
594 "(with URL mods) encoded characters, skipping.\n";
595 &check_packet_limit();
602 unless ($candidate_spa_data) {
603 $candidate_spa_data = $transport_data;
605 ### check to make sure the packet data only contains base64 encoded
606 ### characters per RFC 3548: 0-9, A-Z, a-z, +, /, =
607 unless (&is_base64($candidate_spa_data)) {
609 print STDERR localtime() . " [+] Packet contains non-base64 ",
610 "encoded characters, skipping.\n";
611 &check_packet_limit();
617 ### see if this packet is worthy of being granted access through
619 &SPA_check_grant_access($src_ip, $enc_msg_len, $candidate_spa_data);
621 &collect_warn_die_msgs();
626 sub ethernet_strip() {
631 if (length($pkt) >= $ETH_HDR_LEN) {
632 $eth_data = substr($pkt, $ETH_HDR_LEN);
635 if (not $eth_data and ($debug and $verbose)) {
636 print "[-] Could not properly decode Ethernet header.\n";
638 ### Silently return '' for short frames
642 sub ip_addr_bytes_to_string() {
645 my ($a, $b, $c, $d) = unpack('C4', $bytes);
646 return "$a.$b.$c.$d";
653 if (length($pkt) >= $MIN_IP_HDR_LEN and $pkt =~ /^\x45/) {
654 (my $ver_ihl, $ip->{'tos'}, $ip->{'len'}, $ip->{'id'}, my $flags_frag,
655 $ip->{'ttl'}, $ip->{'proto'}, $ip->{'cksum'}, my $src_ip, my $dest_ip)
656 = unpack("CCnnnCCna4a4", $pkt);
657 $ip->{'ver'} = $ver_ihl >> 4;
658 $ip->{'hlen'} = $ver_ihl & 0x0F;
659 $ip->{'flags'} = $flags_frag >> 13;
660 $ip->{'foffset'} = ($flags_frag & 0x1FFF) * 8;
661 $ip->{'src_ip'} = &ip_addr_bytes_to_string($src_ip);
662 $ip->{'dest_ip'} = &ip_addr_bytes_to_string($dest_ip);
663 my $data_start = $ip->{'hlen'} * 4;
664 if ($data_start >= $MIN_IP_HDR_LEN) {
665 $ip->{'data'} = substr($pkt, $data_start);
668 if (not keys %$ip and ($debug and $verbose)) {
669 print "[-] Could not properly decode IP header.\n";
674 sub icmp_decode_data() {
678 if (length($icmp) >= $MIN_ICMP_HDR_LEN) {
679 $icmp_data = substr($icmp, $MIN_ICMP_HDR_LEN);
681 ### Silently return '' for short packets
682 if (not $icmp_data and ($debug and $verbose)) {
683 print "[-] Could not properly decode ICMP header.\n";
688 sub tcp_decode_data() {
693 if (length($tcp) >= $MIN_TCP_HDR_LEN) {
695 my $data_start = 4 * (ord(substr($tcp, 12, 1)) >> 4);
696 if ($data_start >= $MIN_TCP_HDR_LEN) {
697 $tcp_data = substr($tcp, $data_start);
700 ### Silently return '' for short packets
701 if (not $tcp_data and ($debug and $verbose)) {
702 print "[-] Could not properly decode TCP header.\n";
707 sub udp_decode_data() {
711 if (length($udp) >= $UDP_HDR_LEN) {
712 $udp_data = substr($udp, $UDP_HDR_LEN);
714 ### Silently return '' for short packets
715 if (not $udp_data and ($debug and $verbose)) {
716 print "[-] Could not properly decode UDP header.\n";
723 print STDERR localtime() . " [+] socket_loop() acquiring SPA ",
724 "packet data from: $cmds{'fwknop_serv'} via domain socket: ",
725 "$config{'FWKNOP_SERV_SOCK'}\n";
727 my $fwknop_serv_sock = IO::Socket::UNIX->new(
729 Local => $config{'FWKNOP_SERV_SOCK'},
732 ) or die "[*] Could not acquire fwknopd communications domain socket: $!";
736 my $fwknop_serv_connection = $fwknop_serv_sock->accept();
737 if ($fwknop_serv_connection) {
739 my @fwknop_serv_msgs = <$fwknop_serv_connection>;
740 for my $msg (@fwknop_serv_msgs) {
741 if ($msg =~ /^($ip_re):(\d{1,2}):(\S+)/) {
746 &decode_SPA_data($spa_msg, $src_ip, $proto);
749 @fwknop_serv_msgs = ();
755 sub SPA_check_grant_access() {
756 my ($src_ip, $enc_msg_len, $pkt_data) = @_;
758 if ($spa_dump_packets) {
759 if (&is_base64($pkt_data)) {
760 print "\nLen: $enc_msg_len, pkt: $pkt_data\n";
762 print "\nLine contains non base64 chars, skipping.\n";
767 ### first check to see if we have any matching access directives
768 ### (in access.conf) for $src_ip, and if not we will do _nothing_
769 ### with this packet.
770 my $access_nums_aref = &check_src($src_ip);
772 if ($#$access_nums_aref > -1) {
774 ### See if the packet qualifies for any access
775 SOURCE: for my $num (@$access_nums_aref) {
776 my $access_hr = $access[$num];
778 next SOURCE unless $access_hr->{'DATA_COLLECT_MODE'} == $PCAP
779 or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP
780 or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP;
782 &dump_access($access_hr, $num) if $debug and $verbose;
784 ### keep track of which source block we are dealing with from
786 my $source_block_num = $access_hr->{'block_num'};
788 ### see if we can decrypt and base64-decode
789 &fko_acquire_object() if $use_fko_module;
790 my ($decrypt_rv, $decrypted_msg, $gpg_sign_id, $decrypt_algo)
791 = &SPA_decrypt($pkt_data, $enc_msg_len, $access_hr);
793 unless ($decrypt_rv) {
794 &fko_destroy_object() if $use_fko_module;
798 ### check for replay attacks
799 my ($digest_rv, $digest)
800 = &is_replay_attack($decrypted_msg, $src_ip);
802 &fko_destroy_object() if $use_fko_module;
806 ### see if we have a syntactically valid message - this
807 ### also runs the check_digest() function to validate the
808 ### internal digest against the decrypted data.
809 my ($validate_rv, $msg_hr) = &pcap_validate_msg(
810 $decrypted_msg, $source_block_num, $access_hr);
811 if ($debug and not $validate_rv) {
812 print STDERR localtime() . " [-] Decrypted message does not ",
813 "conform to a valid SPA packet.\n";
815 unless ($validate_rv) {
816 &fko_destroy_object() if $use_fko_module;
820 if ($spa_dump_packets) {
821 print " Disk write digest: $digest\n";
822 for my $key (keys %$msg_hr) {
823 printf " %20s -> %s\n", $key, $msg_hr->{$key};
828 ### check to see if client side time stamp is too old
829 my $time_check_rv = &SPA_check_packet_age($msg_hr->{'remote_time'});
830 unless ($validate_rv) {
831 &fko_destroy_object() if $use_fko_module;
834 next SOURCE unless $time_check_rv;
836 ### dump packet to stderr for debugging purposes
837 &SPA_dump_packet($msg_hr) if $debug;
840 next SOURCE unless &SPA_check_user($access_hr, $src_ip, $msg_hr);
841 unless ($validate_rv) {
842 &fko_destroy_object() if $use_fko_module;
846 ### check authentication method
847 unless (&SPA_check_auth_method($access_hr, $src_ip, $msg_hr)) {
848 &fko_destroy_object() if $use_fko_module;
852 if ($msg_hr->{'action_type'} == $SPA_ACCESS_MODE
853 or $msg_hr->{'action_type'} == $SPA_NAT_ACCESS_MODE
854 or $msg_hr->{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE
855 or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE
856 or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE
857 or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) {
859 if (&SPA_access($msg_hr, $src_ip, $decrypt_algo,
860 $gpg_sign_id, $digest, $access_hr)) {
861 &fko_destroy_object() if $use_fko_module;
864 &fko_destroy_object() if $use_fko_module;
867 } elsif ($msg_hr->{'action_type'} == $SPA_COMMAND_MODE) {
868 if (&SPA_cmd($msg_hr, $src_ip, $decrypt_algo,
869 $gpg_sign_id, $digest, $access_hr)) {
870 &fko_destroy_object() if $use_fko_module;
873 &fko_destroy_object() if $use_fko_module;
879 print STDERR localtime() . " [-] Packet from $src_ip did not ",
880 "match any SOURCE blocks in $config{'ACCESS_CONF'}\n" if $debug;
883 &check_packet_limit();
887 sub check_packet_limit() {
888 ### see if we need to exit if the packet limit (set with -C on the
889 ### command line) has been reached
890 return unless $packet_limit;
893 if ($packet_ctr >= $packet_limit) {
894 &logr('[+]', "packet limit ($packet_limit) reached, exiting.",
896 if ($knoptm_debug_file and -e $knoptm_debug_file) {
897 &logr('[+]', "collecting knoptm debug messages " .
898 "from $knoptm_debug_file", $NO_MAIL);
899 open F, "< $knoptm_debug_file" or die $!;
902 &logr("KNOPTM:", $_, $NO_MAIL);
906 if ($fwkserv_debug_file and -e $fwkserv_debug_file) {
907 &logr('[+]', "collecting fwknop_serv debug messages " .
908 "from $fwkserv_debug_file", $NO_MAIL);
909 open F, "< $fwkserv_debug_file" or die $!;
912 &logr("FWKNOP_SERV:", $_, $NO_MAIL);
922 my ($pkt_data, $enc_msg_len, $access_hr) = @_;
924 my $decrypted_msg = '';
925 my $decrypt_algo = $ALG_RIJNDAEL;
926 my $gpg_sign_id = '';
930 print STDERR localtime() . " [+] Attempting to ",
931 "decrypt the following data ($enc_msg_len bytes):\n";
932 &hex_dump($pkt_data);
935 if (not $cmdl_disable_gpg
936 and $enc_msg_len > $config{'MIN_GNUPG_MSG_SIZE'}
937 and defined $access_hr->{'GPG_REMOTE_ID'}) {
938 ### attempt GPG decrypt (only if the length of the encrypted
939 ### payload is greater than the minimum size for an SPA message
940 ### encrypted with GnuPG; even encrypting a single byte of data
941 ### with a 1024 bit GnuPG key results in 340 bytes of encrypted
942 ### payload in my testing).
943 ($decrypt_rv, $decrypted_msg, $gpg_sign_id) =
944 &pcap_GPG_decrypt_msg($pkt_data, $access_hr);
946 $decrypt_algo = $ALG_GNUPG if $decrypt_rv;
949 ### fall back to Rijndael if the GnuPG decrypt was not successful
950 ### (and note that the GnuPG decryption is only attempted if the
951 ### packet size is large enough).
952 if (defined $access_hr->{'KEY'} and not $decrypt_rv) {
954 ($decrypt_rv, $decrypted_msg) = &pcap_Rijndael_decrypt_msg(
955 $pkt_data, $access_hr->{'KEY'});
959 if ($debug and not $use_fko_module) {
960 ### make sure not to print non-printable stuff
961 my $dec_tmp_msg = $decrypted_msg;
962 $dec_tmp_msg =~ s/[^\x20-\x7e]/NA/g;
963 print STDERR localtime() . " [+] Decrypted ",
964 "message: $dec_tmp_msg\n";
966 print STDERR localtime() . " Decrypted message (hex dump):\n";
967 &hex_dump($decrypted_msg);
971 print STDERR localtime() . " [-] Failed decrypt for SOURCE block ",
972 "$access_hr->{'src_str'}\n" if $debug;
975 return $decrypt_rv, $decrypted_msg, $gpg_sign_id, $decrypt_algo;
978 sub SPA_check_packet_age() {
979 my $remote_time = shift;
981 if ($config{'ENABLE_SPA_PACKET_AGING'} eq 'Y') {
982 my $time_diff = time() - $remote_time;
983 if (abs($time_diff) > $config{'MAX_SPA_PACKET_AGE'}) {
984 &logr('[-]', "remote time stamp age difference is larger than " .
985 "$config{'MAX_SPA_PACKET_AGE'} second max.", $SEND_MAIL);
986 print STDERR localtime() . " [-] Time difference: $time_diff " .
987 "(seconds), " . ($time_diff / 3600) . " (hours)\n";
994 sub SPA_dump_packet() {
997 print STDERR localtime() . " [+] Packet fields:\n";
998 printf STDERR " %-16s %s\n %-16s %s\n %-16s %s\n" .
999 " %-16s %s\n %-16s %s",
1000 'Random data:', $msg_hr->{'random_number'},
1001 'Username:', $msg_hr->{'username'},
1002 'Remote time:', $msg_hr->{'remote_time'},
1003 'Remote ver:', $msg_hr->{'remote_version'},
1004 'Action type:', $msg_hr->{'action_type'};
1006 for my $action_type (keys %spa_mode_strings) {
1007 if ($msg_hr->{'action_type'} == $action_type) {
1008 print STDERR " ($spa_mode_strings{$action_type})\n";
1013 printf STDERR " %-16s %s\n",
1014 'Action:', $msg_hr->{'action'};
1016 if ($msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE
1017 or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE
1018 or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) {
1019 printf STDERR " %-16s %s\n",
1020 'Client timeout:', $msg_hr->{'client_timeout'};
1023 if ($msg_hr->{'server_auth'}) {
1024 if ($msg_hr->{'server_auth'} =~ /^\s*(\w+),(.*)/) {
1025 my $server_auth_type = lc($1);
1026 my $server_auth_crypt_pw = $2;
1028 printf STDERR " %-16s %s", 'Server auth:', $server_auth_type;
1029 for (my $i=0; $i < length($server_auth_crypt_pw); $i++) {
1036 if ($msg_hr->{'nat_info'}) {
1037 printf STDERR " %-16s %s\n", 'NAT info:',
1038 $msg_hr->{'nat_info'};
1040 printf STDERR " %-16s %s\n", "$msg_hr->{'digest_str'} digest:",
1041 $msg_hr->{'digest'};
1045 sub SPA_check_user() {
1046 my ($access_hr, $src_ip, $msg_hr) = @_;
1048 if (defined $access_hr->{'REQUIRE_USERNAME'}) {
1051 for my $valid_user (@{$access_hr->{'VALID_USERS'}}) {
1052 if ($valid_user eq $msg_hr->{'username'}) {
1054 $user = $valid_user;
1058 &logr('[-]', "username mismatch from $src_ip, expecting " .
1059 "$access_hr->{'REQUIRE_USERNAME'}, got " .
1060 "$msg_hr->{'username'}", $SEND_MAIL);
1067 sub SPA_check_auth_method() {
1068 my ($access_hr, $src_ip, $msg_hr) = @_;
1070 my $server_auth_type = '';
1071 my $server_auth_crypt_pw = '';
1072 if ($msg_hr->{'server_auth'}) {
1073 if ($msg_hr->{'server_auth'} =~ /^\s*(\w+),(.*)/) {
1074 $server_auth_type = lc($1);
1075 $server_auth_crypt_pw = $2;
1079 if (defined $access_hr->{'REQUIRE_AUTH_METHOD'}) {
1080 if ($server_auth_type
1081 eq $access_hr->{'REQUIRE_AUTH_METHOD'}) {
1082 if ($server_auth_type eq 'crypt') {
1083 ### check the local UNIX crypt() password associated
1085 unless (&server_auth_verify_crypt_pw(
1086 $msg_hr->{'username'},
1087 $server_auth_crypt_pw,
1088 $access_hr->{'SHADOW_FILE'})) {
1089 &logr('[-]', "IP: $src_ip failed server-auth UNIX " .
1090 "crypt() password test", $NO_MAIL);
1095 &logr('[-]', "required server-auth method " .
1096 "\"$access_hr->{'REQUIRE_AUTH_METHOD'}\" " .
1097 "not supplied by $src_ip", $NO_MAIL);
1105 my ($msg_hr, $src_ip, $decrypt_algo, $gpg_sign_id,
1106 $digest, $access_hr) = @_;
1109 my %open_ports = ();
1110 my %grant_ports = ();
1112 my $grant_access = 0;
1114 if ($access_hr->{'DISABLE_FW_ACCESS'}) {
1115 &logr('[-]', "received fw access request from $src_ip, " .
1116 "but DISABLE_FW_ACCESS is set to a true value " .
1117 "(SOURCE line num: $access_hr->{'src_line_num'})", $NO_MAIL);
1121 if ($msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE
1122 or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE
1123 or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) {
1125 if ($access_hr->{'PERMIT_CLIENT_TIMEOUT'}) {
1126 $access_hr->{'FW_ACCESS_TIMEOUT'} = $msg_hr->{'client_timeout'};
1128 &logr('[-]', "received fw access request from $src_ip, " .
1129 "with client-defined timeout, but PERMIT_CLIENT_TIMEOUT is not " .
1130 "set (SOURCE line num: $access_hr->{'src_line_num'})", $NO_MAIL);
1135 $allow_src = $1 if $msg_hr->{'action'} =~ /($ip_re)/;
1137 unless ($allow_src) {
1138 &logr('[-]', "no valid IP address within action portion of SPA " .
1139 "packet from $src_ip (SOURCE line num: " .
1140 "$access_hr->{'src_line_num'})", $SEND_MAIL);
1144 if ($allow_src eq '0.0.0.0') {
1145 if ($config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y' or not
1146 &is_ip_included($src_ip,
1147 $access_hr->{'REQUIRE_SOURCE_ADDRESS'},
1148 $access_hr->{'require_src_addr_exceptions'})) {
1149 &logr('[-]', "IP: $src_ip sent SPA packet that " .
1150 "contained 0.0.0.0 (-s on the client side) " .
1151 "but REQUIRE_SOURCE_ADDRESS is enabled " .
1152 "(SOURCE line num: $access_hr->{'src_line_num'})",
1156 $allow_src = $src_ip;
1160 if (&is_ip_included($allow_src, $blacklist_ar, $blacklist_exclude_ar)) {
1161 print STDERR localtime() . " [+] SPA_access() ",
1162 "$allow_src in BLACKLIST" if $debug;
1163 &logr('[-]', "allow IP: $allow_src SPA packet from $src_ip is " .
1164 "blacklisted (SOURCE line num: " .
1165 "$access_hr->{'src_line_num'})", $SEND_MAIL);
1169 ### initialize to the OPEN_PORTS directives (if defined; we know that
1170 ### either OPEN_PORTS or PERMIT_CLIENT_PORTS was specified in the
1171 ### access.conf file)
1172 %open_ports = %{$access_hr->{'OPEN_PORTS'}}
1173 if defined $access_hr->{'OPEN_PORTS'};
1175 if ($access_hr->{'ENABLE_EXTERNAL_CMDS'}
1176 or ($config{'FIREWALL_TYPE'} eq 'external_cmd'
1177 and $config{'ENABLE_EXTERNAL_CMDS'} eq 'Y')) {
1182 if ($msg_hr->{'action'} =~ /$ip_re,(tcp|udp|icmp),(\d+)/i) {
1184 ### single port access format (e.g. tcp,22)
1185 my $allow_port = $1;
1186 my $allow_proto = $2;
1188 if ($access_hr->{'PERMIT_CLIENT_PORTS'}) {
1189 $grant_ports{$allow_proto}{$allow_port} = '';
1192 if (defined $open_ports{$allow_proto} and
1193 defined $open_ports{$allow_proto}{$allow_port}) {
1194 $grant_ports{$allow_proto}{$allow_port} = '';
1197 unless ($grant_access) {
1198 &logr('[-]', "IP $allow_src not permitted to open " .
1199 "$allow_proto/$allow_port (SOURCE line num: " .
1200 "$access_hr->{'src_line_num'})", $NO_MAIL);
1206 } elsif ($msg_hr->{'action'} =~ /$ip_re,(\S+)/) {
1208 ### multi-port access format (-A was specified by
1210 my $access_str = $1;
1212 my @dec_allow_ports = split /,/, $access_str;
1214 for my $port_str (@dec_allow_ports) {
1215 if ($port_str =~ m|(\D+)/(\d+)|) {
1219 next unless ($proto eq 'tcp'
1221 or $proto eq 'icmp');
1222 $port = 0 if $proto eq 'icmp';
1224 if ($access_hr->{'PERMIT_CLIENT_PORTS'}) {
1225 $grant_ports{$proto}{$port} = '';
1228 if (defined $open_ports{$proto} and
1229 defined $open_ports{$proto}{$port}) {
1230 $grant_ports{$proto}{$port} = '';
1233 unless ($grant_access) {
1234 &logr('[-]', "IP $allow_src not permitted to " .
1235 "open $proto/$port (SOURCE line num: " .
1236 "$access_hr->{'src_line_num'})", $NO_MAIL);
1245 ### handle SPA access through iptables FORWARD chain for
1246 ### SPA_NAT_ACCESS_MODE messages (or through the INPUT chain for
1247 ### SPA_LOCAL_NAT_ACCESS_MODE messages)
1248 ### iptables -t nat -A PREROUTING -p tcp -s <SPA_src> --dport 55000 \
1249 ### -i eth0 -j DNAT --to 192.168.10.3:80
1250 if ($msg_hr->{'nat_info'}
1251 and $msg_hr->{'nat_info'} =~ /($ip_re),(\d+)/) {
1254 'internal_ip' => $1,
1255 'external_port' => $2,
1258 unless ($config{'FIREWALL_TYPE'} eq 'iptables') {
1259 &logr('[-]', "NAT access requested through non-iptables " .
1260 "firewall (SOURCE line num: ".
1261 "$access_hr->{'src_line_num'})", $NO_MAIL);
1265 if ($msg_hr->{'action_type'} == $SPA_NAT_ACCESS_MODE
1266 or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE) {
1267 unless ($access_hr->{'ENABLE_FORWARD_ACCESS'}) {
1268 &logr('[-]', "FORWARD access requested through non-forward " .
1269 "access SOURCE block (SOURCE line num: ".
1270 "$access_hr->{'src_line_num'})", $NO_MAIL);
1275 if ($msg_hr->{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE
1276 or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) {
1277 unless ($config{'ENABLE_IPT_LOCAL_NAT'} eq 'Y') {
1278 &logr('[-]', "Local NAT access requested without " .
1279 "ENABLE_IPT_LOCAL_NAT enabled", $NO_MAIL);
1284 ### check to see if access is allowed to internal IP (or a local IP
1285 ### for NAT'd local connections)
1286 unless (&is_ip_included($nat_info{'internal_ip'},
1287 $access_hr->{'INTERNAL_NET_ACCESS'},
1288 $access_hr->{'internal_net_exceptions'})) {
1289 &logr('[-]', "NAT access to $nat_info{'internal_ip'} " .
1290 "restricted (SOURCE line num: ".
1291 "$access_hr->{'src_line_num'})", $NO_MAIL);
1295 for my $proto (keys %grant_ports) {
1296 for my $port (keys %{$grant_ports{$proto}}) {
1300 ### we can only map one forwarding port on the external interface
1301 ### to be forwarded to one internal service
1302 if ($port_ctr > 1) {
1303 &logr('[-]', "cannot forward more than one port " .
1304 "(SOURCE line num: $access_hr->{'src_line_num'})", $NO_MAIL);
1308 if ($access_hr->{'ENABLE_FORWARD_ACCESS'}) {
1309 &logr('[-]', "non-forward access requested through FORWARD " .
1310 "access SOURCE block (SOURCE line num: " .
1311 "$access_hr->{'src_line_num'})", $NO_MAIL);
1316 if ($decrypt_algo == $ALG_GNUPG) {
1317 if ($access_hr->{'GPG_REMOTE_ID'} ne 'ANY') {
1318 &logr('[+]', "received valid GnuPG encrypted packet " .
1319 qq|(signed with required key ID: "$gpg_sign_id") from: | .
1320 "$src_ip, remote user: $msg_hr->{'username'}, " .
1321 "client version: $msg_hr->{'remote_version'} " .
1322 "(SOURCE line num: $access_hr->{'src_line_num'})", $NO_MAIL);
1324 &logr('[+]', "received valid GnuPG encrypted packet " .
1325 "from: $src_ip, remote user: $msg_hr->{'username'}, " .
1326 "client version: $msg_hr->{'remote_version'} " .
1327 "(SOURCE line num: $access_hr->{'src_line_num'})",
1331 &logr('[+]', "received valid Rijndael encrypted " .
1332 "packet from: $src_ip, remote user: $msg_hr->{'username'}, " .
1333 "client version: $msg_hr->{'remote_version'} " .
1334 "(SOURCE line num: $access_hr->{'src_line_num'})",
1338 unless ($grant_access) {
1339 &logr('[-]', "Could not work out access to ports from SPA packet " .
1340 "originating from: $src_ip", $NO_MAIL);
1344 ### cache the digest
1345 $digest_store{$digest} = $src_ip;
1347 ### write digest to disk
1348 &diskwrite_digest($digest, $src_ip)
1349 if $config{'ENABLE_DIGEST_PERSISTENCE'} eq 'Y';
1351 ### grant access through the firewall
1352 &grant_access($allow_src, $msg_hr, \%nat_info,
1353 {}, \%grant_ports, $access_hr);
1359 my ($msg_hr, $src_ip, $decrypt_algo, $gpg_sign_id,
1360 $digest, $access_hr) = @_;
1362 unless ($access_hr->{'ENABLE_CMD_EXEC'}) {
1363 &logr('[-]', qq|received command "$msg_hr->{'action'}" | .
1364 "but command mode not enabled for $src_ip", $SEND_MAIL);
1368 if (defined $access_hr->{'CMD_REGEX'}) {
1369 unless ($msg_hr->{'action'} =~ m|$access_hr->{'CMD_REGEX'}|) {
1370 &logr('[-]', qq|received command "$msg_hr->{'action'}" | .
1371 "from $src_ip but CMD_REGEX did not match $src_ip",
1377 my $cmd = $msg_hr->{'action'};
1381 if ($cmd =~ m|^\s*($ip_re),(.*)|) {
1388 ### pre-1.0 versions did not prepend command string with "<ip>,"
1389 if ($cmd_ip eq '0.0.0.0') {
1390 if ($config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y' or not
1391 &is_ip_included($cmd_ip,
1392 $access_hr->{'REQUIRE_SOURCE_ADDRESS'},
1393 $access_hr->{'require_src_addr_exceptions'})) {
1394 &logr('[-]', "IP: $src_ip sent SPA packet that " .
1395 "contained 0.0.0.0 (-s on the client side) " .
1396 "but REQUIRE_SOURCE_ADDRESS is enabled " .
1397 "(SOURCE line num: $access_hr->{'src_line_num'})",
1403 if (&is_ip_included($cmd_ip, $blacklist_ar, $blacklist_exclude_ar)) {
1404 print STDERR localtime() . " [+] SPA_cmd() ",
1405 "$cmd_ip in BLACKLIST" if $debug;
1406 &logr('[-]', "cmd IP: $cmd_ip SPA packet from $src_ip is " .
1407 "blacklisted (SOURCE line num: " .
1408 "$access_hr->{'src_line_num'})", $SEND_MAIL);
1412 if ($decrypt_algo == $ALG_GNUPG) {
1413 if ($access_hr->{'GPG_REMOTE_ID'} ne 'ANY') {
1414 &logr('[+]', "received valid GnuPG encrypted packet " .
1415 qq|(signed with required key ID: "$gpg_sign_id") from: | .
1416 "$src_ip, remote user: $msg_hr->{'username'}",
1419 &logr('[+]', "received valid GnuPG encrypted packet " .
1420 "from: $src_ip, remote user: $msg_hr->{'username'}",
1424 &logr('[+]', "received valid Rijndael encrypted " .
1425 "packet from: $src_ip, remote user: $msg_hr->{'username'}",
1429 &logr('[+]', qq|executing command "$run_cmd" for $src_ip|, $SEND_MAIL);
1431 ### cache the digest
1432 $digest_store{$digest} = $src_ip;
1434 ### write the digest to disk
1435 &diskwrite_digest($digest, $src_ip)
1436 if $config{'ENABLE_DIGEST_PERSISTENCE'} eq 'Y';
1438 ### execute the command
1439 &exec_command($run_cmd, $config{'PCAP_CMD_TIMEOUT'});
1444 sub external_cmd_open() {
1445 my ($src, $msg_hr, $open_ports_hr, $access_hr) = @_;
1450 my $cmd_proto = 'NA';
1451 my $found_port_proto = 0;
1452 my $cmd_alarm = $EXTERNAL_CMD_ALARM;
1454 if ($access_hr->{'EXTERNAL_CMD_OPEN'}) {
1455 $open_cmd = $access_hr->{'EXTERNAL_CMD_OPEN'};
1456 $close_cmd = $access_hr->{'EXTERNAL_CMD_CLOSE'};
1457 $cmd_alarm = $access_hr->{'EXTERNAL_CMD_ALARM'};
1458 } elsif ($config{'EXTERNAL_CMD_OPEN'} and $config{'EXTERNAL_CMD_CLOSE'}) {
1459 $open_cmd = $config{'EXTERNAL_CMD_OPEN'};
1460 $close_cmd = $config{'EXTERNAL_CMD_CLOSE'};
1461 $cmd_alarm = $config{'EXTERNAL_CMD_ALARM'};
1466 PROTO: for my $proto (keys %{$open_ports_hr}) {
1467 for my $port (keys %{$open_ports_hr->{$proto}}) {
1468 ### only allow one port/proto substitution for now - this can be
1469 ### worked around by passing OPEN_PORTS directly (via key
1470 ### substitution below) on the external command line.
1472 $cmd_proto = $proto;
1473 $found_port_proto = 1;
1478 ### perform variable substitutions on the external command to run
1479 $open_cmd = &external_cmd_str_expand($open_cmd, $src, $cmd_port,
1480 $cmd_proto, $access_hr);
1481 $close_cmd = &external_cmd_str_expand($close_cmd, $src, $cmd_port,
1482 $cmd_proto, $access_hr);
1484 &logr('[+]', qq|executing external open command "$open_cmd" for $src|,
1487 ### execute the "open" command
1488 &exec_command($open_cmd, $cmd_alarm);
1490 ### let knoptm run the "close" command
1491 &write_knoptm_fw_cache_entry(
1493 $access_hr->{'FW_ACCESS_TIMEOUT'},
1505 encode_base64($close_cmd, ''),
1512 sub external_cmd_str_expand() {
1513 my ($cmd_str, $src, $cmd_port, $cmd_proto, $access_hr) = @_;
1515 print STDERR localtime() . " [+] External command ",
1516 "(before var expansion): $cmd_str\n" if $debug;
1518 ### expand SPA source IP, port, and protocol
1519 if ($config{'ENABLE_EXT_CMD_PREFIX'} eq 'Y') {
1520 $cmd_str =~ s|\$$config{'EXT_CMD_PREFIX'}SRC|$src|;
1521 $cmd_str =~ s|\$$config{'EXT_CMD_PREFIX'}PORT|$cmd_port|;
1522 $cmd_str =~ s|\$$config{'EXT_CMD_PREFIX'}PROTO|$cmd_proto|;
1524 $cmd_str =~ s|\$SRC|$src|;
1525 $cmd_str =~ s|\$PORT|$cmd_port|;
1526 $cmd_str =~ s|\$PROTO|$cmd_proto|;
1529 ### expand any hash keys from access.conf
1530 for my $key (keys %access_keys) {
1531 next unless defined $access_hr->{$key};
1532 if ($config{'ENABLE_EXT_CMD_PREFIX'} eq 'Y') {
1533 $cmd_str =~ s|\$$config{'EXT_CMD_PREFIX'}$key|$access_hr->{$key}|;
1535 $cmd_str =~ s|\$$key|$access_hr->{$key}|;
1539 print STDERR localtime() . " External command ",
1540 "(after var expansion): $cmd_str\n" if $debug;
1545 sub is_replay_attack() {
1546 my ($decrypted_data, $src_ip) = @_;
1550 my $disk_write_digest = '';
1552 if ($use_fko_module) {
1553 ### store off the original digest type associated with this incoming
1555 $fko_incoming_digest_type = 0;
1556 $fko_incoming_digest_type = $fko_obj->digest_type() or return 1, '';
1559 if ($config{'DIGEST_TYPE'} eq 'ALL') {
1560 if ($use_fko_module) {
1561 for my $digest_type (FKO->FKO_DIGEST_SHA256,
1562 FKO->FKO_DIGEST_SHA1,
1563 FKO->FKO_DIGEST_MD5) {
1564 my $digest = &fko_compute_digest($digest_type);
1566 push @digests, $digest;
1572 push @digests, sha256_base64($decrypted_data);
1573 push @digests, sha1_base64($decrypted_data);
1574 push @digests, md5_base64($decrypted_data);
1577 if ($config{'DIGEST_TYPE'} =~ /SHA256/) {
1578 if ($use_fko_module) {
1579 my $digest = &fko_compute_digest(FKO->FKO_DIGEST_SHA256);
1581 push @digests, $digest;
1586 push @digests, sha256_base64($decrypted_data);
1589 if ($config{'DIGEST_TYPE'} =~ /SHA1/) {
1590 if ($use_fko_module) {
1591 my $digest = &fko_compute_digest(FKO->FKO_DIGEST_SHA1);
1593 push @digests, $digest;
1598 push @digests, sha1_base64($decrypted_data);
1601 if ($config{'DIGEST_TYPE'} =~ /MD5/) {
1602 if ($use_fko_module) {
1603 my $digest = &fko_compute_digest(FKO->FKO_DIGEST_MD5);
1605 push @digests, $digest;
1610 push @digests, md5_base64($decrypted_data);
1617 ### this prefers SHA256 because of the ordering above.
1618 $disk_write_digest = $digests[0];
1621 print STDERR localtime() . ' [+] Final @digests array: ', "\n",
1624 for my $digest (@digests) {
1625 ### note that the %digest_store may contain non-SHA256 digests from
1626 ### a previous instance of fwknop - this check ensures that we
1627 ### consider all previous digests
1628 if (defined $digest_store{$digest}) {
1629 ### Replay attack! Send warning email and return.
1630 if ($digest_store{$digest}) {
1631 &logr('[-]', "attempted SPA packet replay from: $src_ip " .
1632 "(original SPA src: $digest_store{$digest}, " .
1636 &logr('[-]', "attempted SPA packet replay from: $src_ip " .
1637 "($digest: $digest)", $SEND_MAIL);
1640 ### see if we need to exit if the packet limit (set with -C on the
1641 ### command line) has been reached
1642 &check_packet_limit();
1649 ### could not calculate the digest for some reason; don't
1650 ### trust the packet
1651 &logr('[-]', "could not calculate digest " .
1652 "for SPA packet from: $src_ip", $SEND_MAIL);
1655 return $rv, $disk_write_digest;
1658 sub fko_acquire_object() {
1660 &fko_destroy_object() if $fko_obj;
1662 ### initialize the FKO object
1663 $fko_obj = FKO->new()
1664 or die "[*] Could not acquire FKO object: ", FKO->error_str;
1667 print STDERR localtime() . " [+] Using libfko ",
1668 "functions via the FKO module.\n";
1673 sub fko_destroy_object() {
1674 $fko_obj->destroy();
1679 sub fko_compute_digest() {
1680 my $digest_type = shift;
1682 my $fko_err = $fko_obj->digest_type($digest_type);
1684 &logr('[-]', "FKO error setting digest type " .
1685 "$digest_type: " . $fko_obj->errstr($fko_err),
1688 $fko_err = $fko_obj->spa_digest($FKO_RECOMPUTE);
1690 &logr('[-]', "FKO error recomputing computing digest: " .
1691 $fko_obj->errstr($fko_err),
1696 my $digest = $fko_obj->spa_digest();
1698 &logr('[-]', "FKO error computing digest: " .
1699 $fko_obj->errstr($fko_err),
1704 print STDERR localtime() . " [+] FKO calculated digest ",
1705 "(type: $digest_type): $digest\n" if $debug;
1709 sub server_auth_verify_crypt_pw() {
1710 my ($username, $pw, $shadow_file) = @_;
1712 unless (-e $shadow_file) {
1713 &logr('[-]', "shadow file $shadow_file does not exist", $NO_MAIL);
1717 my $shadow_hash = '';
1718 open S, "< $shadow_file" or die "[*] Could not open $shadow_file: $!";
1721 if ($line =~ /^\s*$username:(\S+?):/) {
1727 ### mbr:$1$nrU****************************:13108:0:99999:7:::
1728 unless ($shadow_hash) {
1729 &logr('[-]', "could not get password entry for $username " .
1730 "from /etc/shadow", $NO_MAIL);
1734 return 1 if (crypt($pw, $shadow_hash) eq $shadow_hash);
1740 print STDERR localtime() . " [+] Opening $fw_data_file, and ",
1741 "entering main loop.\n" if $debug;
1743 ### track file size so we can re-open if the logfile is rotated
1744 my $fw_data_file_size = -s $fw_data_file;
1745 my $fw_data_file_inode = (stat($fw_data_file))[1];
1746 my $fw_data_file_check_ctr = 0;
1748 my $skip_first_loop = 1;
1750 open FWLOG, $fw_data_file or die "[*] Could not open $fw_data_file: $!";
1752 ### main server loop to parse iptables log messages
1757 ### allow the contents of the fwdata file to be processed only after
1758 ### the first loop has been executed.
1759 if ($skip_first_loop) {
1761 $skip_first_loop = 0;
1762 seek FWLOG,0,2; ### seek to the end of the file
1770 if ($fw_data_file_check_ctr == 10) {
1771 if (-e $fw_data_file) {
1772 my $size_tmp = -s $fw_data_file;
1773 my $inode_tmp = (stat($fw_data_file))[1];
1774 if ($inode_tmp != $fw_data_file_inode
1775 or $size_tmp < $fw_data_file_size) {
1779 &sys_log('[+]', "iptables syslog file $fw_data_file " .
1780 "shrank or was rotated, so re-opening");
1782 ### re-open the fwdata file
1783 open FWDATA, $fw_data_file or die
1784 "[*] Could not open $fw_data_file: $!";
1786 $skip_first_loop = 1;
1788 ### set file size and inode
1789 $fw_data_file_size = $size_tmp;
1790 $fw_data_file_inode = $inode_tmp;
1793 $fw_data_file_check_ctr = 0;
1796 &process_pkts(\@fw_pkts) if @fw_pkts;
1798 ### always check to see if we need to timeout knock sequences
1799 ### that exceed the KNOCK_INTERVAL
1800 &timeout_invalid_sequences();
1802 &collect_warn_die_msgs();
1804 ### clearerr() on the FWLOG filehandle to be ready for new packets
1807 sleep $config{'SLEEP_INTERVAL'};
1813 sub pcap_validate_msg() {
1814 my ($msg, $source_block_num, $access_hr) = @_;
1817 'random_number' => 0,
1820 'remote_version' => '',
1821 'numeric_version' => 0, ### calculated locally by fwknopd
1822 'action_type' => -1,
1824 'server_auth' => '', ### optional
1825 'nat_info' => '', ### optional
1826 'client_timeout' => -1, ### optional
1833 ### the last field in the SPA packet is the digest, so see if it
1834 ### checks out first (this is the internal digest, not the digest that
1835 ### guards against replay attacks).
1836 unless (&check_digest($msg, \%msg_hsh)) {
1837 print STDERR localtime() . " [-] Key mis-match or broken message ",
1838 "checksum for SOURCE $access_hr->{'src_str'} ",
1839 "(# $source_block_num in access.conf)\n"
1844 unless ($use_fko_module) {
1845 @fields = split /:/, $msg;
1848 print STDERR localtime() . " [-] Could not split decrypted ",
1849 "message into an array.\n" if $debug;
1853 if ($debug and $verbose) {
1854 print STDERR localtime() . " [+] Packet array:\n", Dumper @fields;
1857 unless ($#fields+1 >= $SPA_MIN_PACKET_FIELDS
1858 and $#fields+1 <= $SPA_MAX_PACKET_FIELDS) {
1859 print STDERR localtime() . " [-] Invalid number of fields in ",
1860 "SPA packet, expected $SPA_MIN_PACKET_FIELDS-",
1861 "$SPA_MAX_PACKET_FIELDS, got " . ($#fields+1) . ".\n" if $debug;
1868 if ($use_fko_module) {
1869 $msg_hsh{'random_number'} = $fko_obj->rand_value();
1871 $msg_hsh{'random_number'} = $fields[0];
1873 unless (&is_digit($msg_hsh{'random_number'})) {
1874 &logr('[-]', "non-digit random number in decrypted SPA " .
1875 "packet: $msg_hsh{'random_number'}", $SEND_MAIL);
1881 if ($use_fko_module) {
1882 $msg_hsh{'username'} = $fko_obj->username();
1884 $msg_hsh{'username'} = decode_base64($fields[1]);
1889 if ($use_fko_module) {
1890 $msg_hsh{'remote_time'} = $fko_obj->timestamp();
1892 $msg_hsh{'remote_time'} = $fields[2];
1894 unless (&is_digit($msg_hsh{'remote_time'})) {
1895 &logr('[-]', "non-digit timestamp in decrypted SPA packet",
1900 ### remote client version
1902 if ($use_fko_module) {
1903 $msg_hsh{'remote_version'} = $fko_obj->version();
1905 $msg_hsh{'remote_version'} = $fields[3];
1908 unless (&SPA_parse_client_version(\%msg_hsh)) {
1909 &logr('[-]', "invalid client string in decrypted SPA packet",
1916 if ($use_fko_module) {
1917 $msg_hsh{'action_type'} = $fko_obj->spa_message_type();
1919 $msg_hsh{'action_type'} = $fields[4];
1922 if (&is_digit($msg_hsh{'action_type'})) {
1923 return 0, {} unless $msg_hsh{'action_type'} == $SPA_COMMAND_MODE
1924 or $msg_hsh{'action_type'} == $SPA_ACCESS_MODE
1925 or $msg_hsh{'action_type'} == $SPA_NAT_ACCESS_MODE
1926 or $msg_hsh{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE
1927 or $msg_hsh{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE
1928 or $msg_hsh{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE
1929 or $msg_hsh{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE;
1930 $msg_hsh{'action_type'} = $msg_hsh{'action_type'};
1932 &logr('[-]', "non-digit action type in decrypted SPA packet",
1937 print STDERR localtime() .
1938 " [+] SPA action type: $msg_hsh{'action_type'}\n";
1943 if ($use_fko_module) {
1944 $msg_hsh{'action'} = $fko_obj->spa_message();
1946 $msg_hsh{'action'} = decode_base64($fields[5]);
1949 ### server_auth was introduced in 0.9.3
1951 if ($msg_hsh{'numeric_version'} >= 93) {
1953 ### iptables FORWARD/DNAT access was introduced in 1.9.0
1954 if ($msg_hsh{'numeric_version'} >= 190) {
1956 if ($msg_hsh{'action_type'} == $SPA_NAT_ACCESS_MODE
1957 or $msg_hsh{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE) {
1958 if ($use_fko_module) {
1959 $msg_hsh{'nat_info'} = $fko_obj->spa_nat_access();
1961 if ($#fields == $SPA_MIN_PACKET_FIELDS) {
1962 $msg_hsh{'nat_info'} = decode_base64($fields[6]);
1966 } elsif ($msg_hsh{'numeric_version'} >= 192) {
1967 ### client timeouts were introduced in 1.9.2
1968 if ($msg_hsh{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE) {
1969 if ($use_fko_module) {
1970 $msg_hsh{'client_timeout'} = $fko_obj->spa_client_timeout();
1972 $msg_hsh{'client_timeout'} = $fields[6];
1975 } elsif ($msg_hsh{'action_type'}
1976 == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE) {
1977 if ($use_fko_module) {
1978 $msg_hsh{'nat_info'} = $fko_obj->spa_nat_access();
1979 $msg_hsh{'client_timeout'} = $fko_obj->spa_client_timeout();
1981 $msg_hsh{'nat_info'} = decode_base64($fields[6]);
1982 $msg_hsh{'client_timeout'} = $fields[7];
1985 } elsif ($msg_hsh{'action_type'}
1986 == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) {
1987 if ($use_fko_module) {
1988 $msg_hsh{'nat_info'} = $fko_obj->spa_nat_access();
1989 $msg_hsh{'client_timeout'} = $fko_obj->spa_client_timeout();
1991 $msg_hsh{'nat_info'} = decode_base64($fields[6]);
1992 $msg_hsh{'client_timeout'} = $fields[7];
1997 unless (&is_digit($msg_hsh{'client_timeout'})) {
1998 &logr('[-]', "non-digit client timeout in decrypted " .
1999 "SPA packet", $SEND_MAIL);
2005 if (not $use_fko_module and $#fields+1 > $SPA_MIN_PACKET_FIELDS) {
2006 $msg_hsh{'server_auth'} = decode_base64($fields[6]);
2010 if ($use_fko_module) {
2011 &logr('[-]', "remote libfko version less than minimum ",
2012 "required by FKO module", $SEND_MAIL);
2015 if ($#fields+1 > $SPA_MIN_PACKET_FIELDS) {
2016 $msg_hsh{'server_auth'} = decode_base64($fields[6]);
2021 unless ($use_fko_module and $#fields+1 == $SPA_MIN_PACKET_FIELDS) {
2022 print STDERR localtime() . " [-] SPA packet from version: ",
2023 "$msg_hsh{'remote_version'} ",
2024 "does not have $SPA_MIN_PACKET_FIELDS fields"
2030 print STDERR Dumper \%msg_hsh if $debug and $verbose;
2033 print STDERR localtime() .
2034 " [+] Decoded message: $msg_hsh{'random_number'}:",
2035 "$msg_hsh{'username'}:$msg_hsh{'remote_time'}:",
2036 "$msg_hsh{'remote_version'}:$msg_hsh{'action_type'}:",
2037 "$msg_hsh{'action'}";
2039 if ($msg_hsh{'nat_info'}) {
2040 print STDERR ":$msg_hsh{'nat_info'}";
2043 if ($msg_hsh{'client_timeout'}) {
2044 print STDERR ":$msg_hsh{'client_timeout'}";
2047 ### careful not to display password information
2048 if ($msg_hsh{'server_auth'}
2049 and $msg_hsh{'server_auth'} =~ /^\s*(\w+),(.*)/) {
2051 print STDERR ":$1,";
2052 for (my $i=0; $i < length($2); $i++) {
2057 print STDERR ":$msg_hsh{'digest'}\n";
2059 return 1, \%msg_hsh;
2062 sub SPA_parse_client_version() {
2066 if ($msg_hr->{'remote_version'} =~ /^(\d+\.\d+\.\d+)-pre\d+$/) {
2067 ### remote client is a -pre release
2069 } elsif ($msg_hr->{'remote_version'} =~ /^(\d+\.\d+\.\d+)$/) {
2071 } elsif ($msg_hr->{'remote_version'} =~ /^(\d+\.\d+)-pre\d+$/) {
2072 ### remote client is a -pre release
2074 } elsif ($msg_hr->{'remote_version'} =~ /^(\d+\.\d+)$/) {
2077 print STDERR localtime() . " [-] Could not determine remote ",
2078 "client numeric version." if $debug;
2084 $msg_hr->{'numeric_version'} = $ver;
2086 print STDERR localtime() . " [+] Remote client numeric version: $ver\n"
2091 sub check_digest() {
2092 my ($msg_str, $hr) = @_;
2094 ### give priority to FKO module
2095 return &fko_check_digest($hr) if $use_fko_module;
2098 if ($msg_str =~ /(.*):(\S+)/) {
2101 if (length($sum) == $SHA256_DIGEST_LEN) {
2102 if ($config{'DIGEST_TYPE'} eq 'ALL'
2103 or $config{'DIGEST_TYPE'} =~ /SHA256/) {
2104 if ($sum eq sha256_base64($msg)) {
2105 $hr->{'digest_str'} = 'SHA256';
2106 $hr->{'digest'} = $sum;
2110 } elsif (length($sum) == $SHA1_DIGEST_LEN) {
2111 if ($config{'DIGEST_TYPE'} eq 'ALL'
2112 or $config{'DIGEST_TYPE'} =~ /SHA1/) {
2113 if ($sum eq sha1_base64($msg)) {
2114 $hr->{'digest_str'} = 'SHA1';
2115 $hr->{'digest'} = $sum;
2119 } elsif (length($sum) == $MD5_DIGEST_LEN) {
2120 if ($config{'DIGEST_TYPE'} eq 'ALL'
2121 or $config{'DIGEST_TYPE'} =~ /MD5/) {
2122 if ($sum eq md5_base64($msg)) {
2123 $hr->{'digest_str'} = 'MD5';
2124 $hr->{'digest'} = $sum;
2132 print STDERR localtime() . " [-] Digest alg mis-match.\n" if $debug;
2138 sub fko_check_digest() {
2143 my $digest_type = $fko_incoming_digest_type;
2145 print localtime() . " [+] FKO digest type: $digest_type, ",
2146 "DIGEST_TYPE var: $config{'DIGEST_TYPE'}\n" if $debug;
2148 if ($digest_type == FKO->FKO_DIGEST_SHA256) {
2149 if ($config{'DIGEST_TYPE'} eq 'ALL'
2150 or $config{'DIGEST_TYPE'} =~ /SHA256/) {
2151 $hr->{'digest_str'} = 'SHA256';
2154 } elsif ($digest_type == FKO->FKO_DIGEST_SHA1) {
2155 if ($config{'DIGEST_TYPE'} eq 'ALL'
2156 or $config{'DIGEST_TYPE'} =~ /SHA1/) {
2157 $hr->{'digest_str'} = 'SHA1';
2160 } elsif ($digest_type == FKO->FKO_DIGEST_MD5) {
2161 if ($config{'DIGEST_TYPE'} eq 'ALL'
2162 or $config{'DIGEST_TYPE'} =~ /MD5/) {
2163 $hr->{'digest_str'} = 'MD5';
2167 print STDERR localtime() . " [-] FKO invalid digest type: $digest_type\n"
2172 $hr->{'digest'} = $fko_obj->spa_digest();
2174 print STDERR localtime() . " [-] Digest alg mis-match.\n" if $debug;
2180 sub get_pcap_obj() {
2188 if ($config{'AUTH_MODE'} eq 'FILE_PCAP'
2189 or $config{'AUTH_MODE'} eq 'ULOG_PCAP') {
2191 unless (-e $config{'PCAP_PKT_FILE'}) {
2192 &pcap_file_exists_loop();
2195 unless (-s $config{'PCAP_PKT_FILE'} > 0) {
2196 ### required since we cannot use Net::Pcap::open_offline()
2197 ### to open a zero-size pcap file.
2198 &pcap_nonzero_size_loop();
2201 print STDERR localtime() . " [+] Acquiring packet data from file: ",
2202 "$config{'PCAP_PKT_FILE'}\n" if $debug;
2204 $pcap_t = Net::Pcap::open_offline($config{'PCAP_PKT_FILE'}, \$err)
2205 or die "[*] Could not open $config{'PCAP_PKT_FILE'}: $err";
2207 ### get past any packets that were from a previous fwknopd
2209 Net::Pcap::loop($pcap_t, -1, \&null_func, 'fwknop_tag');
2212 if ($config{'ENABLE_PCAP_PROMISC'} eq 'Y') {
2213 print STDERR localtime() . " [+] Sniffing (promisc) packet data ",
2214 "from interface: $config{'PCAP_INTF'}\n" if $debug;
2215 $pcap_t = Net::Pcap::open_live($config{'PCAP_INTF'},
2216 $config{'MAX_SNIFF_BYTES'}, 1, 100, \$err)
2217 or die "[*] Could not open $config{'PCAP_INTF'}: $err";
2219 print STDERR localtime() . " [+] Sniffing (non-promisc) packet ",
2220 "data from interface: $config{'PCAP_INTF'}\n" if $debug;
2221 $pcap_t = Net::Pcap::open_live($config{'PCAP_INTF'},
2222 $config{'MAX_SNIFF_BYTES'}, 0, 100, \$err)
2223 or die "[*] Could not open $config{'PCAP_INTF'}: $err";
2227 ### apply pcap filter if necessary
2228 if ($config{'PCAP_FILTER'} ne 'NONE') {
2229 if ($config{'AUTH_MODE'} eq 'PCAP') {
2230 if (Net::Pcap::lookupnet($config{'PCAP_INTF'}, \$address,
2231 \$netmask, \$err) != 0) {
2232 if ($config{'ENABLE_PCAP_PROMISC'} eq 'N') {
2233 &logr('[-]', "warning: ENABLE_PCAP_PROMISC is disabled and " .
2234 "could not get net information for " .
2235 "$config{'PCAP_INTF'}: $err, continuing anyway",
2240 ### set the filter on the traffic
2241 Net::Pcap::compile($pcap_t, \$filter, $config{'PCAP_FILTER'},
2243 && die '[*] Unable to compile packet capture filter';
2244 Net::Pcap::setfilter($pcap_t, $filter)
2245 && die '[*] Unable to set packet capture filter';
2251 sub pcap_file_exists_loop() {
2252 while (not -e $config{'PCAP_PKT_FILE'}) {
2253 &logr('[-]', "pcap file $config{'PCAP_PKT_FILE'} does not " .
2254 "exist, waiting $err_wait_timer seconds for sniffer to " .
2255 "create file", $NO_MAIL);
2256 sleep $err_wait_timer;
2261 sub pcap_nonzero_size_loop() {
2262 while (-s $config{'PCAP_PKT_FILE'} == 0) {
2263 &logr('[-]', "zero size pcap file $config{'PCAP_PKT_FILE'}, " .
2264 "waiting $err_wait_timer seconds for packet data", $NO_MAIL);
2265 sleep $err_wait_timer;
2270 sub exec_command() {
2271 my ($cmd, $cmd_alarm) = @_;
2273 if ($pid = fork()) {
2274 local $SIG{'ALRM'} = sub {die "[*] External script timeout.\n"};
2275 ### the external script should be finished within this timeout
2282 kill 9, $pid unless kill 15, $pid;
2285 die "[*] Could not fork for external script: $!" unless defined $pid;
2286 ### if we are already redirecting output within the command itself
2287 ### then don't redirect again
2288 if ($cmd =~ /\s*>\s*/) {
2291 exec qq{$cmd > /dev/null 2>&1};
2297 ### knock server processsing
2298 sub process_pkts() {
2299 my $fw_pkts_aref = shift;
2300 PKT: for my $pkt (@$fw_pkts_aref) {
2316 my $tcp_options = '';
2317 next unless $pkt =~ /kernel.*IN=.*OUT=/;
2318 ### May 18 22:21:26 orthanc kernel: DROP IN=eth2 OUT=
2319 ### MAC=00:60:1d:23:d0:01:00:60:1d:23:d3:0e:08:00 SRC=192.168.20.25
2320 ### DST=192.168.20.1 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=47300 DF
2321 ### PROTO=TCP SPT=34111 DPT=6345 WINDOW=5840 RES=0x00 SYN URGP=0
2322 if ($pkt =~ /SRC=(\S+)\s+DST=(\S+)\s+LEN=(\d+)\s+TOS=(\S+)
2323 \s*.*\s+TTL=(\d+)\s+ID=(\d+)\s*.*\s+PROTO=TCP\s+
2324 SPT=(\d+)\s+DPT=(\d+)\s+WINDOW=(\d+)\s+
2325 RES=\S+\s*(.*)\s+URGP=/x) {
2326 ($src, $dst, $len, $tos, $ttl, $id, $sp, $dp, $win, $flags) =
2327 ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10);
2328 if ($pkt =~ /\sRES=\S+\s*(.*)\s+URGP=/) {
2332 unless ($flags !~ /WIN/ &&
2340 print STDERR localtime() . " [*] err packet: bad tcp flags.\n"
2344 $frag_bit = 1 if $pkt =~ /\sDF\s+PROTO/;
2345 ### don't pickup IP options if --log-ip-options is used
2346 ### (they appear before the PROTO= field).
2347 if ($pkt =~ /URGP=\S+\s+OPT\s+\((\S+)\)/) {
2352 ### Jul 15 23:32:53 orthanc kernel: DROP IN=eth1 OUT=
2353 ### MAC=00:0c:41:24:68:ef:00:0c:41:24:56:37:08:00 SRC=192.168.10.3
2354 ### DST=192.168.10.1 LEN=29 TOS=0x00 PREC=0x00 TTL=64 ID=48500 DF
2355 ### PROTO=UDP SPT=32768 DPT=65533 LEN=9
2356 } elsif ($pkt =~ /SRC=(\S+)\s+DST=(\S+)\s+LEN=(\d+)\s+TOS=(\S+)\s+
2357 .*?\sTTL=(\d+)\s+ID=(\d+)\s*.*\sPROTO=UDP\s+
2358 SPT=(\d+)\s+DPT=(\d+)/x) {
2359 ($src, $dst, $len, $tos, $ttl, $id, $sp, $dp) =
2360 ($1,$2,$3,$4,$5,$6,$7,$8);
2362 ### make sure we have a "reasonable" packet (note that nmap
2363 ### can scan port 0 and iptables can report this fact)
2364 unless ($src and $dst and $len >= 0 and $tos and $ttl >= 0
2365 and $id >= 0 and $sp >= 0 and $dp >= 0) {
2369 } elsif ($pkt =~ /SRC=(\S+)\s+DST=(\S+)\s+LEN=(\d+).*
2370 TTL=(\d+).*PROTO=ICMP\s+TYPE=(\d+)\s+
2371 CODE=(\d+)\s+ID=(\d+)\s+SEQ=(\d+)/x) {
2372 ($src, $dst, $len, $ttl, $type, $code, $id, $seq) =
2373 ($1,$2,$3,$4,$5,$6,$7,$8);
2375 unless ($src and $dst and $len >= 0 and $ttl >= 0 and $proto
2376 and $type >= 0 and $code >= 0 and $id >= 0
2383 print STDERR localtime() . " [-] no regex match for pkt: $pkt\n"
2387 ### check to see if there are any access directives for $src, and
2388 ### if not we will do _nothing_ with this IP (unless we are just
2389 ### trying to fingerprint it).
2390 my $access_nums_aref = &check_src($src) unless $os_fprint_only;
2392 unless ($os_fprint_only) {
2393 unless ($access_nums_aref) {
2394 print STDERR localtime() . " [-] Packet from $src did not ",
2395 "match any SOURCE in $config{'ACCESS_CONF'}\n" if $debug;
2400 if ($proto eq 'tcp') {
2401 print STDERR localtime() . " [+] $proto $src $sp -> $dst $dp, ",
2402 "$flags\n" if $debug;
2403 } elsif ($proto eq 'udp') {
2404 print STDERR localtime() . " [+] $proto $src $sp -> $dst ",
2406 } elsif ($proto eq 'icmp') {
2407 print STDERR localtime() . " [+] $proto $src -> $dst\n" if $debug;
2410 ### try to fingerprint the remote OS even though the knock
2411 ### sequence is not validated yet.
2412 if ($proto eq 'tcp' and $flags =~ /SYN/) { ### must have a SYN pkt
2413 if ($tcp_options) { ### hopefully --log-tcp-options is being used
2415 ### p0f based fingerprinting
2416 &p0f($src, $len, $frag_bit, $ttl, $win, $tcp_options);
2420 next PKT if $os_fprint_only;
2422 my $expecting_decrypt = 0;
2425 NUM: for my $num (@$access_nums_aref) {
2426 my $access_hr = $access[$num];
2428 $ip_sequences{$src}{$num} = {}
2429 unless defined $ip_sequences{$src}{$num};
2431 my $seq_hr = $ip_sequences{$src}{$num};
2433 ### keep track of which source block we are dealing with from
2435 my $source_block_num = $access_hr->{'block_num'};
2437 $seq_hr->{'grant_ctr'} = 0
2438 if not defined $seq_hr->{'grant_ctr'};
2440 ### see if the destination port is part of the correct knock sequence
2442 my $matched_sequence = 0;
2444 if ($access_hr->{'DATA_COLLECT_MODE'} == $ENCRYPT_SEQUENCE) {
2445 if ($dp >= $access_hr->{'PORT_OFFSET'} and
2446 $dp < $access_hr->{'PORT_OFFSET'} + 256) {
2448 ### keep timestamp for when we started tracking the
2449 ### encrypted sequence
2450 $seq_hr->{'enc_stime'} = time()
2451 unless defined $seq_hr->{'enc_stime'};
2453 ### add the destination port to the encrypted sequence
2454 push @{$seq_hr->{'enc_ports'}}, $dp;
2456 print STDERR localtime() . " [+] Added $dp to encrypted ",
2457 "sequence for $src ",
2458 "(packet: $#{$seq_hr->{'enc_ports'}})\n"
2462 ### see if the encrypted sequence checks out
2463 if ($#{$seq_hr->{'enc_ports'}}
2464 == $enc_blocksize - 1) {
2466 $expecting_decrypt = 1;
2468 ### attempt to decrypt the sequence
2469 my ($rv, $allow_src, $dec_allow_port,
2470 $dec_allow_proto, $username) =
2471 &decrypt_sequence($src, $seq_hr,
2477 &logr('[+]', "successful knock decrypt for $src " .
2478 "(SOURCE block: $source_block_num)", $SEND_MAIL);
2480 ### see if we need to match the OS
2481 unless (&matched_os($src, $access_hr)) {
2482 delete $ip_sequences{$src}{$num};
2486 ### see if we need to match the username
2487 unless (&matched_username($username,
2489 delete $ip_sequences{$src}{$num};
2493 ### check to see if we have already exceeded the
2494 ### maximum number of allowed sequences (this helps
2495 ### to prevent replay attacks).
2496 if (defined $access_hr->{'KNOCK_LIMIT'}) {
2497 if ($seq_hr->{'grant_ctr'}
2498 > $access_hr->{'KNOCK_LIMIT'}) {
2499 &logr('[-]', "$src exceeded knock limit (set to " .
2500 "$access_hr->{'KNOCK_LIMIT'} accesses)",
2502 &logr('[-]', "access controls for $src will " .
2503 "not be modified", $SEND_MAIL);
2504 delete $ip_sequences{$src}{$num};
2509 ### all criteria met for encrypted sequence;
2511 my %open_ports = %{$access_hr->{'OPEN_PORTS'}};
2512 $open_ports{$dec_allow_proto}{$dec_allow_port} = '';
2514 &grant_access($allow_src, {}, {}, $seq_hr,
2515 \%open_ports, $access_hr);
2518 delete $ip_sequences{$src}{$num};
2521 } elsif (defined $access_hr->{'SHARED_SEQUENCE'}) {
2522 $seq_hr->{'port_seq'} = 0
2523 unless defined $seq_hr->{'port_seq'};
2524 if ($dp == $access_hr->{'SHARED_SEQUENCE'}->
2525 [$seq_hr->{'port_seq'}]->{'port'}
2526 and $proto eq $access_hr->{'SHARED_SEQUENCE'}->
2527 [$seq_hr->{'port_seq'}]->{'proto'}) {
2529 push @{$seq_hr->{'port_times'}}, time();
2531 ### increment sequence counter (takes into account timing
2533 next NUM unless &incr_seq($src, $seq_hr, $access_hr);
2535 ### if we made it to the end of the sequence then we have
2536 ### a correct knock sequence
2537 if ($seq_hr->{'port_seq'}
2538 == $#{$access_hr->{'SHARED_SEQUENCE'}}+1) {
2539 print STDERR localtime() . " [+] Matched knock ",
2540 "sequence for $src\n" if $debug;
2541 $matched_sequence = 1;
2544 print STDERR localtime() . " [-] Could not match dst ",
2545 "port: $dp at sequence ",
2546 "number: $seq_hr->{'port_seq'}\n"
2548 delete $ip_sequences{$src}{$num};
2553 ### we matched the knock sequence, so reset for new
2554 ### sequence (note we may have other criteria to meet
2555 ### before actually granting access).
2556 if ($matched_sequence) {
2557 delete $seq_hr->{'port_times'};
2558 $seq_hr->{'port_seq'} = 0;
2560 &logr('[+]', "port knock access sequence matched for $src " .
2561 "(SOURCE block: $source_block_num)", $SEND_MAIL);
2563 next NUM unless &matched_os($src, $seq_hr);
2565 ### check to see if we have already exceeded the maximum number
2566 ### of allowed sequences (this helps to prevent replay attacks).
2567 if (defined $access_hr->{'KNOCK_LIMIT'}) {
2568 if ($seq_hr->{'grant_ctr'}
2569 > $access_hr->{'KNOCK_LIMIT'}) {
2570 &logr('[-]', "$src exceeded knock limit (set to " .
2571 "$access_hr->{'KNOCK_LIMIT'} accesses)",
2573 &logr('[-]', "access controls for $src will not be " .
2574 "modified", $SEND_MAIL);
2579 ### if we made it here then the shared sequence checked out and
2580 ### we need to grant access by modifying the iptables ruleset
2581 ### (if the ruleset does not already allow $src of course).
2582 &grant_access($src, {}, {}, $seq_hr,
2583 $access_hr->{'OPEN_PORTS'}, $access_hr);
2586 if ($expecting_decrypt and not $decrypted) {
2587 &logr('[-]', "sequence decrypt failed for $src", $SEND_MAIL);
2591 ### see if we need to exit if the packet limit (set with -C on the
2592 ### command line) has been reached
2593 &check_packet_limit();
2595 if ($os_fprint_only) {
2602 my ($src, $href) = @_;
2604 ### see if we require any OS match at all
2605 return 1 unless (defined $href->{'REQUIRE_OS'} or
2606 defined $href->{'REQUIRE_OS_REGEX'});
2608 unless (defined $p0f{$src}) {
2609 ### could not guess the OS
2610 if (defined $href->{'REQUIRE_OS'}) {
2611 &logr('[-]', "could not fingerprint OS for $src, expecting OS: " .
2612 $href->{'REQUIRE_OS'}, $SEND_MAIL);
2613 } elsif (defined $href->{'REQUIRE_OS_REGEX'}) {
2614 &logr('[-]', "could not fingerprint OS for $src, expecting OS " .
2615 "regex: $href->{'REQUIRE_OS_REGEX'}", $SEND_MAIL);
2620 if (defined $href->{'REQUIRE_OS'}) {
2621 if (defined $p0f{$src}) {
2622 my $first_os_key = '';
2623 for my $os (keys %{$p0f{$src}}) {
2624 $first_os_key = $os unless $first_os_key;
2625 if ($os eq $href->{'REQUIRE_OS'}) {
2626 &logr('[+]', "OS guess: $os " .
2627 "matched for $src", $SEND_MAIL);
2631 ### there may be more than one OS fingerprint, but
2632 ### just print one (if we make it here there was no
2634 &logr('[-]', "OS fingerprint mismatch for $src: " .
2635 "expected: $href->{'REQUIRE_OS'}, " .
2636 "received: $first_os_key", $SEND_MAIL);
2640 } elsif (defined $href->{'REQUIRE_OS_REGEX'}) {
2641 if (defined $p0f{$src}) {
2642 my $first_os_key = '';
2643 for my $os (keys %{$p0f{$src}}) {
2644 $first_os_key = $os unless $first_os_key;
2645 if ($os =~ m|$href->{'REQUIRE_OS_REGEX'}|i) {
2646 &logr('[+]', "OS guess: $os " .
2647 "regex matched for $src", $SEND_MAIL);
2652 ### there may be more than one OS fingerprint, but
2654 &logr('[-]', "OS fingerprint regex mismatch for $src: " .
2655 "expected: $href->{'REQUIRE_OS_REGEX'}, " .
2656 "received: $first_os_key", $SEND_MAIL);
2663 sub matched_username() {
2664 my ($username, $href) = @_;
2666 return 1 unless defined $href->{'REQUIRE_USERNAME'};
2669 if ($username eq $href->{'REQUIRE_USERNAME'}) {
2670 &logr('[+]', "username $username match", $NO_MAIL);
2673 &logr('[-]', "username mismatch, expected: " .
2674 "$href->{'REQUIRE_USERNAME'}, got: $username", $SEND_MAIL);
2678 &logr('[-]', "missing username in encrypted " .
2679 "sequence, expected: $href->{'REQUIRE_USERNAME'}", $SEND_MAIL);
2688 my @access_nums = ();
2690 if (&is_ip_included($src, $blacklist_ar, $blacklist_exclude_ar)) {
2691 print STDERR localtime() . " [+] check_src() ",
2692 "$src in BLACKLIST" if $debug;
2693 return \@access_nums;
2696 ### now process the SOURCE stanzas
2697 for (my $i=0; $i<=$#access; $i++) {
2698 my $access_hr = $access[$i];
2699 my $matched_src = 0;
2700 if (&is_ip_included($src, $access_hr->{'SOURCE'},
2701 $access_hr->{'exclude_nets'})) {
2702 print STDERR localtime() . " [+] Packet from $src matched ",
2703 "$access_hr->{'src_str'} (line: ",
2704 "$access_hr->{'src_line_num'})\n"
2706 push @access_nums, $i;
2709 return \@access_nums;
2715 ### check to make sure the packet data only contains base64 encoded
2716 ### characters per RFC 3548: 0-9, A-Z, a-z, +, /, =
2717 if ($data =~ /[^\x30-\x39\x41-\x5a\x61-\x7a\x2b\x2f\x3d]/) {
2720 if ($data =~ /=[^=]/) {
2726 sub is_url_base64() {
2729 ### check to make sure the packet data only contains base64 encoded
2730 ### characters per RFC 3548, except that "-" replaces "+", and "_"
2732 if ($data =~ /[^\x30-\x39\x41-\x5a\x61-\x7a\x2d\x5f\x3d]/) {
2735 if ($data =~ /=[^=]/) {
2741 sub is_ip_included() {
2742 my ($ip, $include_ar, $exclude_ar) = @_;
2744 my $is_included = 0;
2746 ### check the include criteria
2747 for my $net (@$include_ar) {
2748 if (ipv4_in_network($net, $ip)) {
2749 print STDERR localtime() . " [+] $ip included by $net\n"
2757 ### check the exclude criteria
2758 for my $net (@$exclude_ar) {
2759 if (ipv4_in_network($net, $ip)) {
2760 print STDERR localtime() . " [-] $ip excluded by ! $net\n"
2767 return $is_included;
2771 my ($src, $seq_hr, $access_hr) = @_;
2772 if (defined $access_hr->{'MIN_TIME_DIFF'}) {
2773 ### can check relative timings only after we have more than
2774 ### one matching sequence packet
2775 if ($seq_hr->{'port_seq'} > 0) {
2776 if (defined $access_hr->{'MAX_TIME_DIFF'}) {
2778 if (($time - $seq_hr->{'port_times'}[$seq_hr->{'port_seq'}-1])
2779 > $access_hr->{'MIN_TIME_DIFF'} and
2780 ($time - $seq_hr->{'port_times'}[$seq_hr->{'port_seq'}-1])
2781 < $access_hr->{'MAX_TIME_DIFF'}) {
2782 print STDERR localtime() . " [+] Sequence min/max time match: ",
2783 "($seq_hr->{'port_seq'}) ",
2784 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/",
2785 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n"
2788 &logr('[-]', 'Sequence min/max_time exceeded: ' .
2789 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/" .
2790 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'} " .
2791 "(port sequence num: $seq_hr->{'port_seq'}) ", $SEND_MAIL);
2792 $seq_hr->{'port_seq'} = 0;
2793 delete $seq_hr->{'port_times'};
2798 - $seq_hr->{'port_times'}[$seq_hr->{'port_seq'}-1])
2799 > $access_hr->{'MIN_TIME_DIFF'}) {
2800 print STDERR localtime() . " [+] Sequence min_time match: ",
2801 "($seq_hr->{'port_seq'}) ",
2802 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/",
2803 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n"
2806 &logr('[-]', "Sequence min_time (" .
2807 "$access_hr->{'MIN_TIME_DIFF'} seconds) not met: " .
2808 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/" .
2809 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'} " .
2810 "(port sequence num: $seq_hr->{'port_seq'}) ", $SEND_MAIL);
2811 delete $seq_hr->{'port_times'};
2812 $seq_hr->{'port_seq'} = 0;
2817 print STDERR localtime() . " [+] 1 Sequence match: ",
2818 "($seq_hr->{'port_seq'}) ",
2819 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/",
2820 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n"
2823 } elsif (defined $access_hr->{'MAX_TIME_DIFF'}) {
2824 if ($seq_hr->{'port_seq'} > 0) {
2826 - $seq_hr->{'port_times'}[$seq_hr->{'port_seq'}-1])
2827 < $access_hr->{'MAX_TIME_DIFF'}) {
2828 print STDERR localtime() . " [+] Sequence max_time match: ",
2829 "($seq_hr->{'port_seq'}) ",
2830 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/",
2831 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n"
2834 &logr('[-]', "Sequence max_time ($access_hr->{'MAX_TIME_DIFF'} seconds) exceeded: " .
2835 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/" .
2836 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}" .
2837 "(port sequence num: $seq_hr->{'port_seq'}) ", $SEND_MAIL);
2838 delete $seq_hr->{'port_times'};
2839 $seq_hr->{'port_seq'} = 0;
2843 print STDERR localtime() . " [+] Sequence match: ($seq_hr->{'port_seq'}) ",
2844 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/",
2845 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n"
2849 print STDERR localtime() . " [+] Sequence match: ($seq_hr->{'port_seq'}) ",
2850 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/",
2851 "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n"
2855 ### if we made it here, then we met the timing requirements (if required)
2856 $seq_hr->{'port_seq'}++;
2860 sub pcap_GPG_decrypt_msg() {
2861 my ($msg, $access_hr) = @_;
2866 my $decrypted_msg = '';
2867 my $base64_decoded_msg = '';
2869 my $gpg_sign_id = '';
2871 unless ($msg =~ /^$access_hr->{'GPG_PREFIX'}/) {
2872 if ($access_hr->{'GPG_NO_REQUIRE_PREFIX'}) {
2873 print STDERR localtime() . qq| [-] Incoming base64-encoded |,
2874 qq|SPA packet is not prefixed with: |,
2875 qq|"$access_hr->{'GPG_PREFIX'}"\n| if $debug;
2877 print STDERR localtime() . qq| [+] Adding |,
2878 qq|"$access_hr->{'GPG_PREFIX'}" prefix to |,
2879 "base64-encoded message.\n" if $debug;
2880 $msg = $access_hr->{'GPG_PREFIX'} . $msg;
2884 my ($equals_rv, $equals_padding) = &base64_equals_padding($msg);
2886 unless ($equals_rv) {
2887 return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
2890 if ($equals_rv and $equals_padding) {
2891 print STDERR localtime() . " [+] Padding base64-encoded message ",
2892 "with '$equals_padding'.\n" if $debug;
2893 $msg .= $equals_padding;
2896 if ($config{'ENABLE_SPA_OVER_HTTP'} eq 'Y' and &is_url_base64($msg)) {
2901 if ($use_fko_module) {
2903 my $fko_err = $fko_obj->spa_data($msg);
2906 &logr('[-]', "FKO error setting spa_data(): " .
2907 $fko_obj->errstr($fko_err), $NO_MAIL);
2909 return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
2912 ### set the decryption type to use gpg
2913 $fko_err = $fko_obj->encryption_type(FKO->FKO_ENCRYPTION_GPG);
2916 &logr('[-]', "FKO error setting decryption type to gpg: " .
2917 $fko_obj->errstr($fko_err), $NO_MAIL);
2918 return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
2922 $fko_err = $fko_obj->gpg_home_dir($access_hr->{'GPG_HOME_DIR'});
2925 &logr('[-]', "FKO error setting gpg home dir: " .
2926 $fko_obj->errstr($fko_err), $NO_MAIL);
2927 return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
2931 $fko_err = $fko_obj->gpg_recipient($access_hr->{'GPG_DECRYPT_ID'});
2934 &logr('[-]', "FKO error setting signing key " .
2935 "gpg_signer(): " . $fko_obj->errstr($fko_err),
2938 return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
2941 $fko_err = $fko_obj->decrypt_spa_data($access_hr->{'GPG_DECRYPT_PW'});
2944 &logr('[-]', "FKO error decrypting data via " .
2945 "GnuPG decrypt_spa_data(): " .
2946 $fko_obj->errstr($fko_err),
2949 return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
2952 return 1, $decrypted_msg, $gpg_sign_id;
2956 print STDERR localtime() . " [+] decode_base64() against the ",
2957 "following data: $msg\n";
2960 ### base64 decode the packet
2961 $base64_decoded_msg = decode_base64($msg);
2963 ### continue only if decode_base64() had no "Premature end of base64 data"
2964 ### errors - we want to minimize code that executes against suspicious
2966 if ($warn_msg =~ /Premature\s+end/i
2967 or $warn_msg =~ /Premature\s+padding/i) {
2969 print STDERR localtime() . " [-] $warn_msg";
2971 return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
2974 print STDERR localtime() . " [+] Attempting GnuPG decrypt...\n" if $debug;
2975 if ($debug and $verbose) {
2976 print STDERR localtime() . " Decrypting raw data (hex dump):\n";
2977 &hex_dump($base64_decoded_msg);
2980 my $gnupg = GnuPG::Interface->new();
2982 my %gnupg_options = (
2984 'homedir' => $access_hr->{'GPG_HOME_DIR'},
2988 delete $gnupg_options{'batch'} if ($debug and $verbose and not $test_mode);
2989 delete $gnupg_options{'batch'} if $access_hr->{'GPG_USE_OPTIONS'};
2991 $gnupg->options->hash_init(%gnupg_options);
2993 if ($access_hr->{'GPG_PATH'}) {
2994 $gnupg->call($access_hr->{'GPG_PATH'});
2995 } elsif (defined $cmds{'gpg'}) {
2996 $gnupg->call($cmds{'gpg'});
2999 my $input_fh = IO::Handle->new() or die $!;
3000 my $output_fh = IO::Handle->new() or die $!;
3001 my $error_fh = IO::Handle->new() or die $!;
3002 my $pw_fh = IO::Handle->new() or die $!;
3003 my $status_fh = IO::Handle->new() or die $!;
3005 my $handles = GnuPG::Handles->new(
3007 stdout => $output_fh,
3008 stderr => $error_fh,
3009 passphrase => $pw_fh,
3010 status => $status_fh,
3013 $gnupg->options->default_key($access_hr->{'GPG_DECRYPT_ID'});
3015 if (defined $access_hr->{'GPG_AGENT_INFO'}) {
3017 $ENV{'GPG_AGENT_INFO'} = $access_hr->{'GPG_AGENT_INFO'};
3019 $pid = $gnupg->decrypt('handles' => $handles,
3020 'command_args' => [ qw( --use-agent ) ]);
3022 } elsif ($gpg_agent_info) {
3024 ### global definition for gpg-agent connection information
3025 ### from the command line
3026 $ENV{'GPG_AGENT_INFO'} = $gpg_agent_info;
3028 $pid = $gnupg->decrypt('handles' => $handles,
3029 'command_args' => [ qw( --use-agent ) ]);
3033 $pid = $gnupg->decrypt('handles' => $handles);
3036 print $pw_fh $access_hr->{'GPG_DECRYPT_PW'};
3040 print $input_fh $base64_decoded_msg;
3043 @plaintext = <$output_fh>;
3046 my @errors = <$error_fh>;
3049 my @status = <$status_fh>;
3055 print STDERR localtime() . " [+] GnuPG status messages:\n";
3056 print STDERR for @status;
3059 ### we require the message to be signed; make sure
3060 ### the signature is good
3061 KEY: for my $key_id (@{$access_hr->{'GPG_REMOTE_ID'}}) {
3062 $key_id = $1 if $key_id =~ /^0x(\w+)/;
3063 my $found_candidate_sig = 0;
3065 print STDERR localtime() . " [+] gpg key ID: $key_id\n",
3066 localtime() . " GnuPG error messages:\n";
3068 LINE: for my $err (@errors) {
3069 print STDERR localtime() . " $err" if $debug;
3070 if ($key_id eq 'ANY') {
3071 if ($err =~ /Good\s+signature/i) {
3073 $gpg_sign_id = $key_id;
3077 if ($err =~ /Signature\s+made.*ID\s+$key_id$/) {
3078 $found_candidate_sig = 1;
3081 if ($found_candidate_sig and $err =~ /Good\s+signature/i) {
3083 $gpg_sign_id = $key_id;
3090 if ($found_sig and @plaintext) {
3092 $decrypted_msg .= $_ for @plaintext;
3094 print STDERR localtime() . " [-] GnuPG message not signed by any ",
3095 "required key ID.\n" if $debug;
3098 return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
3101 sub pcap_Rijndael_decrypt_msg() {
3102 my ($msg, $enc_key) = @_;
3104 my $decrypted_msg = '';
3106 my $base64_decoded_msg = '';
3108 unless ($msg =~ /^U2FsdGVkX1/) {
3110 print STDERR localtime() . " [+] Adding encoded 'Salted__' ",
3111 "prefix (U2FsdGVkX1) to incoming encoded SPA packet.\n";
3113 $msg = 'U2FsdGVkX1' . $msg;
3116 my ($equals_rv, $equals_padding) = &base64_equals_padding($msg);
3118 unless ($equals_rv) {
3119 return $decrypt_rv, $decrypted_msg;
3122 if ($use_fko_module) {
3124 if ($config{'ENABLE_SPA_OVER_HTTP'} eq 'Y' and &is_url_base64($msg)) {
3129 my $fko_err = $fko_obj->spa_data($msg);
3132 &logr('[-]', "FKO error setting spa_data(): " .
3133 $fko_obj->errstr($fko_err), $NO_MAIL);
3135 return $decrypt_rv, $decrypted_msg;
3138 $fko_err = $fko_obj->decrypt_spa_data($enc_key);
3141 &logr('[-]', "FKO error decrypting data via " .
3142 "Rijndael decrypt_spa_data(): " .
3143 $fko_obj->errstr($fko_err),
3146 return $decrypt_rv, $decrypted_msg;
3149 return 1, $decrypted_msg;
3153 if ($equals_padding) {
3154 print STDERR localtime() . " [+] Padding base64-encoded message ",
3155 "with '$equals_padding'.\n" if $debug;
3156 $msg .= $equals_padding;
3159 if ($config{'ENABLE_SPA_OVER_HTTP'} eq 'Y' and &is_url_base64($msg)) {
3165 print STDERR localtime() . " [+] decode_base64() against the ",
3166 "following data: $msg\n";
3169 ### base64 decode the packet
3170 $base64_decoded_msg = decode_base64($msg);
3172 ### continue only if decode_base64() had no "Premature end of base64 data"
3173 ### errors - we want to minimize code that executes against suspicious
3175 if ($warn_msg =~ /Premature\s+end/i
3176 or $warn_msg =~ /Premature\s+padding/i) {
3178 print STDERR localtime() . " [-] $warn_msg";
3180 return $decrypt_rv, $decrypted_msg;
3183 ### look for the Salted__ prefix
3184 unless ($base64_decoded_msg =~ /^Salted__/) {
3186 print STDERR localtime() . " [-] base64-decoded data does ",
3187 "not begin with 'Salted__'\n";
3189 return $decrypt_rv, $decrypted_msg;
3192 print STDERR localtime() . " [+] Attempting Rijndael decrypt...\n"
3195 if ($debug and $verbose) {
3196 print STDERR localtime() . " Decrypting raw data (hex dump):\n";
3197 &hex_dump($base64_decoded_msg);
3200 my $cipher = Crypt::CBC->new({
3202 'cipher' => $enc_alg,
3205 $decrypted_msg = $cipher->decrypt($base64_decoded_msg);
3207 if ($debug and $verbose) {
3208 print STDERR " Salt:\n";
3209 &hex_dump($cipher->salt());
3210 print STDERR " Key:\n";
3211 &hex_dump($cipher->key());
3212 print STDERR " IV:\n";
3213 &hex_dump($cipher->iv());
3214 print STDERR " PassPhrase:\n";
3215 &hex_dump($cipher->passphrase());
3216 print STDERR " Block Size: " . $cipher->blocksize() ."\n",
3217 " Key Size: " . $cipher->keysize(). "\n\n";
3221 $decrypted_msg = '';
3225 return $decrypt_rv, $decrypted_msg;
3228 sub decrypt_sequence() {
3229 my ($src, $seq_hr, $access_hr) = @_;
3231 my $cipher_txt = '';
3234 $cipher_txt .= chr($_ - $access_hr->{'PORT_OFFSET'})
3235 for @{$seq_hr->{'enc_ports'}};
3237 return 0 unless $cipher_txt;
3240 my @tmp_chars = split //, $cipher_txt;
3241 print STDERR localtime() . ' [+] Cipher text (' .
3242 length($cipher_txt) . ' bytes): ';
3243 print STDERR ord($_) . ' ' for @tmp_chars;
3247 my $cipher = Crypt::CBC->new({
3248 'key' => $access_hr->{'KEY'},
3249 'cipher' => $enc_alg,
3252 ### we now have our encrypted string, so try to decrypt it
3255 $plain_txt = $cipher->decrypt($cipher_txt);
3259 return 0,0,0,0 if ($@ or not $plain_txt);
3262 my @tmp_chars = split //, $plain_txt;
3263 print STDERR localtime() . " [+] Plain text: ";
3264 print STDERR ord($_) . ' ' for @tmp_chars;
3268 my @chars = split //, $plain_txt;
3270 ### the first four characters in the @chars array represent the
3271 ### four octets of the IP we are going to modify access for
3272 for my $octet ($chars[0], $chars[1], $chars[2], $chars[3]) {
3273 unless (0 <= ord($octet) and ord($octet) < 256) {
3274 &logr('[-]', "invalid IP octet: " . ord($octet), $SEND_MAIL);
3277 $allow_src .= ord($octet) . '.';
3279 $allow_src =~ s/\.$//;
3281 if ($allow_src eq '0.0.0.0') {
3282 ### the client sent 0.0.0.0 across, so it may be behind a
3283 ### NAT device (or the person just doesn't know their source
3284 ### address) so open the firewall for the source of the
3285 ### encrypted sequence.
3286 if ($config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y' or not
3287 &is_ip_included($allow_src,
3288 $access_hr->{'REQUIRE_SOURCE_ADDRESS'},
3289 $access_hr->{'require_src_addr_exceptions'})) {
3290 ### we require the source address to be contained within
3291 ### the encrypted packet.
3297 my $port_upper_bits = ord($chars[4]) << 8;
3298 my $port_lower_bits = ord($chars[5]);
3299 my $allow_port = $port_upper_bits | $port_lower_bits;
3301 unless (0 <= $allow_port and $allow_port < 65536) {
3302 &logr('[-]', "bad port number: $allow_port", $SEND_MAIL);
3306 my $allow_proto = '';
3307 my $proto = ord($chars[6]);
3309 $allow_proto = 'tcp';
3310 } elsif ($proto == 17) {
3311 $allow_proto = 'udp';
3312 } elsif ($proto == 1) {
3313 $allow_proto = 'icmp';
3315 &logr('[-]', "bad protocol number: $proto", $SEND_MAIL);
3319 my $checksum_data = ord($chars[7]);
3322 for (my $i=0; $i < 7; $i++) {
3323 $checksum += ord($chars[$i]);
3325 $checksum = $checksum % 256;
3327 unless ($checksum_data == $checksum) {
3328 &logr('[-]', "invalid checksum for $src", $SEND_MAIL);
3334 while ($i <= $#chars and ord($chars[$i]) != 0) {
3335 $username .= $chars[$i];
3339 return 1, $allow_src, $allow_port, $allow_proto, $username;
3342 sub grant_access() {
3343 my ($src, $msg_hr, $nat_info_hr, $seq_hr,
3344 $open_ports_hr, $access_hr) = @_;
3346 if ($access_hr->{'EXTERNAL_CMD_OPEN'}
3347 or ($config{'FIREWALL_TYPE'} eq 'external_cmd'
3348 and $config{'EXTERNAL_CMD_OPEN'})) {
3350 ### run EXTERNAL_CMD_OPEN and let knoptm run EXTERNAL_CMD_CLOSE
3351 &external_cmd_open($src, $msg_hr, $open_ports_hr, $access_hr);
3354 if ($config{'FIREWALL_TYPE'} eq 'iptables') {
3356 ### iptables access; the destination IP is only used if access is
3357 ### forwarded through the iptables policy
3358 &grant_ipt_access($src, $msg_hr, $nat_info_hr,
3359 $seq_hr, $open_ports_hr, $access_hr);
3361 } elsif ($config{'FIREWALL_TYPE'} eq 'ipfw') {
3364 &grant_ipfw_access($src, $open_ports_hr, $access_hr);
3370 sub grant_ipt_access() {
3371 my ($src, $msg_hr, $nat_info_hr, $seq_hr,
3372 $open_ports_hr, $access_hr) = @_;
3375 my $ipt = &get_iptables_chainmgr_obj($config{'IPT_EXEC_SLEEP'});
3379 if (keys %$msg_hr) { ### For PK mode, this hash ref is empty
3380 if ($msg_hr->{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE
3381 or $msg_hr->{'action_type'}
3382 == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) {
3387 if ($access_hr->{'ENABLE_FORWARD_ACCESS'} or $local_nat) {
3389 unless (defined $nat_info_hr->{'internal_ip'}) {
3390 print STDERR localtime() . " [-] Internal IP not ",
3391 "defined for NAT\n" if $debug;
3395 push @ipt_hrefs, \%ipt_prerouting if %ipt_prerouting;
3396 push @ipt_hrefs, \%ipt_postrouting if %ipt_postrouting;
3399 push @ipt_hrefs, \%ipt_input;
3400 print STDERR localtime() . " [+] INPUT NAT access for $src ",
3401 "to local IP: $nat_info_hr->{'internal_ip'}\n"
3404 push @ipt_hrefs, \%ipt_forward;
3405 print STDERR localtime() . " [+] FORWARD access for $src ",
3406 "to internal IP: $nat_info_hr->{'internal_ip'}\n"
3411 if (defined $nat_info_hr->{'internal_ip'}) {
3415 push @ipt_hrefs, \%ipt_input;
3416 if ($access_hr->{'ENABLE_OUTPUT_ACCESS'}) {
3417 push @ipt_hrefs, \%ipt_output;
3422 for my $hr (@ipt_hrefs) {
3426 print STDERR localtime() . " [+] ipt_href: $ipt_hr_num\n",
3429 my $nat_ip = '0.0.0.0/0';
3432 ### add rule for $ip unless it already exists
3433 my $target = $hr->{'target'};
3434 my $direction = $hr->{'direction'};
3435 my $table = $hr->{'table'};
3436 my $from_chain = $hr->{'from_chain'};
3437 my $to_chain = $hr->{'to_chain'};
3438 my $jump_rule_position = $hr->{'jump_rule_position'};
3439 my $auto_rule_position = $hr->{'auto_rule_position'};
3441 my $grant_src = $src;
3442 my $grant_dst = '0.0.0.0/0';
3444 if ($direction eq 'dst') {
3447 $grant_src = '0.0.0.0/0';
3454 ### make sure "to_chain" exists
3455 for (my $try=0; $try < $config{'IPT_EXEC_TRIES'}; $try++) {
3456 ($rv, $out_ar, $err_ar)
3457 = $ipt->create_chain($table, $to_chain);
3462 print STDERR localtime() . " create_chain() returned: $rv\n"
3465 print STDERR localtime() . " [-] create_chain() ",
3466 "returned: $rv, errors:\n" if $debug;
3467 &psyslog_errs($err_ar);
3472 ### add jump rule to the "to_chain" from the "from_chain"
3473 for (my $try=0; $try < $config{'IPT_EXEC_TRIES'}; $try++) {
3474 ($rv, $out_ar, $err_ar) = $ipt->add_jump_rule($table,
3475 $from_chain, $jump_rule_position, $to_chain);
3480 print STDERR localtime() . " add_jump_rule() ",
3481 "returned: $rv\n" if $debug;
3483 print STDERR localtime() . " [-] add_jump_rule() ",
3484 "returned: $rv, errors:\n" if $debug;
3485 &psyslog_errs($err_ar);
3490 for my $proto (keys %{$open_ports_hr}) {
3491 for my $port (keys %{$open_ports_hr->{$proto}}) {
3493 my $num_chain_rules = 0;
3497 my %extended_info = ('protocol' => $proto);
3498 if ($direction eq 'dst') {
3500 $extended_info{'s_port'} = $port;
3504 $extended_info{'d_port'} = $port;
3507 ### deal with DNAT and SNAT (normally MASQUERADE unless
3508 ### ENABLE_IPT_SNAT is set)
3509 if ($table eq 'nat' and ($target eq 'DNAT'
3510 or $target eq 'SNAT')) {
3511 if ($target eq 'DNAT') {
3512 $extended_info{'to_ip'}
3513 = $nat_info_hr->{'internal_ip'};
3514 $extended_info{'to_port'} = $dport;
3515 $extended_info{'d_port'}
3516 = $nat_info_hr->{'external_port'};
3517 $nat_ip = $nat_info_hr->{'internal_ip'};
3519 $dport = $nat_info_hr->{'external_port'};
3520 } elsif ($target eq 'SNAT') {
3521 $extended_info{'to_ip'}
3522 = $config{'SNAT_TRANSLATE_IP'};
3523 $extended_info{'to_port'} = $dport;
3524 $extended_info{'d_port'} = $dport;
3525 $nat_ip = $config{'SNAT_TRANSLATE_IP'};
3530 ($rv, $num_chain_rules) = $ipt->find_ip_rule($grant_src,
3531 $grant_dst, $table, $to_chain, $target, \%extended_info);
3534 print STDERR localtime() . " find_ip_rule() ",
3535 "returned $rv\n" if $debug;
3536 my $str = "$grant_src -> $grant_dst($proto/$port)";
3537 if ($direction eq 'dst') {
3538 $str = "$grant_src($proto/$port) -> $grant_dst";
3540 if (defined $extended_info{'to_ip'}) {
3541 $str = "$grant_src -> $extended_info{'to_ip'}" .
3542 "($proto/$extended_info{'d_port'} to " .
3543 "$extended_info{'to_port'})";
3545 &logr('[-]', "source: $str already allowed to connect " .
3546 "in chain: $to_chain", $SEND_MAIL);
3548 print STDERR localtime() . " find_ip_rule() ",
3549 "returned $rv\n" if $debug;
3550 my $str = "add $to_chain $grant_src -> " .
3551 "$grant_dst($proto/$port) $target rule ";
3552 if ($direction eq 'dst') {
3553 $str = "add $to_chain $grant_src($proto/$port) -> " .
3554 "$grant_dst $target rule ";
3556 if (defined $extended_info{'to_ip'}) {
3557 $str = "add $to_chain $grant_src -> " .
3558 "$extended_info{'to_ip'}" .
3559 "($proto/$extended_info{'d_port'} to " .
3560 "$extended_info{'to_port'}) " .
3563 $str .= "$access_hr->{'FW_ACCESS_TIMEOUT'} sec";
3565 &logr('[+]', $str, $SEND_MAIL);
3567 for (my $try=0; $try < $config{'IPT_EXEC_TRIES'}; $try++) {
3568 ($rv, $out_ar, $err_ar) = $ipt->add_ip_rule($grant_src,
3569 $grant_dst, $auto_rule_position, $table, $to_chain,
3570 $target, \%extended_info);
3577 print STDERR localtime() . " [+] add_ip_rule() ",
3579 " [+] Dumping $to_chain to ",
3580 "see newly added rule:\n";
3581 $ipt->run_ipt_cmd("$cmds{'iptables'} -t " .
3582 "$table -v -n -L $to_chain");
3585 ### keep track of how many times we have granted access
3586 $seq_hr->{'grant_ctr'}++ unless
3587 $access_hr->{'DATA_COLLECT_MODE'} == $PCAP
3588 or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP
3589 or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP;
3591 ### Communicate the new firewall rule to knoptm so
3592 ### that it can be removed.
3593 &write_knoptm_fw_cache_entry(
3595 $access_hr->{'FW_ACCESS_TIMEOUT'},
3607 encode_base64('NA', ''),
3611 print STDERR localtime() . " [-] add_ip_rule() ",
3612 "returned $rv\n" if $debug;
3613 &psyslog_errs($err_ar);
3619 $seq_hr->{'port_seq'} = 0
3620 unless $access_hr->{'DATA_COLLECT_MODE'} == $PCAP
3621 or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP
3622 or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP;
3628 sub grant_ipfw_access() {
3629 my ($src, $open_ports_hr, $access_hr) = @_;
3631 my $dst = '0.0.0.0/0';
3633 for my $proto (keys %{$open_ports_hr}) {
3634 for my $port (keys %{$open_ports_hr->{$proto}}) {
3636 my ($active_rulenum, $set_num, $new_rulenum)
3637 = &ipfw_find_ip_rule($src, 'any', $proto, $port);
3639 if ($active_rulenum and $set_num == 0) {
3640 &logr('[-]', "source: $src already allowed " .
3641 "to connect to $proto/$port", $SEND_MAIL);
3645 if ($active_rulenum and $ipfw_is_dynamic
3646 and $set_num == $config{'IPFW_SET_NUM'}) {
3647 $msg = 'reactivating ipfw allow rule for '
3649 $msg = 'adding ipfw allow rule for '
3651 $msg .= "$src -> $proto";