#!/usr/bin/perl -w # ############################################################################# # # File: fwknopd (/usr/sbin/fwknopd) # # URL: http://www.cipherdyne.org/fwknop/ # # Purpose: fwknopd implements the server portion of an authorization scheme # known as Single Packet Authorization (SPA) that requires only a # single encrypted packet to communicate various pieces of # information including desired access through an iptables policy # and/or specific commands to execute on the target system. The # main application of this program is to protect services such as # SSH with an additional layer of security in order to make the # exploitation of vulnerabilities (both 0-day and unpatched code) # much more difficult. For more information, see the fwknop(8) man # page. # # More information can be found in the fwknop(8) and fwknopd(8) man # pages, and also online here: # # http://www.cipherdyne.org/fwknop/docs/ # # Author: Michael Rash (mbr@cipherdyne.org) # # Version: 1.9.12 # # Copyright (C) 2004-2009 Michael Rash (mbr@cipherdyne.org) # # License - GNU Public License version 2 (GPLv2): # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 # USA # ############################################################################# # # $Id: fwknopd 1533 2009-09-08 02:44:02Z mbr $ # use IO::Socket; use IO::Handle; use MIME::Base64; use Data::Dumper; use POSIX ':sys_wait_h'; use Getopt::Long; use strict; my $config_file = '/etc/fwknop/fwknop.conf'; my $access_conf_file = ''; my $version = '1.9.12'; my $revision_svn = '$Revision: 1533 $'; my $rev_num = '1'; ($rev_num) = $revision_svn =~ m|\$Rev.*:\s+(\S+)|; my %config = (); my $override_config_str = ''; my %cmds = (); my %p0f = (); my @access = (); my $blacklist_ar = []; my $blacklist_exclude_ar = []; my %p0f_sigs = (); my %pid_files = (); my %ip_sequences = (); my %digest_store = (); my %ipt_input = (); my %ipt_forward = (); my %ipt_prerouting = (); my %ipt_postrouting = (); my %ipt_output = (); ### optional my @ipt_config = (); my $ipfw_is_dynamic = 0; my $os_fprint_only = 0; my $print_version = 0; my $print_help = 0; my $stop_daemons = 0; my $restart = 0; my $status = 0; my $debug = 0; my $packet_ctr = 0; my $packet_limit = 0; my $lib_dir = ''; my $fw_list = 0; my $fw_type = ''; my $fw_flush = 0; my $ipt_del_chains = 0; my $fw_del_ip = ''; my $test_mode = 0; my $verbose = 0; my $imported_gpg = 0; my $os_ipt_log = ''; my $use_sendmail = 0; my $cmdline_intf = ''; my $warn_msg = ''; my $die_msg = ''; my $cmdline_knoptm = ''; my $skip_fko_module = 0; my $use_fko_module = 0; my $test_fko_exists = 0; my $fko_incoming_digest_type = 0; my $fko_obj = (); my $cmdl_disable_gpg = 0; my $cmdline_fwknop_serv = ''; my $knoptm_debug_file = ''; my $knoptm_include_pidname = 0; my $fwkserv_debug_file = ''; my $fwkserv_include_pidname = 0; my $err_wait_timer = 30; ### seconds my $gpg_agent_info = ''; my $gpg_no_options = 0; my $gpg_use_options = 0; my $gpg_default_prefix = 'hQ'; ### base64 encoded 0x8502 my $build_ipt_config = 0; my $skipped_first_loop = 0; my $imported_crypt_cbc = 0; my $pcap_sleep_interval = 1; ### seconds my $imported_iptables_modules = 0; my $include_all_config_data = 0; my $voluntary_exit_timestamp = 0; my $fw_data_file = ''; ### legacy port knocking mode my $dump_config = 0; my $spa_dump_packets = ''; my $cmdline_locale = ''; my $no_locale = 0; ### SPA message types from fwknop clients ### COMMAND message: ### random data : user : client_timestamp : client_version : \ ### type (0) : command : digest my $SPA_COMMAND_MODE = 0; ### ACCESS message (this type is used most often): ### random data : user : client_timestamp : client_version : \ ### type (1) : access_request : digest my $SPA_ACCESS_MODE = 1; ### default ### NAT ACCESS message: ### random data : user : client_timestamp : client_version : \ ### type (2) : access_request : NAT_info : digest my $SPA_NAT_ACCESS_MODE = 2; ### ACCESS message with client-defined firewall timeout: ### random data : user : client_timestamp : client_version : \ ### type (3) : access_request : timeout : digest my $SPA_CLIENT_TIMEOUT_ACCESS_MODE = 3; ### NAT ACCESS message with client-defined firewall timeout: ### random data : user : client_timestamp : client_version : \ ### type (4) : access_request : NAT_info : timeout : digest my $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE = 4; ### local NAT ACCESS message: ### random data : user : client_timestamp : client_version : \ ### type (5) : access_request : NAT_info : message digest my $SPA_LOCAL_NAT_ACCESS_MODE = 5; ### local NAT ACCESS message with client-defined firewall timeout: ### random data : user : client_timestamp : client_version : \ ### type (6) : access_request : NAT_info : timeout : message digest my $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE = 6; my %spa_mode_strings = ( $SPA_COMMAND_MODE => 'SPA_COMMAND_MODE', $SPA_ACCESS_MODE => 'SPA_ACCESS_MODE', $SPA_NAT_ACCESS_MODE => 'SPA_NAT_ACCESS_MODE', $SPA_CLIENT_TIMEOUT_ACCESS_MODE => 'SPA_CLIENT_TIMEOUT_ACCESS_MODE', $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE => 'SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE', $SPA_LOCAL_NAT_ACCESS_MODE => 'SPA_LOCAL_NAT_ACCESS_MODE', $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE => 'SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE' ); ### limits on nummber of fields within a decrypted SPA packet my $SPA_MIN_PACKET_FIELDS = 7; my $SPA_MAX_PACKET_FIELDS = 9; ### default time values my $knock_interval = 60; my $default_access_timeout = 300; my $enc_port_offset = 61000; ### default offset my $enc_key = ''; my $enc_alg = 'Rijndael'; my $enc_blocksize = 32; ### there is a constant "RIJNDAEL_KEYSIZE" in the Crypt::Rijndael sources, but ### it is not used; a 16 byte key size is fine. my $enc_keysize = 16; my $ALG_RIJNDAEL = 1; my $ALG_GNUPG = 2; my $PCAP = 0; my $FILE_PCAP = 1; my $ULOG_PCAP = 2; my $SHARED_SEQUENCE = 3; my $ENCRYPT_SEQUENCE = 4; ### Bool to detect Linux "Cooked" datalink layers my $PCAP_COOKED_INTF = 0; ### digest constants my $SHA256_DIGEST_LEN = 43; my $SHA1_DIGEST_LEN = 27; my $MD5_DIGEST_LEN = 22; my $FKO_RECOMPUTE = 1; ### logr constants my $SEND_MAIL = 1; my $NO_MAIL = 0; my $LOG_VERBOSE = 1; my $LOG_QUIET = 2; my $ZERO_SLEEP = 0; my $STDOUT = 1; my $STDERR = 2; ### packet counters my $tcp_ctr = 0; my $udp_ctr = 0; my $icmp_ctr = 0; ### protocol values my $IPPROTO_ICMP = 1; my $IPPROTO_TCP = 6; my $IPPROTO_UDP = 17; ### tcp option types my $tcp_nop_type = 1; my $tcp_mss_type = 2; my $tcp_win_scale_type = 3; my $tcp_sack_type = 4; my $tcp_timestamp_type = 8; my %tcp_p0f_opt_types = ( 'N' => $tcp_nop_type, 'M' => $tcp_mss_type, 'W' => $tcp_win_scale_type, 'S' => $tcp_sack_type, 'T' => $tcp_timestamp_type ); my $ETH_HDR_LEN = 14; my $MIN_IP_HDR_LEN = 20; my $MIN_ICMP_HDR_LEN = 8; ### most practical for SPA packets over ICMP my $UDP_HDR_LEN = 8; my $MIN_TCP_HDR_LEN = 20; my $EXTERNAL_CMD_ALARM = 30; ### default for external commands my %access_keys = ( 'SOURCE' => [], 'KEY' => '', 'OPEN_PORTS' => '', 'GPG_REMOTE_ID' => '', 'GPG_DECRYPT_ID' => '', 'GPG_DECRYPT_PW' => '', 'GPG_HOME_DIR' => '', 'GPG_NO_OPTIONS' => 0, 'GPG_USE_OPTIONS' => 0, 'GPG_NO_REQUIRE_PREFIX' => 0, 'GPG_PREFIX' => '', 'GPG_PATH' => '', 'ULOG_PCAP' => '', 'FILE_PCAP' => '', 'DATA_COLLECT_MODE' => '', 'ENCRYPT_SEQUENCE' => '', 'SHARED_SEQUENCE' => '', 'PORT_OFFSET' => '', 'REQUIRE_AUTH_METHOD' => '', 'SHADOW_FILE' => '', 'KNOCK_INTERVAL' => '', 'KNOCK_LIMIT' => '', 'PERMIT_CLIENT_PORTS' => '', 'PERMIT_CLIENT_TIMEOUT' => '', 'ENABLE_FORWARD_ACCESS' => 0, 'ENABLE_CMD_EXEC' => '', 'DISABLE_FW_ACCESS' => '', 'REQUIRE_SOURCE_ADDRESS' => [], 'require_src_addr_exceptions' => [], 'INTERNAL_NET_ACCESS' => [], ### for --Forward-access IP restrictions 'internal_net_exceptions' => [], 'CMD_REGEX' => '', 'FW_ACCESS_TIMEOUT' => '', 'REQUIRE_USERNAME' => '', 'MIN_TIME_DIFF' => '', 'MAX_TIME_DIFF' => '', 'RESTRICT_INTF' => '', 'ENABLE_EXTERNAL_CMDS' => 0, 'EXTERNAL_CMD_OPEN' => '', 'EXTERNAL_CMD_CLOSE' => '', 'EXTERNAL_CMD_ALARM' => '', ); my $ip_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|; my @args_cp = @ARGV; ### run GetOpt() to get comand line args &handle_command_line(); &usage(0) if $print_help; if ($print_version) { print "[+] fwknopd v$version (file revision: $rev_num)\n", " by Michael Rash \n"; exit 0; } if ($os_fprint_only) { print "[+] Entering OS fingerprinting mode.\n"; } print STDERR localtime() . " [+] ** Starting fwknopd (debug mode) **\n", " fwknopd Command line: @args_cp\n" if $debug; ### setup to run &fwknop_init(); if ($config{'AUTH_MODE'} eq 'KNOCK' or $os_fprint_only) { ### we are running in traditional port knocking mode &knock_loop(); } elsif ($config{'AUTH_MODE'} eq 'FILE_PCAP' or $config{'AUTH_MODE'} eq 'ULOG_PCAP' or $config{'AUTH_MODE'} eq 'PCAP') { ### we are parsing the pcap file created by the ulogd pcap ### writer, or in sniffing mode against an interface require Net::Pcap; if ($debug ) { print STDERR "[+] Net::Pcap::VERSION $Net::Pcap::VERSION\n"; } &pcap_loop(); } elsif ($config{'AUTH_MODE'} eq 'SOCKET') { ### we are going to acquire SPA packet data from the fwknop_serv ### process via a domain socket. fwknop_serv itself will listen ### on a tcp or udp port for an incoming SPA packet, so libpcap ### does not become involved in this mode. &socket_loop(); } exit 0; #============================ end main ============================== sub pcap_loop() { ### we use both a size and an inode check in the FILE_PCAP and ### ULOG_PCAP modes to check if the file has been rotated my $pcap_file_size = 0; my $pcap_file_inode = 0; ### get pcap opject my $pcap_t = &get_pcap_obj(); ### Check for "cooked" Linux datalink layers (i.e. rp-pppoe) eval { if (not $PCAP_COOKED_INTF and $Net::Pcap::VERSION > 0.05) { if (Net::Pcap::datalink_val_to_name( Net::Pcap::datalink($pcap_t)) eq 'LINUX_SLL') { print STDERR "[+] Detected Linux Cooked Interface.\n" if $debug; $PCAP_COOKED_INTF = 1; } } }; &collect_warn_die_msgs() if $@; if ($config{'AUTH_MODE'} eq 'FILE_PCAP' or $config{'AUTH_MODE'} eq 'ULOG_PCAP') { ### get file size (we don't need a -e check here because ### this is handled in get_pcap_obj()). $pcap_file_size = -s $config{'PCAP_PKT_FILE'}; ### get inode associated with the sniffing file $pcap_file_inode = (stat($config{'PCAP_PKT_FILE'}))[1]; } print STDERR localtime() . " [+] pcap_loop()\n" if $debug; my $check_file_ctr = 0; &collect_warn_die_msgs(); for (;;) { my @tmpcbargs; my $cbcalled = 0; my $tmpcb = sub { my $tag = $_[0]; my %hdr = %{$_[1]}; my $pkt = $_[2]; @tmpcbargs = ($tag, \%hdr, $pkt); $cbcalled = 1; }; Net::Pcap::loop($pcap_t, 1, $tmpcb, 'fwknop_tag'); pcap_process_pkt(@tmpcbargs) if($cbcalled); if ($config{'AUTH_MODE'} eq 'FILE_PCAP' or $config{'AUTH_MODE'} eq 'ULOG_PCAP') { ### check to see if the pcap file has been rotated (we need to ### close and re-open) if ($check_file_ctr >= 10) { if (-e $config{'PCAP_PKT_FILE'}) { my $size_tmp = -s $config{'PCAP_PKT_FILE'}; my $inode_tmp = (stat($config{'PCAP_PKT_FILE'}))[1]; if ($inode_tmp != $pcap_file_inode or $size_tmp < $pcap_file_size) { ### the file was rotated or shrank, so get new ### pcap_t object Net::Pcap::close($pcap_t); &logr('[+]', "pcap file $config{'PCAP_PKT_FILE'} " . "shrank or was rotated, so re-opening", $NO_MAIL); $pcap_t = &get_pcap_obj(); ### set file size and inode $pcap_file_size = $size_tmp; $pcap_file_inode = $inode_tmp; } } else { Net::Pcap::close($pcap_t); &logr('[+]', "pcap file $config{'PCAP_PKT_FILE'} " . "was rotated, so re-opening", $NO_MAIL); $pcap_t = &get_pcap_obj(); ### set file size and inode $pcap_file_size = -s $config{'PCAP_PKT_FILE'}; $pcap_file_inode = (stat($config{'PCAP_PKT_FILE'}))[1]; } $check_file_ctr = 0; } $check_file_ctr++; } &collect_warn_die_msgs(); sleep $pcap_sleep_interval; } Net::Pcap::close($pcap_t); return; } sub pcap_process_pkt() { my ($tag, $hdr, $pkt) = @_; &collect_warn_die_msgs(); return unless $tag eq 'fwknop_tag'; return unless defined $hdr; return unless defined $pkt; my $ether_data = ''; my $ip = ''; my $src_ip = ''; my $proto = ''; my $transport_data = ''; if ($debug) { print STDERR localtime() . " [+] Received packet ***[" . localtime() . "]***\n"; if ($verbose) { print STDERR localtime() . " Complete raw packet data (hex dump, including ", "packet headers):\n"; &hex_dump($pkt); } } ### check the length of the packet; if it is not at least ### 160 bytes long (this is the default MIN_SPA_PKT_LEN value, and ### this is conservative) then it cannot be an SPA packet my $pkt_len = length($pkt); if (length($pkt) < $config{'MIN_SPA_PKT_LEN'}) { if ($debug and $verbose) { print "[-] Packet length ($pkt_len bytes) less than $config{'MIN_SPA_PKT_LEN'}\n", " minimum, so is not an SPA packet; skipping.\n"; } return; } if ($config{'AUTH_MODE'} eq 'ULOG_PCAP') { ### The ulogd pcap writer does not include link layer information $ip = &ip_decode($pkt) or return; } else { if ($config{'FIREWALL_TYPE'} eq 'ipfw' and $config{'PCAP_INTF'} eq 'lo0') { ### it seems that FreeBSD does not include an Ethernet header ### over loopback but puts a different set of four bytes $pkt =~ s/^.{4}// if $pkt =~ /^[^\x45].{3}\x45/; $ip = &ip_decode($pkt) or return; } else { if ($PCAP_COOKED_INTF) { $ether_data = unpack("x16a*", $pkt); } else { $ether_data = ðernet_strip($pkt) or return; } $ip = &ip_decode($ether_data) or return; } } ### get the source IP address from the IP header $src_ip = $ip->{'src_ip'} or return; ### get the protocol $proto = $ip->{'proto'} or return; if ($proto == $IPPROTO_ICMP) { $transport_data = &icmp_decode_data($ip->{'data'}); } elsif ($proto == $IPPROTO_TCP) { $transport_data = &tcp_decode_data($ip->{'data'}); } elsif ($proto == $IPPROTO_UDP) { $transport_data = &udp_decode_data($ip->{'data'}); } else { return; } &decode_SPA_data($transport_data, $src_ip, $proto); return; } sub decode_SPA_data() { my ($transport_data, $src_ip, $proto) = @_; ### make sure we have _some_ data in the packet; in practice ### any valid SPA message will be longer than 10 bytes, but this ### check is better than nothing return if $transport_data eq ''; my $enc_msg_len = 0; $enc_msg_len = length($transport_data); if (10 < $enc_msg_len and $enc_msg_len < $config{'MAX_SNIFF_BYTES'}) { print STDERR localtime() . " [+] Data len: $enc_msg_len bytes\n" if $debug; } else { print STDERR localtime() . " [-] $enc_msg_len bytes, not ", "attempting decrypt)\n" if $debug; return; } if ($debug) { ### make sure not to print non-printable stuff my $data_tmp = $transport_data; $data_tmp =~ s/[^\x20-\x7e]/NA/g; print STDERR localtime() . " [+] Raw packet data (single line): $data_tmp\n"; ### print packet data out in tcpdump -X format if ($verbose) { print STDERR localtime() . " Raw packet data (hex dump, minus packet headers):\n"; &hex_dump($transport_data); } } my $candidate_spa_data = ''; if ($proto == $IPPROTO_TCP and $config{'ENABLE_SPA_OVER_HTTP'} eq 'Y') { if ($transport_data =~ m|GET\s+(\S+)\s+HTTP/\d|) { $candidate_spa_data = $1; $candidate_spa_data =~ s/\.html// if $candidate_spa_data =~ /\.html/; $candidate_spa_data =~ s|^/|| if $candidate_spa_data =~ m|^/|; $candidate_spa_data =~ s|^http://\S+/|| if $candidate_spa_data =~ m|^http://\S+/|; unless (&is_url_base64($candidate_spa_data)) { if ($debug) { print STDERR localtime() . " [+] Packet contains non-base64 ", "(with URL mods) encoded characters, skipping.\n"; &check_packet_limit(); } return; } } } unless ($candidate_spa_data) { $candidate_spa_data = $transport_data; ### check to make sure the packet data only contains base64 encoded ### characters per RFC 3548: 0-9, A-Z, a-z, +, /, = unless (&is_base64($candidate_spa_data)) { if ($debug) { print STDERR localtime() . " [+] Packet contains non-base64 ", "encoded characters, skipping.\n"; &check_packet_limit(); } return; } } ### see if this packet is worthy of being granted access through ### the firewall &SPA_check_grant_access($src_ip, $enc_msg_len, $candidate_spa_data); &collect_warn_die_msgs(); return; } sub ethernet_strip() { my $pkt = shift; my $eth_data = ''; if (length($pkt) >= $ETH_HDR_LEN) { $eth_data = substr($pkt, $ETH_HDR_LEN); } if (not $eth_data and ($debug and $verbose)) { print "[-] Could not properly decode Ethernet header.\n"; } ### Silently return '' for short frames return $eth_data; } sub ip_addr_bytes_to_string() { my $bytes = shift; my ($a, $b, $c, $d) = unpack('C4', $bytes); return "$a.$b.$c.$d"; } sub ip_decode() { my $pkt = shift; my $ip = {}; if (length($pkt) >= $MIN_IP_HDR_LEN and $pkt =~ /^\x45/) { (my $ver_ihl, $ip->{'tos'}, $ip->{'len'}, $ip->{'id'}, my $flags_frag, $ip->{'ttl'}, $ip->{'proto'}, $ip->{'cksum'}, my $src_ip, my $dest_ip) = unpack("CCnnnCCna4a4", $pkt); $ip->{'ver'} = $ver_ihl >> 4; $ip->{'hlen'} = $ver_ihl & 0x0F; $ip->{'flags'} = $flags_frag >> 13; $ip->{'foffset'} = ($flags_frag & 0x1FFF) * 8; $ip->{'src_ip'} = &ip_addr_bytes_to_string($src_ip); $ip->{'dest_ip'} = &ip_addr_bytes_to_string($dest_ip); my $data_start = $ip->{'hlen'} * 4; if ($data_start >= $MIN_IP_HDR_LEN) { $ip->{'data'} = substr($pkt, $data_start); } } if (not keys %$ip and ($debug and $verbose)) { print "[-] Could not properly decode IP header.\n"; } return $ip; } sub icmp_decode_data() { my $icmp = shift; my $icmp_data = ''; if (length($icmp) >= $MIN_ICMP_HDR_LEN) { $icmp_data = substr($icmp, $MIN_ICMP_HDR_LEN); } ### Silently return '' for short packets if (not $icmp_data and ($debug and $verbose)) { print "[-] Could not properly decode ICMP header.\n"; } return $icmp_data; } sub tcp_decode_data() { my $tcp = shift; my $tcp_data = ''; if (length($tcp) >= $MIN_TCP_HDR_LEN) { my $data_start = 4 * (ord(substr($tcp, 12, 1)) >> 4); if ($data_start >= $MIN_TCP_HDR_LEN) { $tcp_data = substr($tcp, $data_start); } } ### Silently return '' for short packets if (not $tcp_data and ($debug and $verbose)) { print "[-] Could not properly decode TCP header.\n"; } return $tcp_data; } sub udp_decode_data() { my $udp = shift; my $udp_data = ''; if (length($udp) >= $UDP_HDR_LEN) { $udp_data = substr($udp, $UDP_HDR_LEN); } ### Silently return '' for short packets if (not $udp_data and ($debug and $verbose)) { print "[-] Could not properly decode UDP header.\n"; } return $udp_data; } sub socket_loop() { print STDERR localtime() . " [+] socket_loop() acquiring SPA ", "packet data from: $cmds{'fwknop_serv'} via domain socket: ", "$config{'FWKNOP_SERV_SOCK'}\n"; my $fwknop_serv_sock = IO::Socket::UNIX->new( Type => SOCK_STREAM, Local => $config{'FWKNOP_SERV_SOCK'}, Listen => SOMAXCONN, Timeout => .1 ) or die "[*] Could not acquire fwknopd communications domain socket: $!"; for (;;) { my $fwknop_serv_connection = $fwknop_serv_sock->accept(); if ($fwknop_serv_connection) { my @fwknop_serv_msgs = <$fwknop_serv_connection>; for my $msg (@fwknop_serv_msgs) { if ($msg =~ /^($ip_re):(\d{1,2}):(\S+)/) { my $src_ip = $1; my $proto = $2; my $spa_msg = $3; &decode_SPA_data($spa_msg, $src_ip, $proto); } } @fwknop_serv_msgs = (); } } return; } sub SPA_check_grant_access() { my ($src_ip, $enc_msg_len, $pkt_data) = @_; if ($spa_dump_packets) { if (&is_base64($pkt_data)) { print "\nLen: $enc_msg_len, pkt: $pkt_data\n"; } else { print "\nLine contains non base64 chars, skipping.\n"; return; } } ### first check to see if we have any matching access directives ### (in access.conf) for $src_ip, and if not we will do _nothing_ ### with this packet. my $access_nums_aref = &check_src($src_ip); if ($#$access_nums_aref > -1) { ### See if the packet qualifies for any access SOURCE: for my $num (@$access_nums_aref) { my $access_hr = $access[$num]; next SOURCE unless $access_hr->{'DATA_COLLECT_MODE'} == $PCAP or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP; &dump_access($access_hr, $num) if $debug and $verbose; ### keep track of which source block we are dealing with from ### access.conf my $source_block_num = $access_hr->{'block_num'}; ### see if we can decrypt and base64-decode &fko_acquire_object() if $use_fko_module; my ($decrypt_rv, $decrypted_msg, $gpg_sign_id, $decrypt_algo) = &SPA_decrypt($pkt_data, $enc_msg_len, $access_hr); unless ($decrypt_rv) { &fko_destroy_object() if $use_fko_module; next SOURCE; } ### check for replay attacks my ($digest_rv, $digest) = &is_replay_attack($decrypted_msg, $src_ip); if ($digest_rv) { &fko_destroy_object() if $use_fko_module; return; } ### see if we have a syntactically valid message - this ### also runs the check_digest() function to validate the ### internal digest against the decrypted data. my ($validate_rv, $msg_hr) = &pcap_validate_msg( $decrypted_msg, $source_block_num, $access_hr); if ($debug and not $validate_rv) { print STDERR localtime() . " [-] Decrypted message does not ", "conform to a valid SPA packet.\n"; } unless ($validate_rv) { &fko_destroy_object() if $use_fko_module; next SOURCE; } if ($spa_dump_packets) { print " Disk write digest: $digest\n"; for my $key (keys %$msg_hr) { printf " %20s -> %s\n", $key, $msg_hr->{$key}; } return; } ### check to see if client side time stamp is too old my $time_check_rv = &SPA_check_packet_age($msg_hr->{'remote_time'}); unless ($validate_rv) { &fko_destroy_object() if $use_fko_module; next SOURCE; } next SOURCE unless $time_check_rv; ### dump packet to stderr for debugging purposes &SPA_dump_packet($msg_hr) if $debug; ### check username next SOURCE unless &SPA_check_user($access_hr, $src_ip, $msg_hr); unless ($validate_rv) { &fko_destroy_object() if $use_fko_module; next SOURCE; } ### check authentication method unless (&SPA_check_auth_method($access_hr, $src_ip, $msg_hr)) { &fko_destroy_object() if $use_fko_module; next SOURCE; } if ($msg_hr->{'action_type'} == $SPA_ACCESS_MODE or $msg_hr->{'action_type'} == $SPA_NAT_ACCESS_MODE or $msg_hr->{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) { if (&SPA_access($msg_hr, $src_ip, $decrypt_algo, $gpg_sign_id, $digest, $access_hr)) { &fko_destroy_object() if $use_fko_module; last SOURCE; } else { &fko_destroy_object() if $use_fko_module; next SOURCE; } } elsif ($msg_hr->{'action_type'} == $SPA_COMMAND_MODE) { if (&SPA_cmd($msg_hr, $src_ip, $decrypt_algo, $gpg_sign_id, $digest, $access_hr)) { &fko_destroy_object() if $use_fko_module; last SOURCE; } else { &fko_destroy_object() if $use_fko_module; next SOURCE; } } } } else { print STDERR localtime() . " [-] Packet from $src_ip did not ", "match any SOURCE blocks in $config{'ACCESS_CONF'}\n" if $debug; } &check_packet_limit(); return; } sub check_packet_limit() { ### see if we need to exit if the packet limit (set with -C on the ### command line) has been reached return unless $packet_limit; $packet_ctr++; if ($packet_ctr >= $packet_limit) { &logr('[+]', "packet limit ($packet_limit) reached, exiting.", $NO_MAIL); if ($knoptm_debug_file and -e $knoptm_debug_file) { &logr('[+]', "collecting knoptm debug messages " . "from $knoptm_debug_file", $NO_MAIL); open F, "< $knoptm_debug_file" or die $!; while () { chomp; &logr("KNOPTM:", $_, $NO_MAIL); } close F; } if ($fwkserv_debug_file and -e $fwkserv_debug_file) { &logr('[+]', "collecting fwknop_serv debug messages " . "from $fwkserv_debug_file", $NO_MAIL); open F, "< $fwkserv_debug_file" or die $!; while () { chomp; &logr("FWKNOP_SERV:", $_, $NO_MAIL); } close F; } exit 0; } return; } sub SPA_decrypt() { my ($pkt_data, $enc_msg_len, $access_hr) = @_; my $decrypted_msg = ''; my $decrypt_algo = $ALG_RIJNDAEL; my $gpg_sign_id = ''; my $decrypt_rv = 0; if ($debug) { print STDERR localtime() . " [+] Attempting to ", "decrypt the following data ($enc_msg_len bytes):\n"; &hex_dump($pkt_data); } if (not $cmdl_disable_gpg and $enc_msg_len > $config{'MIN_GNUPG_MSG_SIZE'} and defined $access_hr->{'GPG_REMOTE_ID'}) { ### attempt GPG decrypt (only if the length of the encrypted ### payload is greater than the minimum size for an SPA message ### encrypted with GnuPG; even encrypting a single byte of data ### with a 1024 bit GnuPG key results in 340 bytes of encrypted ### payload in my testing). ($decrypt_rv, $decrypted_msg, $gpg_sign_id) = &pcap_GPG_decrypt_msg($pkt_data, $access_hr); $decrypt_algo = $ALG_GNUPG if $decrypt_rv; } ### fall back to Rijndael if the GnuPG decrypt was not successful ### (and note that the GnuPG decryption is only attempted if the ### packet size is large enough). if (defined $access_hr->{'KEY'} and not $decrypt_rv) { ($decrypt_rv, $decrypted_msg) = &pcap_Rijndael_decrypt_msg( $pkt_data, $access_hr->{'KEY'}); } if ($decrypt_rv) { if ($debug and not $use_fko_module) { ### make sure not to print non-printable stuff my $dec_tmp_msg = $decrypted_msg; $dec_tmp_msg =~ s/[^\x20-\x7e]/NA/g; print STDERR localtime() . " [+] Decrypted ", "message: $dec_tmp_msg\n"; if ($verbose) { print STDERR localtime() . " Decrypted message (hex dump):\n"; &hex_dump($decrypted_msg); } } } else { print STDERR localtime() . " [-] Failed decrypt for SOURCE block ", "$access_hr->{'src_str'}\n" if $debug; } return $decrypt_rv, $decrypted_msg, $gpg_sign_id, $decrypt_algo; } sub SPA_check_packet_age() { my $remote_time = shift; if ($config{'ENABLE_SPA_PACKET_AGING'} eq 'Y') { my $time_diff = time() - $remote_time; if (abs($time_diff) > $config{'MAX_SPA_PACKET_AGE'}) { &logr('[-]', "remote time stamp age difference is larger than " . "$config{'MAX_SPA_PACKET_AGE'} second max.", $SEND_MAIL); print STDERR localtime() . " [-] Time difference: $time_diff " . "(seconds), " . ($time_diff / 3600) . " (hours)\n"; return 0; } } return 1; } sub SPA_dump_packet() { my $msg_hr = shift; print STDERR localtime() . " [+] Packet fields:\n"; printf STDERR " %-16s %s\n %-16s %s\n %-16s %s\n" . " %-16s %s\n %-16s %s", 'Random data:', $msg_hr->{'random_number'}, 'Username:', $msg_hr->{'username'}, 'Remote time:', $msg_hr->{'remote_time'}, 'Remote ver:', $msg_hr->{'remote_version'}, 'Action type:', $msg_hr->{'action_type'}; for my $action_type (keys %spa_mode_strings) { if ($msg_hr->{'action_type'} == $action_type) { print STDERR " ($spa_mode_strings{$action_type})\n"; last; } } printf STDERR " %-16s %s\n", 'Action:', $msg_hr->{'action'}; if ($msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) { printf STDERR " %-16s %s\n", 'Client timeout:', $msg_hr->{'client_timeout'}; } if ($msg_hr->{'server_auth'}) { if ($msg_hr->{'server_auth'} =~ /^\s*(\w+),(.*)/) { my $server_auth_type = lc($1); my $server_auth_crypt_pw = $2; if ($debug) { printf STDERR " %-16s %s", 'Server auth:', $server_auth_type; for (my $i=0; $i < length($server_auth_crypt_pw); $i++) { print STDERR '*'; } print STDERR "\n"; } } } if ($msg_hr->{'nat_info'}) { printf STDERR " %-16s %s\n", 'NAT info:', $msg_hr->{'nat_info'}; } printf STDERR " %-16s %s\n", "$msg_hr->{'digest_str'} digest:", $msg_hr->{'digest'}; return; } sub SPA_check_user() { my ($access_hr, $src_ip, $msg_hr) = @_; if (defined $access_hr->{'REQUIRE_USERNAME'}) { my $found = 0; my $user = ''; for my $valid_user (@{$access_hr->{'VALID_USERS'}}) { if ($valid_user eq $msg_hr->{'username'}) { $found = 1; $user = $valid_user; } } unless ($found) { &logr('[-]', "username mismatch from $src_ip, expecting " . "$access_hr->{'REQUIRE_USERNAME'}, got " . "$msg_hr->{'username'}", $SEND_MAIL); return 0; } } return 1; } sub SPA_check_auth_method() { my ($access_hr, $src_ip, $msg_hr) = @_; my $server_auth_type = ''; my $server_auth_crypt_pw = ''; if ($msg_hr->{'server_auth'}) { if ($msg_hr->{'server_auth'} =~ /^\s*(\w+),(.*)/) { $server_auth_type = lc($1); $server_auth_crypt_pw = $2; } } if (defined $access_hr->{'REQUIRE_AUTH_METHOD'}) { if ($server_auth_type eq $access_hr->{'REQUIRE_AUTH_METHOD'}) { if ($server_auth_type eq 'crypt') { ### check the local UNIX crypt() password associated ### with the user unless (&server_auth_verify_crypt_pw( $msg_hr->{'username'}, $server_auth_crypt_pw, $access_hr->{'SHADOW_FILE'})) { &logr('[-]', "IP: $src_ip failed server-auth UNIX " . "crypt() password test", $NO_MAIL); return 0; } } } else { &logr('[-]', "required server-auth method " . "\"$access_hr->{'REQUIRE_AUTH_METHOD'}\" " . "not supplied by $src_ip", $NO_MAIL); return 0; } } return 1; } sub SPA_access() { my ($msg_hr, $src_ip, $decrypt_algo, $gpg_sign_id, $digest, $access_hr) = @_; my $allow_src = ''; my %open_ports = (); my %grant_ports = (); my %nat_info = (); my $grant_access = 0; if ($access_hr->{'DISABLE_FW_ACCESS'}) { &logr('[-]', "received fw access request from $src_ip, " . "but DISABLE_FW_ACCESS is set to a true value " . "(SOURCE line num: $access_hr->{'src_line_num'})", $NO_MAIL); return 0; } if ($msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) { if ($access_hr->{'PERMIT_CLIENT_TIMEOUT'}) { $access_hr->{'FW_ACCESS_TIMEOUT'} = $msg_hr->{'client_timeout'}; } else { &logr('[-]', "received fw access request from $src_ip, " . "with client-defined timeout, but PERMIT_CLIENT_TIMEOUT is not " . "set (SOURCE line num: $access_hr->{'src_line_num'})", $NO_MAIL); return 0; } } $allow_src = $1 if $msg_hr->{'action'} =~ /($ip_re)/; unless ($allow_src) { &logr('[-]', "no valid IP address within action portion of SPA " . "packet from $src_ip (SOURCE line num: " . "$access_hr->{'src_line_num'})", $SEND_MAIL); return 0; } if ($allow_src eq '0.0.0.0') { if ($config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y' or not &is_ip_included($src_ip, $access_hr->{'REQUIRE_SOURCE_ADDRESS'}, $access_hr->{'require_src_addr_exceptions'})) { &logr('[-]', "IP: $src_ip sent SPA packet that " . "contained 0.0.0.0 (-s on the client side) " . "but REQUIRE_SOURCE_ADDRESS is enabled " . "(SOURCE line num: $access_hr->{'src_line_num'})", $SEND_MAIL); return 0; } else { $allow_src = $src_ip; } } if (&is_ip_included($allow_src, $blacklist_ar, $blacklist_exclude_ar)) { print STDERR localtime() . " [+] SPA_access() ", "$allow_src in BLACKLIST" if $debug; &logr('[-]', "allow IP: $allow_src SPA packet from $src_ip is " . "blacklisted (SOURCE line num: " . "$access_hr->{'src_line_num'})", $SEND_MAIL); return 0; } ### initialize to the OPEN_PORTS directives (if defined; we know that ### either OPEN_PORTS or PERMIT_CLIENT_PORTS was specified in the ### access.conf file) %open_ports = %{$access_hr->{'OPEN_PORTS'}} if defined $access_hr->{'OPEN_PORTS'}; if ($access_hr->{'ENABLE_EXTERNAL_CMDS'} or ($config{'FIREWALL_TYPE'} eq 'external_cmd' and $config{'ENABLE_EXTERNAL_CMDS'} eq 'Y')) { $grant_access = 1; } if ($msg_hr->{'action'} =~ /$ip_re,(tcp|udp|icmp),(\d+)/i) { ### single port access format (e.g. tcp,22) my $allow_port = $1; my $allow_proto = $2; if ($access_hr->{'PERMIT_CLIENT_PORTS'}) { $grant_ports{$allow_proto}{$allow_port} = ''; $grant_access = 1; } else { if (defined $open_ports{$allow_proto} and defined $open_ports{$allow_proto}{$allow_port}) { $grant_ports{$allow_proto}{$allow_port} = ''; $grant_access = 1; } else { unless ($grant_access) { &logr('[-]', "IP $allow_src not permitted to open " . "$allow_proto/$allow_port (SOURCE line num: " . "$access_hr->{'src_line_num'})", $NO_MAIL); return 0; } } } } elsif ($msg_hr->{'action'} =~ /$ip_re,(\S+)/) { ### multi-port access format (-A was specified by ### the client) my $access_str = $1; my @dec_allow_ports = split /,/, $access_str; for my $port_str (@dec_allow_ports) { if ($port_str =~ m|(\D+)/(\d+)|) { my $proto = lc($1); my $port = $2; next unless ($proto eq 'tcp' or $proto eq 'udp' or $proto eq 'icmp'); $port = 0 if $proto eq 'icmp'; if ($access_hr->{'PERMIT_CLIENT_PORTS'}) { $grant_ports{$proto}{$port} = ''; $grant_access = 1; } else { if (defined $open_ports{$proto} and defined $open_ports{$proto}{$port}) { $grant_ports{$proto}{$port} = ''; $grant_access = 1; } else { unless ($grant_access) { &logr('[-]', "IP $allow_src not permitted to " . "open $proto/$port (SOURCE line num: " . "$access_hr->{'src_line_num'})", $NO_MAIL); return 0; } } } } } } ### handle SPA access through iptables FORWARD chain for ### SPA_NAT_ACCESS_MODE messages (or through the INPUT chain for ### SPA_LOCAL_NAT_ACCESS_MODE messages) ### iptables -t nat -A PREROUTING -p tcp -s --dport 55000 \ ### -i eth0 -j DNAT --to 192.168.10.3:80 if ($msg_hr->{'nat_info'} and $msg_hr->{'nat_info'} =~ /($ip_re),(\d+)/) { %nat_info = ( 'internal_ip' => $1, 'external_port' => $2, ); unless ($config{'FIREWALL_TYPE'} eq 'iptables') { &logr('[-]', "NAT access requested through non-iptables " . "firewall (SOURCE line num: ". "$access_hr->{'src_line_num'})", $NO_MAIL); return 0; } if ($msg_hr->{'action_type'} == $SPA_NAT_ACCESS_MODE or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE) { unless ($access_hr->{'ENABLE_FORWARD_ACCESS'}) { &logr('[-]', "FORWARD access requested through non-forward " . "access SOURCE block (SOURCE line num: ". "$access_hr->{'src_line_num'})", $NO_MAIL); return 0; } } if ($msg_hr->{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) { unless ($config{'ENABLE_IPT_LOCAL_NAT'} eq 'Y') { &logr('[-]', "Local NAT access requested without " . "ENABLE_IPT_LOCAL_NAT enabled", $NO_MAIL); return 0; } } ### check to see if access is allowed to internal IP (or a local IP ### for NAT'd local connections) unless (&is_ip_included($nat_info{'internal_ip'}, $access_hr->{'INTERNAL_NET_ACCESS'}, $access_hr->{'internal_net_exceptions'})) { &logr('[-]', "NAT access to $nat_info{'internal_ip'} " . "restricted (SOURCE line num: ". "$access_hr->{'src_line_num'})", $NO_MAIL); return 0; } my $port_ctr = 0; for my $proto (keys %grant_ports) { for my $port (keys %{$grant_ports{$proto}}) { $port_ctr++; } } ### we can only map one forwarding port on the external interface ### to be forwarded to one internal service if ($port_ctr > 1) { &logr('[-]', "cannot forward more than one port " . "(SOURCE line num: $access_hr->{'src_line_num'})", $NO_MAIL); return 0; } } else { if ($access_hr->{'ENABLE_FORWARD_ACCESS'}) { &logr('[-]', "non-forward access requested through FORWARD " . "access SOURCE block (SOURCE line num: " . "$access_hr->{'src_line_num'})", $NO_MAIL); return 0; } } if ($decrypt_algo == $ALG_GNUPG) { if ($access_hr->{'GPG_REMOTE_ID'} ne 'ANY') { &logr('[+]', "received valid GnuPG encrypted packet " . qq|(signed with required key ID: "$gpg_sign_id") from: | . "$src_ip, remote user: $msg_hr->{'username'}, " . "client version: $msg_hr->{'remote_version'} " . "(SOURCE line num: $access_hr->{'src_line_num'})", $NO_MAIL); } else { &logr('[+]', "received valid GnuPG encrypted packet " . "from: $src_ip, remote user: $msg_hr->{'username'}, " . "client version: $msg_hr->{'remote_version'} " . "(SOURCE line num: $access_hr->{'src_line_num'})", $NO_MAIL); } } else { &logr('[+]', "received valid Rijndael encrypted " . "packet from: $src_ip, remote user: $msg_hr->{'username'}, " . "client version: $msg_hr->{'remote_version'} " . "(SOURCE line num: $access_hr->{'src_line_num'})", $NO_MAIL); } unless ($grant_access) { &logr('[-]', "Could not work out access to ports from SPA packet " . "originating from: $src_ip", $NO_MAIL); return 0; } ### cache the digest $digest_store{$digest} = $src_ip; ### write digest to disk &diskwrite_digest($digest, $src_ip) if $config{'ENABLE_DIGEST_PERSISTENCE'} eq 'Y'; ### grant access through the firewall &grant_access($allow_src, $msg_hr, \%nat_info, {}, \%grant_ports, $access_hr); return 1; } sub SPA_cmd() { my ($msg_hr, $src_ip, $decrypt_algo, $gpg_sign_id, $digest, $access_hr) = @_; unless ($access_hr->{'ENABLE_CMD_EXEC'}) { &logr('[-]', qq|received command "$msg_hr->{'action'}" | . "but command mode not enabled for $src_ip", $SEND_MAIL); return 0; } if (defined $access_hr->{'CMD_REGEX'}) { unless ($msg_hr->{'action'} =~ m|$access_hr->{'CMD_REGEX'}|) { &logr('[-]', qq|received command "$msg_hr->{'action'}" | . "from $src_ip but CMD_REGEX did not match $src_ip", $SEND_MAIL); return 0; } } my $cmd = $msg_hr->{'action'}; my $run_cmd = ''; my $cmd_ip = ''; if ($cmd =~ m|^\s*($ip_re),(.*)|) { $cmd_ip = $1; $run_cmd = $2; } else { $run_cmd = $cmd; } ### pre-1.0 versions did not prepend command string with "," if ($cmd_ip eq '0.0.0.0') { if ($config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y' or not &is_ip_included($cmd_ip, $access_hr->{'REQUIRE_SOURCE_ADDRESS'}, $access_hr->{'require_src_addr_exceptions'})) { &logr('[-]', "IP: $src_ip sent SPA packet that " . "contained 0.0.0.0 (-s on the client side) " . "but REQUIRE_SOURCE_ADDRESS is enabled " . "(SOURCE line num: $access_hr->{'src_line_num'})", $SEND_MAIL); return 0; } } if (&is_ip_included($cmd_ip, $blacklist_ar, $blacklist_exclude_ar)) { print STDERR localtime() . " [+] SPA_cmd() ", "$cmd_ip in BLACKLIST" if $debug; &logr('[-]', "cmd IP: $cmd_ip SPA packet from $src_ip is " . "blacklisted (SOURCE line num: " . "$access_hr->{'src_line_num'})", $SEND_MAIL); return 0; } if ($decrypt_algo == $ALG_GNUPG) { if ($access_hr->{'GPG_REMOTE_ID'} ne 'ANY') { &logr('[+]', "received valid GnuPG encrypted packet " . qq|(signed with required key ID: "$gpg_sign_id") from: | . "$src_ip, remote user: $msg_hr->{'username'}", $NO_MAIL); } else { &logr('[+]', "received valid GnuPG encrypted packet " . "from: $src_ip, remote user: $msg_hr->{'username'}", $NO_MAIL); } } else { &logr('[+]', "received valid Rijndael encrypted " . "packet from: $src_ip, remote user: $msg_hr->{'username'}", $NO_MAIL); } &logr('[+]', qq|executing command "$run_cmd" for $src_ip|, $SEND_MAIL); ### cache the digest $digest_store{$digest} = $src_ip; ### write the digest to disk &diskwrite_digest($digest, $src_ip) if $config{'ENABLE_DIGEST_PERSISTENCE'} eq 'Y'; ### execute the command &exec_command($run_cmd, $config{'PCAP_CMD_TIMEOUT'}); return 1; } sub external_cmd_open() { my ($src, $msg_hr, $open_ports_hr, $access_hr) = @_; my $open_cmd = ''; my $close_cmd = ''; my $cmd_port = 0; my $cmd_proto = 'NA'; my $found_port_proto = 0; my $cmd_alarm = $EXTERNAL_CMD_ALARM; if ($access_hr->{'EXTERNAL_CMD_OPEN'}) { $open_cmd = $access_hr->{'EXTERNAL_CMD_OPEN'}; $close_cmd = $access_hr->{'EXTERNAL_CMD_CLOSE'}; $cmd_alarm = $access_hr->{'EXTERNAL_CMD_ALARM'}; } elsif ($config{'EXTERNAL_CMD_OPEN'} and $config{'EXTERNAL_CMD_CLOSE'}) { $open_cmd = $config{'EXTERNAL_CMD_OPEN'}; $close_cmd = $config{'EXTERNAL_CMD_CLOSE'}; $cmd_alarm = $config{'EXTERNAL_CMD_ALARM'}; } else { return; } PROTO: for my $proto (keys %{$open_ports_hr}) { for my $port (keys %{$open_ports_hr->{$proto}}) { ### only allow one port/proto substitution for now - this can be ### worked around by passing OPEN_PORTS directly (via key ### substitution below) on the external command line. $cmd_port = $port; $cmd_proto = $proto; $found_port_proto = 1; last PROTO; } } ### perform variable substitutions on the external command to run $open_cmd = &external_cmd_str_expand($open_cmd, $src, $cmd_port, $cmd_proto, $access_hr); $close_cmd = &external_cmd_str_expand($close_cmd, $src, $cmd_port, $cmd_proto, $access_hr); &logr('[+]', qq|executing external open command "$open_cmd" for $src|, $SEND_MAIL); ### execute the "open" command &exec_command($open_cmd, $cmd_alarm); ### let knoptm run the "close" command &write_knoptm_fw_cache_entry( time(), $access_hr->{'FW_ACCESS_TIMEOUT'}, $src, 0, '0.0.0.0', $cmd_port, $cmd_proto, 'NA', 'NA', 'NA', 'NA', '0.0.0.0/0', 0, encode_base64($close_cmd, ''), $cmd_alarm ); return; } sub external_cmd_str_expand() { my ($cmd_str, $src, $cmd_port, $cmd_proto, $access_hr) = @_; print STDERR localtime() . " [+] External command ", "(before var expansion): $cmd_str\n" if $debug; ### expand SPA source IP, port, and protocol if ($config{'ENABLE_EXT_CMD_PREFIX'} eq 'Y') { $cmd_str =~ s|\$$config{'EXT_CMD_PREFIX'}SRC|$src|; $cmd_str =~ s|\$$config{'EXT_CMD_PREFIX'}PORT|$cmd_port|; $cmd_str =~ s|\$$config{'EXT_CMD_PREFIX'}PROTO|$cmd_proto|; } else { $cmd_str =~ s|\$SRC|$src|; $cmd_str =~ s|\$PORT|$cmd_port|; $cmd_str =~ s|\$PROTO|$cmd_proto|; } ### expand any hash keys from access.conf for my $key (keys %access_keys) { next unless defined $access_hr->{$key}; if ($config{'ENABLE_EXT_CMD_PREFIX'} eq 'Y') { $cmd_str =~ s|\$$config{'EXT_CMD_PREFIX'}$key|$access_hr->{$key}|; } else { $cmd_str =~ s|\$$key|$access_hr->{$key}|; } } print STDERR localtime() . " External command ", "(after var expansion): $cmd_str\n" if $debug; return $cmd_str; } sub is_replay_attack() { my ($decrypted_data, $src_ip) = @_; my $rv = 0; my @digests = (); my $disk_write_digest = ''; if ($use_fko_module) { ### store off the original digest type associated with this incoming ### SPA packet $fko_incoming_digest_type = 0; $fko_incoming_digest_type = $fko_obj->digest_type() or return 1, ''; } if ($config{'DIGEST_TYPE'} eq 'ALL') { if ($use_fko_module) { for my $digest_type (FKO->FKO_DIGEST_SHA256, FKO->FKO_DIGEST_SHA1, FKO->FKO_DIGEST_MD5) { my $digest = &fko_compute_digest($digest_type); if ($digest) { push @digests, $digest; } else { return 1, ''; } } } else { push @digests, sha256_base64($decrypted_data); push @digests, sha1_base64($decrypted_data); push @digests, md5_base64($decrypted_data); } } else { if ($config{'DIGEST_TYPE'} =~ /SHA256/) { if ($use_fko_module) { my $digest = &fko_compute_digest(FKO->FKO_DIGEST_SHA256); if ($digest) { push @digests, $digest; } else { return 1, ''; } } else { push @digests, sha256_base64($decrypted_data); } } if ($config{'DIGEST_TYPE'} =~ /SHA1/) { if ($use_fko_module) { my $digest = &fko_compute_digest(FKO->FKO_DIGEST_SHA1); if ($digest) { push @digests, $digest; } else { return 1, ''; } } else { push @digests, sha1_base64($decrypted_data); } } if ($config{'DIGEST_TYPE'} =~ /MD5/) { if ($use_fko_module) { my $digest = &fko_compute_digest(FKO->FKO_DIGEST_MD5); if ($digest) { push @digests, $digest; } else { return 1, ''; } } else { push @digests, md5_base64($decrypted_data); } } } if (@digests) { ### this prefers SHA256 because of the ordering above. $disk_write_digest = $digests[0]; if ($debug) { print STDERR localtime() . ' [+] Final @digests array: ', "\n", Dumper(@digests); } for my $digest (@digests) { ### note that the %digest_store may contain non-SHA256 digests from ### a previous instance of fwknop - this check ensures that we ### consider all previous digests if (defined $digest_store{$digest}) { ### Replay attack! Send warning email and return. if ($digest_store{$digest}) { &logr('[-]', "attempted SPA packet replay from: $src_ip " . "(original SPA src: $digest_store{$digest}, " . "digest: $digest)", $SEND_MAIL); } else { &logr('[-]', "attempted SPA packet replay from: $src_ip " . "($digest: $digest)", $SEND_MAIL); } ### see if we need to exit if the packet limit (set with -C on the ### command line) has been reached &check_packet_limit(); $rv = 1; last; } } } else { ### could not calculate the digest for some reason; don't ### trust the packet &logr('[-]', "could not calculate digest " . "for SPA packet from: $src_ip", $SEND_MAIL); $rv = 1; } return $rv, $disk_write_digest; } sub fko_acquire_object() { &fko_destroy_object() if $fko_obj; ### initialize the FKO object $fko_obj = FKO->new() or die "[*] Could not acquire FKO object: ", FKO->error_str; if ($debug) { print STDERR localtime() . " [+] Using libfko ", "functions via the FKO module.\n"; } return; } sub fko_destroy_object() { $fko_obj->destroy(); $fko_obj = (); return; } sub fko_compute_digest() { my $digest_type = shift; my $fko_err = $fko_obj->digest_type($digest_type); if ($fko_err) { &logr('[-]', "FKO error setting digest type " . "$digest_type: " . $fko_obj->errstr($fko_err), $NO_MAIL); } $fko_err = $fko_obj->spa_digest($FKO_RECOMPUTE); if ($fko_err) { &logr('[-]', "FKO error recomputing computing digest: " . $fko_obj->errstr($fko_err), $NO_MAIL); return 0; } my $digest = $fko_obj->spa_digest(); unless ($digest) { &logr('[-]', "FKO error computing digest: " . $fko_obj->errstr($fko_err), $NO_MAIL); return 0; } print STDERR localtime() . " [+] FKO calculated digest ", "(type: $digest_type): $digest\n" if $debug; return $digest; } sub server_auth_verify_crypt_pw() { my ($username, $pw, $shadow_file) = @_; unless (-e $shadow_file) { &logr('[-]', "shadow file $shadow_file does not exist", $NO_MAIL); return 0; } my $shadow_hash = ''; open S, "< $shadow_file" or die "[*] Could not open $shadow_file: $!"; while () { my $line = $_; if ($line =~ /^\s*$username:(\S+?):/) { $shadow_hash = $1; } } close S; ### mbr:$1$nrU****************************:13108:0:99999:7::: unless ($shadow_hash) { &logr('[-]', "could not get password entry for $username " . "from /etc/shadow", $NO_MAIL); return 0; } return 1 if (crypt($pw, $shadow_hash) eq $shadow_hash); return 0; } sub knock_loop() { print STDERR localtime() . " [+] Opening $fw_data_file, and ", "entering main loop.\n" if $debug; ### track file size so we can re-open if the logfile is rotated my $fw_data_file_size = -s $fw_data_file; my $fw_data_file_inode = (stat($fw_data_file))[1]; my $fw_data_file_check_ctr = 0; my $skip_first_loop = 1; open FWLOG, $fw_data_file or die "[*] Could not open $fw_data_file: $!"; ### main server loop to parse iptables log messages MAIN: for (;;) { my @fw_pkts = (); ### allow the contents of the fwdata file to be processed only after ### the first loop has been executed. if ($skip_first_loop) { $skip_first_loop = 0; seek FWLOG,0,2; ### seek to the end of the file next MAIN; } else { @fw_pkts = ; } if ($fw_data_file_check_ctr == 10) { if (-e $fw_data_file) { my $size_tmp = -s $fw_data_file; my $inode_tmp = (stat($fw_data_file))[1]; if ($inode_tmp != $fw_data_file_inode or $size_tmp < $fw_data_file_size) { close FWDATA; &sys_log('[+]', "iptables syslog file $fw_data_file " . "shrank or was rotated, so re-opening"); ### re-open the fwdata file open FWDATA, $fw_data_file or die "[*] Could not open $fw_data_file: $!"; $skip_first_loop = 1; ### set file size and inode $fw_data_file_size = $size_tmp; $fw_data_file_inode = $inode_tmp; } } $fw_data_file_check_ctr = 0; } &process_pkts(\@fw_pkts) if @fw_pkts; ### always check to see if we need to timeout knock sequences ### that exceed the KNOCK_INTERVAL &timeout_invalid_sequences(); &collect_warn_die_msgs(); ### clearerr() on the FWLOG filehandle to be ready for new packets FWLOG->clearerr(); sleep $config{'SLEEP_INTERVAL'}; } close FWLOG; return; } sub pcap_validate_msg() { my ($msg, $source_block_num, $access_hr) = @_; my %msg_hsh = ( 'random_number' => 0, 'username' => '', 'remote_time' => 0, 'remote_version' => '', 'numeric_version' => 0, ### calculated locally by fwknopd 'action_type' => -1, 'action' => '', 'server_auth' => '', ### optional 'nat_info' => '', ### optional 'client_timeout' => -1, ### optional 'digest' => '' ); my @fields = (); my $fko_err = 0; ### the last field in the SPA packet is the digest, so see if it ### checks out first (this is the internal digest, not the digest that ### guards against replay attacks). unless (&check_digest($msg, \%msg_hsh)) { print STDERR localtime() . " [-] Key mis-match or broken message ", "checksum for SOURCE $access_hr->{'src_str'} ", "(# $source_block_num in access.conf)\n" if $debug; return 0, {}; } unless ($use_fko_module) { @fields = split /:/, $msg; unless (@fields) { print STDERR localtime() . " [-] Could not split decrypted ", "message into an array.\n" if $debug; return 0, {}; } if ($debug and $verbose) { print STDERR localtime() . " [+] Packet array:\n", Dumper @fields; } unless ($#fields+1 >= $SPA_MIN_PACKET_FIELDS and $#fields+1 <= $SPA_MAX_PACKET_FIELDS) { print STDERR localtime() . " [-] Invalid number of fields in ", "SPA packet, expected $SPA_MIN_PACKET_FIELDS-", "$SPA_MAX_PACKET_FIELDS, got " . ($#fields+1) . ".\n" if $debug; return 0, {}; } } ### random number # if ($use_fko_module) { $msg_hsh{'random_number'} = $fko_obj->rand_value(); } else { $msg_hsh{'random_number'} = $fields[0]; } unless (&is_digit($msg_hsh{'random_number'})) { &logr('[-]', "non-digit random number in decrypted SPA " . "packet: $msg_hsh{'random_number'}", $SEND_MAIL); return 0, {}; } ### username # if ($use_fko_module) { $msg_hsh{'username'} = $fko_obj->username(); } else { $msg_hsh{'username'} = decode_base64($fields[1]); } ### timestamp # if ($use_fko_module) { $msg_hsh{'remote_time'} = $fko_obj->timestamp(); } else { $msg_hsh{'remote_time'} = $fields[2]; } unless (&is_digit($msg_hsh{'remote_time'})) { &logr('[-]', "non-digit timestamp in decrypted SPA packet", $SEND_MAIL); return 0, {}; } ### remote client version # if ($use_fko_module) { $msg_hsh{'remote_version'} = $fko_obj->version(); } else { $msg_hsh{'remote_version'} = $fields[3]; } unless (&SPA_parse_client_version(\%msg_hsh)) { &logr('[-]', "invalid client string in decrypted SPA packet", $SEND_MAIL); return 0, {}; } ### message type # if ($use_fko_module) { $msg_hsh{'action_type'} = $fko_obj->spa_message_type(); } else { $msg_hsh{'action_type'} = $fields[4]; } if (&is_digit($msg_hsh{'action_type'})) { return 0, {} unless $msg_hsh{'action_type'} == $SPA_COMMAND_MODE or $msg_hsh{'action_type'} == $SPA_ACCESS_MODE or $msg_hsh{'action_type'} == $SPA_NAT_ACCESS_MODE or $msg_hsh{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE or $msg_hsh{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE or $msg_hsh{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE or $msg_hsh{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE; $msg_hsh{'action_type'} = $msg_hsh{'action_type'}; } else { &logr('[-]', "non-digit action type in decrypted SPA packet", $SEND_MAIL); return 0, {}; } if ($debug) { print STDERR localtime() . " [+] SPA action type: $msg_hsh{'action_type'}\n"; } ### action # if ($use_fko_module) { $msg_hsh{'action'} = $fko_obj->spa_message(); } else { $msg_hsh{'action'} = decode_base64($fields[5]); } ### server_auth was introduced in 0.9.3 # if ($msg_hsh{'numeric_version'} >= 93) { ### iptables FORWARD/DNAT access was introduced in 1.9.0 if ($msg_hsh{'numeric_version'} >= 190) { my $found = 0; if ($msg_hsh{'action_type'} == $SPA_NAT_ACCESS_MODE or $msg_hsh{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE) { if ($use_fko_module) { $msg_hsh{'nat_info'} = $fko_obj->spa_nat_access(); } else { if ($#fields == $SPA_MIN_PACKET_FIELDS) { $msg_hsh{'nat_info'} = decode_base64($fields[6]); } } $found = 1; } elsif ($msg_hsh{'numeric_version'} >= 192) { ### client timeouts were introduced in 1.9.2 if ($msg_hsh{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE) { if ($use_fko_module) { $msg_hsh{'client_timeout'} = $fko_obj->spa_client_timeout(); } else { $msg_hsh{'client_timeout'} = $fields[6]; } $found = 1; } elsif ($msg_hsh{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE) { if ($use_fko_module) { $msg_hsh{'nat_info'} = $fko_obj->spa_nat_access(); $msg_hsh{'client_timeout'} = $fko_obj->spa_client_timeout(); } else { $msg_hsh{'nat_info'} = decode_base64($fields[6]); $msg_hsh{'client_timeout'} = $fields[7]; } $found = 1; } elsif ($msg_hsh{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) { if ($use_fko_module) { $msg_hsh{'nat_info'} = $fko_obj->spa_nat_access(); $msg_hsh{'client_timeout'} = $fko_obj->spa_client_timeout(); } else { $msg_hsh{'nat_info'} = decode_base64($fields[6]); $msg_hsh{'client_timeout'} = $fields[7]; } $found = 1; } if ($found) { unless (&is_digit($msg_hsh{'client_timeout'})) { &logr('[-]', "non-digit client timeout in decrypted " . "SPA packet", $SEND_MAIL); return 0, {}; } } } unless ($found) { if (not $use_fko_module and $#fields+1 > $SPA_MIN_PACKET_FIELDS) { $msg_hsh{'server_auth'} = decode_base64($fields[6]); } } } else { if ($use_fko_module) { &logr('[-]', "remote libfko version less than minimum ", "required by FKO module", $SEND_MAIL); return 0, {}; } else { if ($#fields+1 > $SPA_MIN_PACKET_FIELDS) { $msg_hsh{'server_auth'} = decode_base64($fields[6]); } } } } else { unless ($use_fko_module and $#fields+1 == $SPA_MIN_PACKET_FIELDS) { print STDERR localtime() . " [-] SPA packet from version: ", "$msg_hsh{'remote_version'} ", "does not have $SPA_MIN_PACKET_FIELDS fields" if $debug; return 0, {}; } } print STDERR Dumper \%msg_hsh if $debug and $verbose; if ($debug) { print STDERR localtime() . " [+] Decoded message: $msg_hsh{'random_number'}:", "$msg_hsh{'username'}:$msg_hsh{'remote_time'}:", "$msg_hsh{'remote_version'}:$msg_hsh{'action_type'}:", "$msg_hsh{'action'}"; if ($msg_hsh{'nat_info'}) { print STDERR ":$msg_hsh{'nat_info'}"; } if ($msg_hsh{'client_timeout'}) { print STDERR ":$msg_hsh{'client_timeout'}"; } ### careful not to display password information if ($msg_hsh{'server_auth'} and $msg_hsh{'server_auth'} =~ /^\s*(\w+),(.*)/) { print STDERR ":$1,"; for (my $i=0; $i < length($2); $i++) { print STDERR "*"; } } print STDERR ":$msg_hsh{'digest'}\n"; } return 1, \%msg_hsh; } sub SPA_parse_client_version() { my $msg_hr = shift; my $ver = ''; if ($msg_hr->{'remote_version'} =~ /^(\d+\.\d+\.\d+)-pre\d+$/) { ### remote client is a -pre release $ver = $1; } elsif ($msg_hr->{'remote_version'} =~ /^(\d+\.\d+\.\d+)$/) { $ver = $1; } elsif ($msg_hr->{'remote_version'} =~ /^(\d+\.\d+)-pre\d+$/) { ### remote client is a -pre release $ver = $1; } elsif ($msg_hr->{'remote_version'} =~ /^(\d+\.\d+)$/) { $ver = $1; } else { print STDERR localtime() . " [-] Could not determine remote ", "client numeric version." if $debug; return 0; } $ver =~ s|\.||g; $ver =~ s|^0||; $msg_hr->{'numeric_version'} = $ver; print STDERR localtime() . " [+] Remote client numeric version: $ver\n" if $debug; return 1; } sub check_digest() { my ($msg_str, $hr) = @_; ### give priority to FKO module return &fko_check_digest($hr) if $use_fko_module; my $rv = 0; if ($msg_str =~ /(.*):(\S+)/) { my $msg = $1; my $sum = $2; if (length($sum) == $SHA256_DIGEST_LEN) { if ($config{'DIGEST_TYPE'} eq 'ALL' or $config{'DIGEST_TYPE'} =~ /SHA256/) { if ($sum eq sha256_base64($msg)) { $hr->{'digest_str'} = 'SHA256'; $hr->{'digest'} = $sum; $rv = 1; } } } elsif (length($sum) == $SHA1_DIGEST_LEN) { if ($config{'DIGEST_TYPE'} eq 'ALL' or $config{'DIGEST_TYPE'} =~ /SHA1/) { if ($sum eq sha1_base64($msg)) { $hr->{'digest_str'} = 'SHA1'; $hr->{'digest'} = $sum; $rv = 1; } } } elsif (length($sum) == $MD5_DIGEST_LEN) { if ($config{'DIGEST_TYPE'} eq 'ALL' or $config{'DIGEST_TYPE'} =~ /MD5/) { if ($sum eq md5_base64($msg)) { $hr->{'digest_str'} = 'MD5'; $hr->{'digest'} = $sum; $rv = 1; } } } } unless ($rv) { print STDERR localtime() . " [-] Digest alg mis-match.\n" if $debug; } return $rv; } sub fko_check_digest() { my $hr = shift; my $rv = 0; my $digest_type = $fko_incoming_digest_type; print localtime() . " [+] FKO digest type: $digest_type, ", "DIGEST_TYPE var: $config{'DIGEST_TYPE'}\n" if $debug; if ($digest_type == FKO->FKO_DIGEST_SHA256) { if ($config{'DIGEST_TYPE'} eq 'ALL' or $config{'DIGEST_TYPE'} =~ /SHA256/) { $hr->{'digest_str'} = 'SHA256'; $rv = 1; } } elsif ($digest_type == FKO->FKO_DIGEST_SHA1) { if ($config{'DIGEST_TYPE'} eq 'ALL' or $config{'DIGEST_TYPE'} =~ /SHA1/) { $hr->{'digest_str'} = 'SHA1'; $rv = 1; } } elsif ($digest_type == FKO->FKO_DIGEST_MD5) { if ($config{'DIGEST_TYPE'} eq 'ALL' or $config{'DIGEST_TYPE'} =~ /MD5/) { $hr->{'digest_str'} = 'MD5'; $rv = 1; } } else { print STDERR localtime() . " [-] FKO invalid digest type: $digest_type\n" if $debug; } if ($rv) { $hr->{'digest'} = $fko_obj->spa_digest(); } else { print STDERR localtime() . " [-] Digest alg mis-match.\n" if $debug; } return $rv; } sub get_pcap_obj() { my $pcap_t = ''; my $filter = ''; my $err = ''; my $netmask = 0; my $address = 0; if ($config{'AUTH_MODE'} eq 'FILE_PCAP' or $config{'AUTH_MODE'} eq 'ULOG_PCAP') { unless (-e $config{'PCAP_PKT_FILE'}) { &pcap_file_exists_loop(); } unless (-s $config{'PCAP_PKT_FILE'} > 0) { ### required since we cannot use Net::Pcap::open_offline() ### to open a zero-size pcap file. &pcap_nonzero_size_loop(); } print STDERR localtime() . " [+] Acquiring packet data from file: ", "$config{'PCAP_PKT_FILE'}\n" if $debug; $pcap_t = Net::Pcap::open_offline($config{'PCAP_PKT_FILE'}, \$err) or die "[*] Could not open $config{'PCAP_PKT_FILE'}: $err"; ### get past any packets that were from a previous fwknopd ### execution. Net::Pcap::loop($pcap_t, -1, \&null_func, 'fwknop_tag'); } else { if ($config{'ENABLE_PCAP_PROMISC'} eq 'Y') { print STDERR localtime() . " [+] Sniffing (promisc) packet data ", "from interface: $config{'PCAP_INTF'}\n" if $debug; $pcap_t = Net::Pcap::open_live($config{'PCAP_INTF'}, $config{'MAX_SNIFF_BYTES'}, 1, 100, \$err) or die "[*] Could not open $config{'PCAP_INTF'}: $err"; } else { print STDERR localtime() . " [+] Sniffing (non-promisc) packet ", "data from interface: $config{'PCAP_INTF'}\n" if $debug; $pcap_t = Net::Pcap::open_live($config{'PCAP_INTF'}, $config{'MAX_SNIFF_BYTES'}, 0, 100, \$err) or die "[*] Could not open $config{'PCAP_INTF'}: $err"; } } ### apply pcap filter if necessary if ($config{'PCAP_FILTER'} ne 'NONE') { if ($config{'AUTH_MODE'} eq 'PCAP') { if (Net::Pcap::lookupnet($config{'PCAP_INTF'}, \$address, \$netmask, \$err) != 0) { if ($config{'ENABLE_PCAP_PROMISC'} eq 'N') { &logr('[-]', "warning: ENABLE_PCAP_PROMISC is disabled and " . "could not get net information for " . "$config{'PCAP_INTF'}: $err, continuing anyway", $NO_MAIL); } } } ### set the filter on the traffic Net::Pcap::compile($pcap_t, \$filter, $config{'PCAP_FILTER'}, 0, $netmask) && die '[*] Unable to compile packet capture filter'; Net::Pcap::setfilter($pcap_t, $filter) && die '[*] Unable to set packet capture filter'; } return $pcap_t; } sub pcap_file_exists_loop() { while (not -e $config{'PCAP_PKT_FILE'}) { &logr('[-]', "pcap file $config{'PCAP_PKT_FILE'} does not " . "exist, waiting $err_wait_timer seconds for sniffer to " . "create file", $NO_MAIL); sleep $err_wait_timer; } return; } sub pcap_nonzero_size_loop() { while (-s $config{'PCAP_PKT_FILE'} == 0) { &logr('[-]', "zero size pcap file $config{'PCAP_PKT_FILE'}, " . "waiting $err_wait_timer seconds for packet data", $NO_MAIL); sleep $err_wait_timer; } return; } sub exec_command() { my ($cmd, $cmd_alarm) = @_; my $pid; if ($pid = fork()) { local $SIG{'ALRM'} = sub {die "[*] External script timeout.\n"}; ### the external script should be finished within this timeout alarm $cmd_alarm; eval { waitpid($pid, 0); }; alarm 0; if ($@) { kill 9, $pid unless kill 15, $pid; } } else { die "[*] Could not fork for external script: $!" unless defined $pid; ### if we are already redirecting output within the command itself ### then don't redirect again if ($cmd =~ /\s*>\s*/) { exec qq{$cmd}; } else { exec qq{$cmd > /dev/null 2>&1}; } } return; } ### knock server processsing sub process_pkts() { my $fw_pkts_aref = shift; PKT: for my $pkt (@$fw_pkts_aref) { my $src = ''; my $dst = ''; my $len = -1; my $tos = ''; my $ttl = -1; my $id = -1; my $proto = ''; my $sp = -1; my $dp = -1; my $win = -1; my $type = -1; my $code = -1; my $seq = -1; my $flags = ''; my $frag_bit = 0; my $tcp_options = ''; next unless $pkt =~ /kernel.*IN=.*OUT=/; ### May 18 22:21:26 orthanc kernel: DROP IN=eth2 OUT= ### MAC=00:60:1d:23:d0:01:00:60:1d:23:d3:0e:08:00 SRC=192.168.20.25 ### DST=192.168.20.1 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=47300 DF ### PROTO=TCP SPT=34111 DPT=6345 WINDOW=5840 RES=0x00 SYN URGP=0 if ($pkt =~ /SRC=(\S+)\s+DST=(\S+)\s+LEN=(\d+)\s+TOS=(\S+) \s*.*\s+TTL=(\d+)\s+ID=(\d+)\s*.*\s+PROTO=TCP\s+ SPT=(\d+)\s+DPT=(\d+)\s+WINDOW=(\d+)\s+ RES=\S+\s*(.*)\s+URGP=/x) { ($src, $dst, $len, $tos, $ttl, $id, $sp, $dp, $win, $flags) = ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10); if ($pkt =~ /\sRES=\S+\s*(.*)\s+URGP=/) { $flags = $1; } $proto = 'tcp'; unless ($flags !~ /WIN/ && $flags =~ /ACK/ || $flags =~ /SYN/ || $flags =~ /RST/ || $flags =~ /URG/ || $flags =~ /PSH/ || $flags =~ /FIN/ || $flags eq 'NULL') { print STDERR localtime() . " [*] err packet: bad tcp flags.\n" if $debug; next PKT; } $frag_bit = 1 if $pkt =~ /\sDF\s+PROTO/; ### don't pickup IP options if --log-ip-options is used ### (they appear before the PROTO= field). if ($pkt =~ /URGP=\S+\s+OPT\s+\((\S+)\)/) { $tcp_options = $1; } $tcp_ctr++; ### Jul 15 23:32:53 orthanc kernel: DROP IN=eth1 OUT= ### MAC=00:0c:41:24:68:ef:00:0c:41:24:56:37:08:00 SRC=192.168.10.3 ### DST=192.168.10.1 LEN=29 TOS=0x00 PREC=0x00 TTL=64 ID=48500 DF ### PROTO=UDP SPT=32768 DPT=65533 LEN=9 } elsif ($pkt =~ /SRC=(\S+)\s+DST=(\S+)\s+LEN=(\d+)\s+TOS=(\S+)\s+ .*?\sTTL=(\d+)\s+ID=(\d+)\s*.*\sPROTO=UDP\s+ SPT=(\d+)\s+DPT=(\d+)/x) { ($src, $dst, $len, $tos, $ttl, $id, $sp, $dp) = ($1,$2,$3,$4,$5,$6,$7,$8); $proto = 'udp'; ### make sure we have a "reasonable" packet (note that nmap ### can scan port 0 and iptables can report this fact) unless ($src and $dst and $len >= 0 and $tos and $ttl >= 0 and $id >= 0 and $sp >= 0 and $dp >= 0) { next PKT; } $udp_ctr++; } elsif ($pkt =~ /SRC=(\S+)\s+DST=(\S+)\s+LEN=(\d+).* TTL=(\d+).*PROTO=ICMP\s+TYPE=(\d+)\s+ CODE=(\d+)\s+ID=(\d+)\s+SEQ=(\d+)/x) { ($src, $dst, $len, $ttl, $type, $code, $id, $seq) = ($1,$2,$3,$4,$5,$6,$7,$8); $proto = 'icmp'; unless ($src and $dst and $len >= 0 and $ttl >= 0 and $proto and $type >= 0 and $code >= 0 and $id >= 0 and $seq >= 0) { next PKT; } $proto = 'icmp'; $icmp_ctr++; } else { print STDERR localtime() . " [-] no regex match for pkt: $pkt\n" if $debug; } ### check to see if there are any access directives for $src, and ### if not we will do _nothing_ with this IP (unless we are just ### trying to fingerprint it). my $access_nums_aref = &check_src($src) unless $os_fprint_only; unless ($os_fprint_only) { unless ($access_nums_aref) { print STDERR localtime() . " [-] Packet from $src did not ", "match any SOURCE in $config{'ACCESS_CONF'}\n" if $debug; next PKT; } } if ($proto eq 'tcp') { print STDERR localtime() . " [+] $proto $src $sp -> $dst $dp, ", "$flags\n" if $debug; } elsif ($proto eq 'udp') { print STDERR localtime() . " [+] $proto $src $sp -> $dst ", "$dp\n" if $debug; } elsif ($proto eq 'icmp') { print STDERR localtime() . " [+] $proto $src -> $dst\n" if $debug; } ### try to fingerprint the remote OS even though the knock ### sequence is not validated yet. if ($proto eq 'tcp' and $flags =~ /SYN/) { ### must have a SYN pkt if ($tcp_options) { ### hopefully --log-tcp-options is being used ### p0f based fingerprinting &p0f($src, $len, $frag_bit, $ttl, $win, $tcp_options); } } next PKT if $os_fprint_only; my $expecting_decrypt = 0; my $decrypted = 0; NUM: for my $num (@$access_nums_aref) { my $access_hr = $access[$num]; $ip_sequences{$src}{$num} = {} unless defined $ip_sequences{$src}{$num}; my $seq_hr = $ip_sequences{$src}{$num}; ### keep track of which source block we are dealing with from ### access.conf my $source_block_num = $access_hr->{'block_num'}; $seq_hr->{'grant_ctr'} = 0 if not defined $seq_hr->{'grant_ctr'}; ### see if the destination port is part of the correct knock sequence ### for this source my $matched_sequence = 0; if ($access_hr->{'DATA_COLLECT_MODE'} == $ENCRYPT_SEQUENCE) { if ($dp >= $access_hr->{'PORT_OFFSET'} and $dp < $access_hr->{'PORT_OFFSET'} + 256) { ### keep timestamp for when we started tracking the ### encrypted sequence $seq_hr->{'enc_stime'} = time() unless defined $seq_hr->{'enc_stime'}; ### add the destination port to the encrypted sequence push @{$seq_hr->{'enc_ports'}}, $dp; print STDERR localtime() . " [+] Added $dp to encrypted ", "sequence for $src ", "(packet: $#{$seq_hr->{'enc_ports'}})\n" if $debug; } ### see if the encrypted sequence checks out if ($#{$seq_hr->{'enc_ports'}} == $enc_blocksize - 1) { $expecting_decrypt = 1; ### attempt to decrypt the sequence my ($rv, $allow_src, $dec_allow_port, $dec_allow_proto, $username) = &decrypt_sequence($src, $seq_hr, $access_hr); if ($rv) { $decrypted = 1; &logr('[+]', "successful knock decrypt for $src " . "(SOURCE block: $source_block_num)", $SEND_MAIL); ### see if we need to match the OS unless (&matched_os($src, $access_hr)) { delete $ip_sequences{$src}{$num}; next NUM; } ### see if we need to match the username unless (&matched_username($username, $access_hr)) { delete $ip_sequences{$src}{$num}; next NUM; } ### check to see if we have already exceeded the ### maximum number of allowed sequences (this helps ### to prevent replay attacks). if (defined $access_hr->{'KNOCK_LIMIT'}) { if ($seq_hr->{'grant_ctr'} > $access_hr->{'KNOCK_LIMIT'}) { &logr('[-]', "$src exceeded knock limit (set to " . "$access_hr->{'KNOCK_LIMIT'} accesses)", $SEND_MAIL); &logr('[-]', "access controls for $src will " . "not be modified", $SEND_MAIL); delete $ip_sequences{$src}{$num}; next NUM; } } ### all criteria met for encrypted sequence; ### grant access my %open_ports = %{$access_hr->{'OPEN_PORTS'}}; $open_ports{$dec_allow_proto}{$dec_allow_port} = ''; &grant_access($allow_src, {}, {}, $seq_hr, \%open_ports, $access_hr); } delete $ip_sequences{$src}{$num}; next NUM; } } elsif (defined $access_hr->{'SHARED_SEQUENCE'}) { $seq_hr->{'port_seq'} = 0 unless defined $seq_hr->{'port_seq'}; if ($dp == $access_hr->{'SHARED_SEQUENCE'}-> [$seq_hr->{'port_seq'}]->{'port'} and $proto eq $access_hr->{'SHARED_SEQUENCE'}-> [$seq_hr->{'port_seq'}]->{'proto'}) { push @{$seq_hr->{'port_times'}}, time(); ### increment sequence counter (takes into account timing ### requirements). next NUM unless &incr_seq($src, $seq_hr, $access_hr); ### if we made it to the end of the sequence then we have ### a correct knock sequence if ($seq_hr->{'port_seq'} == $#{$access_hr->{'SHARED_SEQUENCE'}}+1) { print STDERR localtime() . " [+] Matched knock ", "sequence for $src\n" if $debug; $matched_sequence = 1; } } else { print STDERR localtime() . " [-] Could not match dst ", "port: $dp at sequence ", "number: $seq_hr->{'port_seq'}\n" if $debug; delete $ip_sequences{$src}{$num}; next NUM; } } ### we matched the knock sequence, so reset for new ### sequence (note we may have other criteria to meet ### before actually granting access). if ($matched_sequence) { delete $seq_hr->{'port_times'}; $seq_hr->{'port_seq'} = 0; &logr('[+]', "port knock access sequence matched for $src " . "(SOURCE block: $source_block_num)", $SEND_MAIL); next NUM unless &matched_os($src, $seq_hr); ### check to see if we have already exceeded the maximum number ### of allowed sequences (this helps to prevent replay attacks). if (defined $access_hr->{'KNOCK_LIMIT'}) { if ($seq_hr->{'grant_ctr'} > $access_hr->{'KNOCK_LIMIT'}) { &logr('[-]', "$src exceeded knock limit (set to " . "$access_hr->{'KNOCK_LIMIT'} accesses)", $SEND_MAIL); &logr('[-]', "access controls for $src will not be " . "modified", $SEND_MAIL); next NUM; } } ### if we made it here then the shared sequence checked out and ### we need to grant access by modifying the iptables ruleset ### (if the ruleset does not already allow $src of course). &grant_access($src, {}, {}, $seq_hr, $access_hr->{'OPEN_PORTS'}, $access_hr); } } if ($expecting_decrypt and not $decrypted) { &logr('[-]', "sequence decrypt failed for $src", $SEND_MAIL); } } ### see if we need to exit if the packet limit (set with -C on the ### command line) has been reached &check_packet_limit(); if ($os_fprint_only) { &print_p0f(); } return; } sub matched_os() { my ($src, $href) = @_; ### see if we require any OS match at all return 1 unless (defined $href->{'REQUIRE_OS'} or defined $href->{'REQUIRE_OS_REGEX'}); unless (defined $p0f{$src}) { ### could not guess the OS if (defined $href->{'REQUIRE_OS'}) { &logr('[-]', "could not fingerprint OS for $src, expecting OS: " . $href->{'REQUIRE_OS'}, $SEND_MAIL); } elsif (defined $href->{'REQUIRE_OS_REGEX'}) { &logr('[-]', "could not fingerprint OS for $src, expecting OS " . "regex: $href->{'REQUIRE_OS_REGEX'}", $SEND_MAIL); } return 0; } if (defined $href->{'REQUIRE_OS'}) { if (defined $p0f{$src}) { my $first_os_key = ''; for my $os (keys %{$p0f{$src}}) { $first_os_key = $os unless $first_os_key; if ($os eq $href->{'REQUIRE_OS'}) { &logr('[+]', "OS guess: $os " . "matched for $src", $SEND_MAIL); return 1; } } ### there may be more than one OS fingerprint, but ### just print one (if we make it here there was no ### match). &logr('[-]', "OS fingerprint mismatch for $src: " . "expected: $href->{'REQUIRE_OS'}, " . "received: $first_os_key", $SEND_MAIL); return 0; } } elsif (defined $href->{'REQUIRE_OS_REGEX'}) { if (defined $p0f{$src}) { my $first_os_key = ''; for my $os (keys %{$p0f{$src}}) { $first_os_key = $os unless $first_os_key; if ($os =~ m|$href->{'REQUIRE_OS_REGEX'}|i) { &logr('[+]', "OS guess: $os " . "regex matched for $src", $SEND_MAIL); return 1; } } ### there may be more than one OS fingerprint, but ### just print one. &logr('[-]', "OS fingerprint regex mismatch for $src: " . "expected: $href->{'REQUIRE_OS_REGEX'}, " . "received: $first_os_key", $SEND_MAIL); return 0; } } return 0; } sub matched_username() { my ($username, $href) = @_; return 1 unless defined $href->{'REQUIRE_USERNAME'}; if ($username) { if ($username eq $href->{'REQUIRE_USERNAME'}) { &logr('[+]', "username $username match", $NO_MAIL); return 1; } else { &logr('[-]', "username mismatch, expected: " . "$href->{'REQUIRE_USERNAME'}, got: $username", $SEND_MAIL); return 0; } } else { &logr('[-]', "missing username in encrypted " . "sequence, expected: $href->{'REQUIRE_USERNAME'}", $SEND_MAIL); return 0; } return 0; } sub check_src() { my $src = shift; my @access_nums = (); if (&is_ip_included($src, $blacklist_ar, $blacklist_exclude_ar)) { print STDERR localtime() . " [+] check_src() ", "$src in BLACKLIST" if $debug; return \@access_nums; } ### now process the SOURCE stanzas for (my $i=0; $i<=$#access; $i++) { my $access_hr = $access[$i]; my $matched_src = 0; if (&is_ip_included($src, $access_hr->{'SOURCE'}, $access_hr->{'exclude_nets'})) { print STDERR localtime() . " [+] Packet from $src matched ", "$access_hr->{'src_str'} (line: ", "$access_hr->{'src_line_num'})\n" if $debug; push @access_nums, $i; } } return \@access_nums; } sub is_base64() { my $data = shift; ### check to make sure the packet data only contains base64 encoded ### characters per RFC 3548: 0-9, A-Z, a-z, +, /, = if ($data =~ /[^\x30-\x39\x41-\x5a\x61-\x7a\x2b\x2f\x3d]/) { return 0; } if ($data =~ /=[^=]/) { return 0; } return 1; } sub is_url_base64() { my $data = shift; ### check to make sure the packet data only contains base64 encoded ### characters per RFC 3548, except that "-" replaces "+", and "_" ### replaces "/": if ($data =~ /[^\x30-\x39\x41-\x5a\x61-\x7a\x2d\x5f\x3d]/) { return 0; } if ($data =~ /=[^=]/) { return 0; } return 1; } sub is_ip_included() { my ($ip, $include_ar, $exclude_ar) = @_; my $is_included = 0; ### check the include criteria for my $net (@$include_ar) { if (ipv4_in_network($net, $ip)) { print STDERR localtime() . " [+] $ip included by $net\n" if $debug; $is_included = 1; last; } } if ($is_included) { ### check the exclude criteria for my $net (@$exclude_ar) { if (ipv4_in_network($net, $ip)) { print STDERR localtime() . " [-] $ip excluded by ! $net\n" if $debug; $is_included = 0; last; } } } return $is_included; } sub incr_seq() { my ($src, $seq_hr, $access_hr) = @_; if (defined $access_hr->{'MIN_TIME_DIFF'}) { ### can check relative timings only after we have more than ### one matching sequence packet if ($seq_hr->{'port_seq'} > 0) { if (defined $access_hr->{'MAX_TIME_DIFF'}) { my $time = time(); if (($time - $seq_hr->{'port_times'}[$seq_hr->{'port_seq'}-1]) > $access_hr->{'MIN_TIME_DIFF'} and ($time - $seq_hr->{'port_times'}[$seq_hr->{'port_seq'}-1]) < $access_hr->{'MAX_TIME_DIFF'}) { print STDERR localtime() . " [+] Sequence min/max time match: ", "($seq_hr->{'port_seq'}) ", "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/", "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n" if $debug; } else { &logr('[-]', 'Sequence min/max_time exceeded: ' . "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/" . "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'} " . "(port sequence num: $seq_hr->{'port_seq'}) ", $SEND_MAIL); $seq_hr->{'port_seq'} = 0; delete $seq_hr->{'port_times'}; return 0; } } else { if ((time() - $seq_hr->{'port_times'}[$seq_hr->{'port_seq'}-1]) > $access_hr->{'MIN_TIME_DIFF'}) { print STDERR localtime() . " [+] Sequence min_time match: ", "($seq_hr->{'port_seq'}) ", "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/", "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n" if $debug; } else { &logr('[-]', "Sequence min_time (" . "$access_hr->{'MIN_TIME_DIFF'} seconds) not met: " . "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/" . "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'} " . "(port sequence num: $seq_hr->{'port_seq'}) ", $SEND_MAIL); delete $seq_hr->{'port_times'}; $seq_hr->{'port_seq'} = 0; return 0; } } } else { print STDERR localtime() . " [+] 1 Sequence match: ", "($seq_hr->{'port_seq'}) ", "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/", "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n" if $debug; } } elsif (defined $access_hr->{'MAX_TIME_DIFF'}) { if ($seq_hr->{'port_seq'} > 0) { if ((time() - $seq_hr->{'port_times'}[$seq_hr->{'port_seq'}-1]) < $access_hr->{'MAX_TIME_DIFF'}) { print STDERR localtime() . " [+] Sequence max_time match: ", "($seq_hr->{'port_seq'}) ", "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/", "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n" if $debug; } else { &logr('[-]', "Sequence max_time ($access_hr->{'MAX_TIME_DIFF'} seconds) exceeded: " . "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/" . "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}" . "(port sequence num: $seq_hr->{'port_seq'}) ", $SEND_MAIL); delete $seq_hr->{'port_times'}; $seq_hr->{'port_seq'} = 0; return 0; } } else { print STDERR localtime() . " [+] Sequence match: ($seq_hr->{'port_seq'}) ", "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/", "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n" if $debug; } } else { print STDERR localtime() . " [+] Sequence match: ($seq_hr->{'port_seq'}) ", "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/", "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n" if $debug; } ### if we made it here, then we met the timing requirements (if required) $seq_hr->{'port_seq'}++; return 1; } sub pcap_GPG_decrypt_msg() { my ($msg, $access_hr) = @_; my @plaintext = (); my $decrypt_rv = 0; my $pid; my $decrypted_msg = ''; my $base64_decoded_msg = ''; my $found_sig = 0; my $gpg_sign_id = ''; unless ($msg =~ /^$access_hr->{'GPG_PREFIX'}/) { if ($access_hr->{'GPG_NO_REQUIRE_PREFIX'}) { print STDERR localtime() . qq| [-] Incoming base64-encoded |, qq|SPA packet is not prefixed with: |, qq|"$access_hr->{'GPG_PREFIX'}"\n| if $debug; } else { print STDERR localtime() . qq| [+] Adding |, qq|"$access_hr->{'GPG_PREFIX'}" prefix to |, "base64-encoded message.\n" if $debug; $msg = $access_hr->{'GPG_PREFIX'} . $msg; } } my ($equals_rv, $equals_padding) = &base64_equals_padding($msg); unless ($equals_rv) { return $decrypt_rv, $decrypted_msg, $gpg_sign_id; } if ($equals_rv and $equals_padding) { print STDERR localtime() . " [+] Padding base64-encoded message ", "with '$equals_padding'.\n" if $debug; $msg .= $equals_padding; } if ($config{'ENABLE_SPA_OVER_HTTP'} eq 'Y' and &is_url_base64($msg)) { $msg =~ s|\-|+|g; $msg =~ s|\_|/|g; } if ($use_fko_module) { my $fko_err = $fko_obj->spa_data($msg); if ($fko_err) { if ($debug) { &logr('[-]', "FKO error setting spa_data(): " . $fko_obj->errstr($fko_err), $NO_MAIL); } return $decrypt_rv, $decrypted_msg, $gpg_sign_id; } ### set the decryption type to use gpg $fko_err = $fko_obj->encryption_type(FKO->FKO_ENCRYPTION_GPG); if ($fko_err) { if ($debug) { &logr('[-]', "FKO error setting decryption type to gpg: " . $fko_obj->errstr($fko_err), $NO_MAIL); return $decrypt_rv, $decrypted_msg, $gpg_sign_id; } } $fko_err = $fko_obj->gpg_home_dir($access_hr->{'GPG_HOME_DIR'}); if ($fko_err) { if ($debug) { &logr('[-]', "FKO error setting gpg home dir: " . $fko_obj->errstr($fko_err), $NO_MAIL); return $decrypt_rv, $decrypted_msg, $gpg_sign_id; } } $fko_err = $fko_obj->gpg_recipient($access_hr->{'GPG_DECRYPT_ID'}); if ($fko_err) { if ($debug) { &logr('[-]', "FKO error setting signing key " . "gpg_signer(): " . $fko_obj->errstr($fko_err), $NO_MAIL); } return $decrypt_rv, $decrypted_msg, $gpg_sign_id; } $fko_err = $fko_obj->decrypt_spa_data($access_hr->{'GPG_DECRYPT_PW'}); if ($fko_err) { if ($debug) { &logr('[-]', "FKO error decrypting data via " . "GnuPG decrypt_spa_data(): " . $fko_obj->errstr($fko_err), $NO_MAIL); } return $decrypt_rv, $decrypted_msg, $gpg_sign_id; } return 1, $decrypted_msg, $gpg_sign_id; } if ($debug) { print STDERR localtime() . " [+] decode_base64() against the ", "following data: $msg\n"; } ### base64 decode the packet $base64_decoded_msg = decode_base64($msg); ### continue only if decode_base64() had no "Premature end of base64 data" ### errors - we want to minimize code that executes against suspicious ### packet data if ($warn_msg =~ /Premature\s+end/i or $warn_msg =~ /Premature\s+padding/i) { if ($debug) { print STDERR localtime() . " [-] $warn_msg"; } return $decrypt_rv, $decrypted_msg, $gpg_sign_id; } print STDERR localtime() . " [+] Attempting GnuPG decrypt...\n" if $debug; if ($debug and $verbose) { print STDERR localtime() . " Decrypting raw data (hex dump):\n"; &hex_dump($base64_decoded_msg); } my $gnupg = GnuPG::Interface->new(); my %gnupg_options = ( 'batch' => 1, 'homedir' => $access_hr->{'GPG_HOME_DIR'}, 'no_options' => 1 ); delete $gnupg_options{'batch'} if ($debug and $verbose and not $test_mode); delete $gnupg_options{'batch'} if $access_hr->{'GPG_USE_OPTIONS'}; $gnupg->options->hash_init(%gnupg_options); if ($access_hr->{'GPG_PATH'}) { $gnupg->call($access_hr->{'GPG_PATH'}); } elsif (defined $cmds{'gpg'}) { $gnupg->call($cmds{'gpg'}); } my $input_fh = IO::Handle->new() or die $!; my $output_fh = IO::Handle->new() or die $!; my $error_fh = IO::Handle->new() or die $!; my $pw_fh = IO::Handle->new() or die $!; my $status_fh = IO::Handle->new() or die $!; my $handles = GnuPG::Handles->new( stdin => $input_fh, stdout => $output_fh, stderr => $error_fh, passphrase => $pw_fh, status => $status_fh, ); $gnupg->options->default_key($access_hr->{'GPG_DECRYPT_ID'}); if (defined $access_hr->{'GPG_AGENT_INFO'}) { $ENV{'GPG_AGENT_INFO'} = $access_hr->{'GPG_AGENT_INFO'}; $pid = $gnupg->decrypt('handles' => $handles, 'command_args' => [ qw( --use-agent ) ]); } elsif ($gpg_agent_info) { ### global definition for gpg-agent connection information ### from the command line $ENV{'GPG_AGENT_INFO'} = $gpg_agent_info; $pid = $gnupg->decrypt('handles' => $handles, 'command_args' => [ qw( --use-agent ) ]); } else { $pid = $gnupg->decrypt('handles' => $handles); } print $pw_fh $access_hr->{'GPG_DECRYPT_PW'}; close $pw_fh; print $input_fh $base64_decoded_msg; close $input_fh; @plaintext = <$output_fh>; close $output_fh; my @errors = <$error_fh>; close $error_fh; my @status = <$status_fh>; close $status_fh; waitpid $pid, 0; if ($debug) { print STDERR localtime() . " [+] GnuPG status messages:\n"; print STDERR for @status; } ### we require the message to be signed; make sure ### the signature is good KEY: for my $key_id (@{$access_hr->{'GPG_REMOTE_ID'}}) { $key_id = $1 if $key_id =~ /^0x(\w+)/; my $found_candidate_sig = 0; if ($debug) { print STDERR localtime() . " [+] gpg key ID: $key_id\n", localtime() . " GnuPG error messages:\n"; } LINE: for my $err (@errors) { print STDERR localtime() . " $err" if $debug; if ($key_id eq 'ANY') { if ($err =~ /Good\s+signature/i) { $found_sig = 1; $gpg_sign_id = $key_id; last KEY; } } else { if ($err =~ /Signature\s+made.*ID\s+$key_id$/) { $found_candidate_sig = 1; next LINE; } if ($found_candidate_sig and $err =~ /Good\s+signature/i) { $found_sig = 1; $gpg_sign_id = $key_id; last KEY; } } } } if ($found_sig and @plaintext) { $decrypt_rv = 1; $decrypted_msg .= $_ for @plaintext; } else { print STDERR localtime() . " [-] GnuPG message not signed by any ", "required key ID.\n" if $debug; } return $decrypt_rv, $decrypted_msg, $gpg_sign_id; } sub pcap_Rijndael_decrypt_msg() { my ($msg, $enc_key) = @_; my $decrypted_msg = ''; my $decrypt_rv = 0; my $base64_decoded_msg = ''; unless ($msg =~ /^U2FsdGVkX1/) { if ($debug) { print STDERR localtime() . " [+] Adding encoded 'Salted__' ", "prefix (U2FsdGVkX1) to incoming encoded SPA packet.\n"; } $msg = 'U2FsdGVkX1' . $msg; } my ($equals_rv, $equals_padding) = &base64_equals_padding($msg); unless ($equals_rv) { return $decrypt_rv, $decrypted_msg; } if ($use_fko_module) { if ($config{'ENABLE_SPA_OVER_HTTP'} eq 'Y' and &is_url_base64($msg)) { $msg =~ s|\-|+|g; $msg =~ s|\_|/|g; } my $fko_err = $fko_obj->spa_data($msg); if ($fko_err) { if ($debug) { &logr('[-]', "FKO error setting spa_data(): " . $fko_obj->errstr($fko_err), $NO_MAIL); } return $decrypt_rv, $decrypted_msg; } $fko_err = $fko_obj->decrypt_spa_data($enc_key); if ($fko_err) { if ($debug) { &logr('[-]', "FKO error decrypting data via " . "Rijndael decrypt_spa_data(): " . $fko_obj->errstr($fko_err), $NO_MAIL); } return $decrypt_rv, $decrypted_msg; } return 1, $decrypted_msg; } if ($equals_padding) { print STDERR localtime() . " [+] Padding base64-encoded message ", "with '$equals_padding'.\n" if $debug; $msg .= $equals_padding; } if ($config{'ENABLE_SPA_OVER_HTTP'} eq 'Y' and &is_url_base64($msg)) { $msg =~ s|\-|+|g; $msg =~ s|\_|/|g; } if ($debug) { print STDERR localtime() . " [+] decode_base64() against the ", "following data: $msg\n"; } ### base64 decode the packet $base64_decoded_msg = decode_base64($msg); ### continue only if decode_base64() had no "Premature end of base64 data" ### errors - we want to minimize code that executes against suspicious ### packet data if ($warn_msg =~ /Premature\s+end/i or $warn_msg =~ /Premature\s+padding/i) { if ($debug) { print STDERR localtime() . " [-] $warn_msg"; } return $decrypt_rv, $decrypted_msg; } ### look for the Salted__ prefix unless ($base64_decoded_msg =~ /^Salted__/) { if ($debug) { print STDERR localtime() . " [-] base64-decoded data does ", "not begin with 'Salted__'\n"; } return $decrypt_rv, $decrypted_msg; } print STDERR localtime() . " [+] Attempting Rijndael decrypt...\n" if $debug; if ($debug and $verbose) { print STDERR localtime() . " Decrypting raw data (hex dump):\n"; &hex_dump($base64_decoded_msg); } my $cipher = Crypt::CBC->new({ 'key' => $enc_key, 'cipher' => $enc_alg, }); eval { $decrypted_msg = $cipher->decrypt($base64_decoded_msg); }; if ($debug and $verbose) { print STDERR " Salt:\n"; &hex_dump($cipher->salt()); print STDERR " Key:\n"; &hex_dump($cipher->key()); print STDERR " IV:\n"; &hex_dump($cipher->iv()); print STDERR " PassPhrase:\n"; &hex_dump($cipher->passphrase()); print STDERR " Block Size: " . $cipher->blocksize() ."\n", " Key Size: " . $cipher->keysize(). "\n\n"; } if ($@) { $decrypted_msg = ''; } else { $decrypt_rv = 1; } return $decrypt_rv, $decrypted_msg; } sub decrypt_sequence() { my ($src, $seq_hr, $access_hr) = @_; my $cipher_txt = ''; my $allow_src = ''; $cipher_txt .= chr($_ - $access_hr->{'PORT_OFFSET'}) for @{$seq_hr->{'enc_ports'}}; return 0 unless $cipher_txt; if ($debug) { my @tmp_chars = split //, $cipher_txt; print STDERR localtime() . ' [+] Cipher text (' . length($cipher_txt) . ' bytes): '; print STDERR ord($_) . ' ' for @tmp_chars; print STDERR "\n"; } my $cipher = Crypt::CBC->new({ 'key' => $access_hr->{'KEY'}, 'cipher' => $enc_alg, }); ### we now have our encrypted string, so try to decrypt it my $plain_txt = ''; eval { $plain_txt = $cipher->decrypt($cipher_txt); }; undef $cipher; return 0,0,0,0 if ($@ or not $plain_txt); if ($debug) { my @tmp_chars = split //, $plain_txt; print STDERR localtime() . " [+] Plain text: "; print STDERR ord($_) . ' ' for @tmp_chars; print STDERR "\n"; } my @chars = split //, $plain_txt; ### the first four characters in the @chars array represent the ### four octets of the IP we are going to modify access for for my $octet ($chars[0], $chars[1], $chars[2], $chars[3]) { unless (0 <= ord($octet) and ord($octet) < 256) { &logr('[-]', "invalid IP octet: " . ord($octet), $SEND_MAIL); return 0,0,0,0; } $allow_src .= ord($octet) . '.'; } $allow_src =~ s/\.$//; if ($allow_src eq '0.0.0.0') { ### the client sent 0.0.0.0 across, so it may be behind a ### NAT device (or the person just doesn't know their source ### address) so open the firewall for the source of the ### encrypted sequence. if ($config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y' or not &is_ip_included($allow_src, $access_hr->{'REQUIRE_SOURCE_ADDRESS'}, $access_hr->{'require_src_addr_exceptions'})) { ### we require the source address to be contained within ### the encrypted packet. return 0,0,0,0; } $allow_src = $src; } my $port_upper_bits = ord($chars[4]) << 8; my $port_lower_bits = ord($chars[5]); my $allow_port = $port_upper_bits | $port_lower_bits; unless (0 <= $allow_port and $allow_port < 65536) { &logr('[-]', "bad port number: $allow_port", $SEND_MAIL); return 0,0,0,0; } my $allow_proto = ''; my $proto = ord($chars[6]); if ($proto == 6) { $allow_proto = 'tcp'; } elsif ($proto == 17) { $allow_proto = 'udp'; } elsif ($proto == 1) { $allow_proto = 'icmp'; } else { &logr('[-]', "bad protocol number: $proto", $SEND_MAIL); return 0,0,0,0; } my $checksum_data = ord($chars[7]); my $checksum = 0; for (my $i=0; $i < 7; $i++) { $checksum += ord($chars[$i]); } $checksum = $checksum % 256; unless ($checksum_data == $checksum) { &logr('[-]', "invalid checksum for $src", $SEND_MAIL); return 0,0,0,0; } my $username = ''; my $i=8; while ($i <= $#chars and ord($chars[$i]) != 0) { $username .= $chars[$i]; $i++; } return 1, $allow_src, $allow_port, $allow_proto, $username; } sub grant_access() { my ($src, $msg_hr, $nat_info_hr, $seq_hr, $open_ports_hr, $access_hr) = @_; if ($access_hr->{'EXTERNAL_CMD_OPEN'} or ($config{'FIREWALL_TYPE'} eq 'external_cmd' and $config{'EXTERNAL_CMD_OPEN'})) { ### run EXTERNAL_CMD_OPEN and let knoptm run EXTERNAL_CMD_CLOSE &external_cmd_open($src, $msg_hr, $open_ports_hr, $access_hr); } else { if ($config{'FIREWALL_TYPE'} eq 'iptables') { ### iptables access; the destination IP is only used if access is ### forwarded through the iptables policy &grant_ipt_access($src, $msg_hr, $nat_info_hr, $seq_hr, $open_ports_hr, $access_hr); } elsif ($config{'FIREWALL_TYPE'} eq 'ipfw') { ### ipfw access &grant_ipfw_access($src, $open_ports_hr, $access_hr); } } return; } sub grant_ipt_access() { my ($src, $msg_hr, $nat_info_hr, $seq_hr, $open_ports_hr, $access_hr) = @_; my @ipt_hrefs = (); my $ipt = &get_iptables_chainmgr_obj($config{'IPT_EXEC_SLEEP'}); my $local_nat = 0; if (keys %$msg_hr) { ### For PK mode, this hash ref is empty if ($msg_hr->{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) { $local_nat = 1; } } if ($access_hr->{'ENABLE_FORWARD_ACCESS'} or $local_nat) { unless (defined $nat_info_hr->{'internal_ip'}) { print STDERR localtime() . " [-] Internal IP not ", "defined for NAT\n" if $debug; undef $ipt; return; } push @ipt_hrefs, \%ipt_prerouting if %ipt_prerouting; push @ipt_hrefs, \%ipt_postrouting if %ipt_postrouting; if ($local_nat) { push @ipt_hrefs, \%ipt_input; print STDERR localtime() . " [+] INPUT NAT access for $src ", "to local IP: $nat_info_hr->{'internal_ip'}\n" if $debug; } else { push @ipt_hrefs, \%ipt_forward; print STDERR localtime() . " [+] FORWARD access for $src ", "to internal IP: $nat_info_hr->{'internal_ip'}\n" if $debug; } } else { if (defined $nat_info_hr->{'internal_ip'}) { undef $ipt; return; } push @ipt_hrefs, \%ipt_input; if ($access_hr->{'ENABLE_OUTPUT_ACCESS'}) { push @ipt_hrefs, \%ipt_output; } } my $ipt_hr_num = 0; for my $hr (@ipt_hrefs) { if ($debug) { $ipt_hr_num++; print STDERR localtime() . " [+] ipt_href: $ipt_hr_num\n", Dumper($hr); } my $nat_ip = '0.0.0.0/0'; my $nat_port = 0; ### add rule for $ip unless it already exists my $target = $hr->{'target'}; my $direction = $hr->{'direction'}; my $table = $hr->{'table'}; my $from_chain = $hr->{'from_chain'}; my $to_chain = $hr->{'to_chain'}; my $jump_rule_position = $hr->{'jump_rule_position'}; my $auto_rule_position = $hr->{'auto_rule_position'}; my $grant_src = $src; my $grant_dst = '0.0.0.0/0'; if ($direction eq 'dst') { ### OUTPUT chain $grant_dst = $src; $grant_src = '0.0.0.0/0'; } my $rv = 0; my $out_ar = []; my $err_ar = []; ### make sure "to_chain" exists for (my $try=0; $try < $config{'IPT_EXEC_TRIES'}; $try++) { ($rv, $out_ar, $err_ar) = $ipt->create_chain($table, $to_chain); last if $rv; } if ($rv) { print STDERR localtime() . " create_chain() returned: $rv\n" if $debug; } else { print STDERR localtime() . " [-] create_chain() ", "returned: $rv, errors:\n" if $debug; &psyslog_errs($err_ar); undef $ipt; return; } ### add jump rule to the "to_chain" from the "from_chain" for (my $try=0; $try < $config{'IPT_EXEC_TRIES'}; $try++) { ($rv, $out_ar, $err_ar) = $ipt->add_jump_rule($table, $from_chain, $jump_rule_position, $to_chain); last if $rv; } if ($rv) { print STDERR localtime() . " add_jump_rule() ", "returned: $rv\n" if $debug; } else { print STDERR localtime() . " [-] add_jump_rule() ", "returned: $rv, errors:\n" if $debug; &psyslog_errs($err_ar); undef $ipt; return; } for my $proto (keys %{$open_ports_hr}) { for my $port (keys %{$open_ports_hr->{$proto}}) { my $num_chain_rules = 0; my $dport = $port; my $sport = 0; my %extended_info = ('protocol' => $proto); if ($direction eq 'dst') { ### OUTPUT chain $extended_info{'s_port'} = $port; $sport = $port; $dport = 0; } else { $extended_info{'d_port'} = $port; } ### deal with DNAT and SNAT (normally MASQUERADE unless ### ENABLE_IPT_SNAT is set) if ($table eq 'nat' and ($target eq 'DNAT' or $target eq 'SNAT')) { if ($target eq 'DNAT') { $extended_info{'to_ip'} = $nat_info_hr->{'internal_ip'}; $extended_info{'to_port'} = $dport; $extended_info{'d_port'} = $nat_info_hr->{'external_port'}; $nat_ip = $nat_info_hr->{'internal_ip'}; $nat_port = $dport; $dport = $nat_info_hr->{'external_port'}; } elsif ($target eq 'SNAT') { $extended_info{'to_ip'} = $config{'SNAT_TRANSLATE_IP'}; $extended_info{'to_port'} = $dport; $extended_info{'d_port'} = $dport; $nat_ip = $config{'SNAT_TRANSLATE_IP'}; $nat_port = $dport; } } ($rv, $num_chain_rules) = $ipt->find_ip_rule($grant_src, $grant_dst, $table, $to_chain, $target, \%extended_info); if ($rv) { print STDERR localtime() . " find_ip_rule() ", "returned $rv\n" if $debug; my $str = "$grant_src -> $grant_dst($proto/$port)"; if ($direction eq 'dst') { $str = "$grant_src($proto/$port) -> $grant_dst"; } if (defined $extended_info{'to_ip'}) { $str = "$grant_src -> $extended_info{'to_ip'}" . "($proto/$extended_info{'d_port'} to " . "$extended_info{'to_port'})"; } &logr('[-]', "source: $str already allowed to connect " . "in chain: $to_chain", $SEND_MAIL); } else { print STDERR localtime() . " find_ip_rule() ", "returned $rv\n" if $debug; my $str = "add $to_chain $grant_src -> " . "$grant_dst($proto/$port) $target rule "; if ($direction eq 'dst') { $str = "add $to_chain $grant_src($proto/$port) -> " . "$grant_dst $target rule "; } if (defined $extended_info{'to_ip'}) { $str = "add $to_chain $grant_src -> " . "$extended_info{'to_ip'}" . "($proto/$extended_info{'d_port'} to " . "$extended_info{'to_port'}) " . "$target rule "; } $str .= "$access_hr->{'FW_ACCESS_TIMEOUT'} sec"; &logr('[+]', $str, $SEND_MAIL); for (my $try=0; $try < $config{'IPT_EXEC_TRIES'}; $try++) { ($rv, $out_ar, $err_ar) = $ipt->add_ip_rule($grant_src, $grant_dst, $auto_rule_position, $table, $to_chain, $target, \%extended_info); last if $rv; } if ($rv) { if ($debug) { print STDERR localtime() . " [+] add_ip_rule() ", "returned $rv\n", " [+] Dumping $to_chain to ", "see newly added rule:\n"; $ipt->run_ipt_cmd("$cmds{'iptables'} -t " . "$table -v -n -L $to_chain"); } ### keep track of how many times we have granted access $seq_hr->{'grant_ctr'}++ unless $access_hr->{'DATA_COLLECT_MODE'} == $PCAP or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP; ### Communicate the new firewall rule to knoptm so ### that it can be removed. &write_knoptm_fw_cache_entry( time(), $access_hr->{'FW_ACCESS_TIMEOUT'}, $grant_src, $sport, $grant_dst, $dport, $proto, $table, $to_chain, $target, $direction, $nat_ip, $nat_port, encode_base64('NA', ''), 0 ); } else { print STDERR localtime() . " [-] add_ip_rule() ", "returned $rv\n" if $debug; &psyslog_errs($err_ar); } } } } } $seq_hr->{'port_seq'} = 0 unless $access_hr->{'DATA_COLLECT_MODE'} == $PCAP or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP; undef $ipt; return; } sub grant_ipfw_access() { my ($src, $open_ports_hr, $access_hr) = @_; my $dst = '0.0.0.0/0'; for my $proto (keys %{$open_ports_hr}) { for my $port (keys %{$open_ports_hr->{$proto}}) { my ($active_rulenum, $set_num, $new_rulenum) = &ipfw_find_ip_rule($src, 'any', $proto, $port); if ($active_rulenum and $set_num == 0) { &logr('[-]', "source: $src already allowed " . "to connect to $proto/$port", $SEND_MAIL); } else { my $msg = ''; if ($active_rulenum and $ipfw_is_dynamic and $set_num == $config{'IPFW_SET_NUM'}) { $msg = 'reactivating ipfw allow rule for ' } else { $msg = 'adding ipfw allow rule for ' } $msg .= "$src -> $proto"; $msg .= "/$port" if $proto ne 'icmp'; $msg .= " ($access_hr->{'FW_ACCESS_TIMEOUT'} " . "seconds)"; &logr('[+]', $msg, $SEND_MAIL); my $res = 0; if ($active_rulenum and $ipfw_is_dynamic and $set_num == $config{'IPFW_SET_NUM'}) { $res = &ipfw_move_rule($active_rulenum, 0); } else { $res = &ipfw_add_ip_rule($new_rulenum, $src, 'any', $proto, $port); } if ($res) { ### communicate the new rule to knoptm so that it can ### be removed. &write_knoptm_fw_cache_entry( time(), $access_hr->{'FW_ACCESS_TIMEOUT'}, $src, 0, $dst, $port, $proto, 'NA', 'NA', 'NA', 'NA', '0.0.0.0/0', 0, encode_base64('NA', ''), 0 ); } } } } return; } sub ipt_check_stateful_rule() { my $ipt = &get_iptables_chainmgr_obj($ZERO_SLEEP); print STDERR localtime() . " [+] Checking for iptables state ", "tracking rule...\n" if $debug; ### check for at least one state tracking rule in _some_ chain my ($rv, $out_ar, $err_ar) = $ipt->run_ipt_cmd( "$cmds{'iptables'} -v -n -L"); my $found_state_rule = 0; for my $rule (@$out_ar) { ### ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED if ($rule =~ /\sACCEPT\s+.*ESTABLISHED/) { $found_state_rule = 1; last; } } unless ($found_state_rule) { &logr('[-]', "warning, could not find any iptables state tracking " . "rules", $SEND_MAIL); } undef $ipt; return; } sub ipfw_check_stateful_rule() { my $cmd = "$cmds{'ipfw'} -dS list"; open LIST, "$cmd |" or die "[*] Could not execute $cmd: $!"; my $found_state_rule = 0; while () { if (/check-state/) { ### from the ipfw man page: # check-state # Checks the packet against the dynamic ruleset. If a match is # found, execute the action associated with the rule which gener- # ated this dynamic rule, otherwise move to the next rule. # Check-state rules do not have a body. If no check-state rule is # found, the dynamic ruleset is checked at the first keep-state or # limit rule. $found_state_rule = 1; $ipfw_is_dynamic = 1; last; } elsif (/keep-state/) { ### from the ipfw man page: # keep-state # Upon a match, the firewall will create a dynamic rule, whose # default behaviour is to match bidirectional traffic between # source and destination IP/port using the same protocol. The rule # has a limited lifetime (controlled by a set of sysctl(8) vari- # ables), and the lifetime is refreshed every time a matching # packet is found. $found_state_rule = 1; $ipfw_is_dynamic = 1; last; } elsif (/allow.*to\s+any\s+established/) { $found_state_rule = 1; last; } } close LIST; unless ($found_state_rule) { &logr('[-]', "warning, could not find ipfw state tracking rules", $SEND_MAIL); } return; } sub ipfw_del_ip() { my @del_rule_nums = (); print "[+] Deleting allow rules for src $fw_del_ip...\n"; my $cmd = "$cmds{'ipfw'} -S list"; open LIST, "$cmd |" or die "[*] Could not execute $cmd: $!"; while () { ### 00003 set 0 allow tcp from 127.0.0.2 to any dst-port 22 keep-state ### 00002 allow tcp from 1.1.1.1 to any dst-port 22 keep-state if (/^\s*(\d+)\s+set\s+\d+\s+allow\s+\S+\s+from\s+$fw_del_ip\s+to\s+ any\s+/x) { push @del_rule_nums, $1; } } close LIST; ### delete all rules that have the IP as a source to any destination for my $rulenum (@del_rule_nums) { my $cmd = "$cmds{'ipfw'} delete $rulenum"; print " $cmd\n"; open IPFW, "| $cmd" or die "[*] Could not execute $cmd"; close IPFW; } return 0; } sub ipfw_find_ip_rule() { my ($src, $dst, $proto, $port) = @_; my $active_rulenum = 0; my $set_num = -1; my $new_rulenum = $config{'IPFW_RULE_NUM'}; ### sets a minimum my %rule_nums = (); my $cmd = "$cmds{'ipfw'} -S list"; open LIST, "$cmd |" or die "[*] Could not execute $cmd: $!"; while () { if ($proto eq 'tcp') { ### 00002 set 2 allow tcp from 1.1.1.1 to any dst-port 22 keep-state if (/^\s*(\#\s+DISABLED\s+)?(\d+)\s+set\s+(\d+)\s+allow\s+$proto\s+ from\s+$src\s+to\s+$dst\s+dst-port\s+$port\s+ keep-state/x) { $active_rulenum = $2; $set_num = $3; } } elsif ($proto eq 'udp') { ### 00002 set 2 allow udp from 1.1.1.1 to any dst-port 53 if (/^\s*(\#\s+DISABLED\s+)?(\d+)\s+set\s+(\d+)\s+allow\s+$proto\s+ from\s+$src\s+to\s+$dst\s+dst-port\s+$port\s+keep-state/x) { $active_rulenum = $2; $set_num = $3; } } else { ### icmp if (/^\s*(\#\s+DISABLED\s+)?(\d+)\s+set\s+(\d+)\s+allow\s+$proto\s+ from\s+$src\s+to\s+$dst/x) { $active_rulenum = $2; $set_num = $3; } } if (/^\s*(\d+)/) { my $rulenum = $1; ### remove any leading zeros from the rule number $rulenum =~ s/^0{1,4}//g; $rule_nums{$rulenum} = ''; } } close LIST; if ($active_rulenum) { ### remove any leading zeros from the rule number $active_rulenum =~ s/^0{1,4}//g; } ### find the next unused rule number $new_rulenum++ while (defined $rule_nums{$new_rulenum}); return $active_rulenum, $set_num, $new_rulenum; } sub ipfw_add_ip_rule() { my ($new_rulenum, $src, $dst, $proto, $port) = @_; my $cmd = "$cmds{'ipfw'} add $new_rulenum " . "allow $proto from $src to $dst"; if ($proto eq 'tcp' or $proto eq 'udp') { $cmd .= " $port"; ### only add keep-state if ipfw already uses dynamic rules $cmd .= ' keep-state' if $ipfw_is_dynamic; } if ($debug) { print STDERR "[+] ipfw_add_ip_rule() $new_rulenum ", "$src $dst $proto $port\n BEFORE:\n"; &ipfw_policy_print($STDERR); } open IPFW, "| $cmd" or die "[*] Could not execute $cmd: $!"; close IPFW; if ($debug) { print STDERR " AFTER:\n"; &ipfw_policy_print($STDERR); } return 1; } sub ipfw_delete_ip_rule() { my $rulenum = shift; if ($debug) { print STDERR "[+] ipfw_delete_ip_rule() rulenum: $rulenum\n", " BEFORE:\n"; &ipfw_policy_print($STDERR); } open IPFW, "| $cmds{'ipfw'} delete $rulenum" or die "[*] Could not ", "execute $cmds{'ipfw'} delete $rulenum"; close IPFW; if ($debug) { print STDERR " AFTER:\n"; &ipfw_policy_print($STDERR); } return 1; } sub ipfw_disable_set() { my $setnum = shift; if ($debug) { print STDERR "[+] ipfw_disable_set() set num: $setnum\n", " BEFORE:\n"; &ipfw_policy_print($STDERR); } open IPFW, "| $cmds{'ipfw'} set disable $setnum" or die "[*] Could ", "not execute $cmds{'ipfw'} set disable $setnum"; close IPFW; if ($debug) { print STDERR " AFTER:\n"; &ipfw_policy_print($STDERR); } return 1; } sub ipfw_move_rule() { my ($rulenum, $setnum) = @_; if ($debug) { print STDERR "[+] ipfw_move_rule() rulenum: $rulenum, set: $setnum\n", " BEFORE:\n"; &ipfw_policy_print($STDERR); } my $cmd = "$cmds{'ipfw'} set move rule $rulenum to $setnum"; open IPFW, "| $cmd" or die "[*] Could not execute $cmd: $!"; close IPFW; if ($debug) { print STDERR " AFTER:\n"; &ipfw_policy_print($STDERR); } return 1; } sub write_knoptm_fw_cache_entry() { my ($rule_timestamp, $timeout, $src, $sport, $dst, $dport, $proto, $table, $chain, $target, $direction, $nat_ip, $nat_port, $external_cmd_close, $external_cmd_alarm) = @_; ### the rule is permanent per the zero value for FW_ACCESS_TIMEOUT in ### this source block return if $timeout == 0; my $knoptm_cache_entry_line = "$rule_timestamp $timeout $src $sport " . "$dst $dport $proto $table $chain $target $direction $nat_ip " . "$nat_port $external_cmd_close $external_cmd_alarm"; print STDERR localtime() . " [+] Writing fw time cache entry to: ", "$config{'KNOPTM_IP_TIMEOUT_SOCK'} $knoptm_cache_entry_line\n" if $debug; ### open domain socket with running knoptm process my $sock = IO::Socket::UNIX->new($config{'KNOPTM_IP_TIMEOUT_SOCK'}) or die "[*] Could not acquire $config{'KNOPTM_IP_TIMEOUT_SOCK'} ", "socket: $!"; print $sock "$knoptm_cache_entry_line\n"; close $sock; return; } sub timeout_invalid_sequences() { for my $src (keys %ip_sequences) { for my $seq_num (keys %{$ip_sequences{$src}}) { my $knock_interval = $access[$seq_num]{'KNOCK_INTERVAL'}; if (defined $access[$seq_num]{'KNOCK_LIMIT'}) { if (defined $ip_sequences{$src}{$seq_num}{'grant_ctr'} and $ip_sequences{$src}{$seq_num}{'grant_ctr'} > $access[$seq_num]{'KNOCK_LIMIT'}) { ### don't timeout knock sequence if the knock limit ### has been exceeded next; } } ### encrypted sequences if (defined $ip_sequences{$src}{$seq_num}{'enc_stime'}) { if (time() - $ip_sequences{$src}{$seq_num}{'enc_stime'} > $knock_interval) { &logr('[+]', "invalid encrypted sequence $src timeout", $NO_MAIL); delete $ip_sequences{$src}{$seq_num}; next; } } ### shared sequences if (defined $ip_sequences{$src}{$seq_num}{'port_stime'}) { if (time() - $ip_sequences{$src} {$seq_num}{'port_stime'}->[0] > $knock_interval) { &logr('[+]', "invalid shared sequence $src timeout", $NO_MAIL); delete $ip_sequences{$src}{$seq_num}; next; } } } } return; } sub p0f() { my ($src, $len, $frag_bit, $ttl, $win, $tcp_options) = @_; print STDERR localtime() . " [+] p0f(): $src len: $len, frag_bit: ", "$frag_bit, ttl: $ttl, win: $win\n" if $debug; my ($options_aref) = &parse_tcp_options($src, $tcp_options); return unless $options_aref; ### try to match SYN packet length LEN: for my $sig_len (keys %p0f_sigs) { my $matched_len = 0; if ($sig_len eq '*') { ### len can be wildcarded in pf.os $matched_len = 1; } elsif ($sig_len =~ /^\%(\d+)/) { if (($len % $1) == 0) { $matched_len = 1; } } elsif ($len == $sig_len) { $matched_len = 1; } next LEN unless $matched_len; ### try to match fragmentation bit FRAG: for my $test_frag_bit ($frag_bit, '*') { ### don't need "%nnn" check next FRAG unless defined $p0f_sigs{$sig_len}{$test_frag_bit}; ### find out for which p0f sigs the TTL is within range TTL: for my $sig_ttl (keys %{$p0f_sigs{$sig_len}{$test_frag_bit}}) { unless ($ttl > $sig_ttl - $config{'MAX_HOPS'} and $ttl <= $sig_ttl) { next TTL; } ### match tcp window size WIN: for my $sig_win_size (keys %{$p0f_sigs{$sig_len}{$test_frag_bit}{$sig_ttl}}) { my $matched_win_size = 0; if ($sig_win_size eq '*') { $matched_win_size = 1; } elsif ($sig_win_size =~ /^\%(\d+)/) { if (($win % $1) == 0) { $matched_win_size = 1; } } elsif ($sig_win_size =~ /^S(\d+)/) { ### window size must be a multiple of maximum ### seqment size my $multiple = $1; for my $opt_hr (@$options_aref) { if (defined $opt_hr->{$tcp_p0f_opt_types{'M'}}) { my $mss_val = $opt_hr->{$tcp_p0f_opt_types{'M'}}; if ($win == $mss_val * $multiple) { $matched_win_size = 1; } } last; } } elsif ($sig_win_size == $win) { $matched_win_size = 1; } next WIN unless $matched_win_size; TCPOPTS: for my $sig_opts (keys %{$p0f_sigs{$sig_len} {$test_frag_bit}{$sig_ttl}{$sig_win_size}}) { my @sig_opts = split /\,/, $sig_opts; for (my $i=0; $i<=$#sig_opts; $i++) { ### tcp option order is important. Check to see if ### the option order in the packet matches the order we ### expect to see in the signature if ($sig_opts[$i] =~ /^([NMWST])/) { my $sig_letter = $1; unless (defined $options_aref->[$i]-> {$tcp_p0f_opt_types{$sig_letter}}) { next TCPOPTS; ### could not match tcp option order } ### MSS, window scale, and timestamp have ### specific signatures requirements on values if ($sig_letter eq 'M') { if ($sig_opts[$i] =~ /M(\d+)/) { my $sig_mss_val = $1; next TCPOPTS unless $options_aref->[$i]-> {$tcp_p0f_opt_types{$sig_letter}} == $sig_mss_val; } elsif ($sig_opts[$i] =~ /M\%(\d+)/) { my $sig_mss_mod_val = $1; next TCPOPTS unless (($options_aref->[$i]-> {$tcp_p0f_opt_types{$sig_letter}} % $sig_mss_mod_val) == 0); } ### else it is "M*" which always matches } elsif ($sig_letter eq 'W') { if ($sig_opts[$i] =~ /W(\d+)/) { my $sig_win_val = $1; next TCPOPTS unless $options_aref->[$i]-> {$tcp_p0f_opt_types{$sig_letter}} == $sig_win_val; } elsif ($sig_opts[$i] =~ /W\%(\d+)/) { my $sig_win_mod_val = $1; next TCPOPTS unless (($options_aref->[$i]-> {$tcp_p0f_opt_types{$sig_letter}} % $sig_win_mod_val) == 0); } ### else it is "W*" which always matches } elsif ($sig_letter eq 'T') { if ($sig_opts[$i] =~ /T0/) { next TCPOPTS unless $options_aref->[$i]-> {$tcp_p0f_opt_types{$sig_letter}} == 0; } ### else it is just "T" which matches } } } OS: for my $os (keys %{$p0f_sigs{$sig_len} {$test_frag_bit}{$sig_ttl}{$sig_win_size} {$sig_opts}}) { my $sig = $p0f_sigs{$sig_len} {$test_frag_bit}{$sig_ttl}{$sig_win_size} {$sig_opts}{$os}; print STDERR localtime() . " [+] os: $os, $sig\n" if $debug; $p0f{$src}{$os} = $sig; } } } } } } return; } sub parse_tcp_options() { my ($src, $tcp_options) = @_; my @opts = (); my @hex_nums = (); my $debug_str = ''; if (length($tcp_options) % 2 != 0) { ### make sure length a multiple of two &logr('[-]', 'tcp options length not a multiple of two.', $NO_MAIL); return ''; } ### $tcp_options is a hex string like "020405B401010402" from the iptables ### log message my @chars = split //, $tcp_options; for (my $i=0; $i <= $#chars; $i += 2) { my $str = $chars[$i] . $chars[$i+1]; push @hex_nums, $str; } my $max_parse_attempts = $#chars; my $parse_ctr = 0; OPT: for (my $opt_kind=0; $opt_kind <= $#hex_nums;) { $parse_ctr++; return [] if $parse_ctr > $max_parse_attempts; last OPT unless defined $hex_nums[$opt_kind+1]; my $is_nop = 0; my $len = hex($hex_nums[$opt_kind+1]); if (hex($hex_nums[$opt_kind]) == $tcp_nop_type) { $debug_str .= 'NOP, ' if $debug; push @opts, {$tcp_nop_type => ''}; $is_nop = 1; } elsif (hex($hex_nums[$opt_kind]) == $tcp_mss_type) { ### MSS my $mss_hex = ''; for (my $i=$opt_kind+2; $i < ($opt_kind+$len); $i++) { $mss_hex .= $hex_nums[$i]; } my $mss = hex($mss_hex); push @opts, {$tcp_mss_type => $mss}; $debug_str .= 'MSS: ' . hex($mss_hex) . ', ' if $debug; } elsif (hex($hex_nums[$opt_kind]) == $tcp_win_scale_type) { my $window_scale_hex = ''; for (my $i=$opt_kind+2; $i < ($opt_kind+$len); $i++) { $window_scale_hex .= $hex_nums[$i]; } my $win_scale = hex($window_scale_hex); push @opts, {$tcp_win_scale_type => $win_scale}; $debug_str .= 'Win Scale: ' . hex($window_scale_hex) . ', ' if $debug; } elsif (hex($hex_nums[$opt_kind]) == $tcp_sack_type) { push @opts, {$tcp_sack_type => ''}; $debug_str .= 'SACK, ' if $debug; } elsif (hex($hex_nums[$opt_kind]) == $tcp_timestamp_type) { my $timestamp_hex = ''; for (my $i=$opt_kind+2; $i < ($opt_kind+$len) - 4; $i++) { $timestamp_hex .= $hex_nums[$i]; } my $timestamp = hex($timestamp_hex); push @opts, {$tcp_timestamp_type => $timestamp}; $debug_str .= 'Timestamp: ' . hex($timestamp_hex) . ', ' if $debug; } elsif (hex($hex_nums[$opt_kind]) == 0) { ### End of option list last OPT; } if ($is_nop) { $opt_kind += 1; } else { if ($len == 0 or $len == 1) { ### this should never happen; it indicates a broken TCP stack ### or maliciously constructed options since the len field is ### large enough to accomodate the TLV encoding my $msg = "broken $len-byte len field within TCP options " . "string: $tcp_options from source IP: $src"; print STDERR " $msg\n" if $debug; &logr('[-]', $msg, $NO_MAIL); return []; } ### get to the next option-kind field $opt_kind += $len; } } if ($debug) { $debug_str =~ s/\,$//; print STDERR localtime() . " [+] $debug_str\n" if $debug; } return \@opts; } sub print_p0f() { for my $src (keys %p0f) { print "[+] $src\n"; for my $os (keys %{$p0f{$src}}) { printf " %-33s%s\n", $p0f{$src}{$os}, $os; } } exit 0; } sub import_p0f_sigs() { my $p0f_file = $config{'P0F_FILE'}; open P, "< $p0f_file" or die '[*] Could not open ', "$p0f_file: $!"; my @lines =

; close P; my $os = ''; for my $line (@lines) { chomp $line; next if $line =~ /^\s*#/; next unless $line =~ /\S/; ### S3:64:1:60:M*,S,T,N,W1: Linux:2.5::Linux 2.5 (sometimes 2.4) ### 16384:64:1:60:M*,N,W0,N,N,T: FreeBSD:4.4::FreeBSD 4.4 ### 16384:64:1:44:M*: FreeBSD:2.0-2.2::FreeBSD 2.0-4.1 if ($line =~ /^(\S+?):(\S+?):(\S+?):(\S+?):(\S+?):\s+(.*)\s*/) { my $win_size = $1; my $ttl = $2; my $frag_bit = $3; my $len = $4; my $options = $5; my $os = $6; my $sig_str = "$win_size:$ttl:$frag_bit:$len:$options"; ### don't know how to handle MTU-based window size yet unless ($win_size =~ /T/) { $p0f_sigs{$len}{$frag_bit}{$ttl}{$win_size}{$options}{$os} = $sig_str; } } } print STDERR Dumper %p0f_sigs if $debug and $verbose; &logr('[+]', 'imported p0f-based passive OS fingerprinting signatures', $NO_MAIL); return; } sub import_access() { open A, "< $config{'ACCESS_CONF'}" or die "[*] Could not open ", "$config{'ACCESS_CONF'}: $!"; my @lines = ; close A; my $src = ''; my $type = ''; my $valid_ctr = 0; my $source_block_num = 0; for (my $i=0; $i<=$#lines; $i++) { my $line = $lines[$i]; chomp $line; next if $line =~ /^\s*#/; next unless $line =~ /\S/; die "[*] No semicolon ending found for line: ", "$line in $config{'ACCESS_CONF'}" unless $line =~ /^.+;/; } for (my $i=0; $i<=$#lines; $i++) { my $line = $lines[$i]; chomp $line; next if $line =~ /^\s*#/; next unless $line =~ /\S/; die "[*] No semicolon ending found for line: ", "$line in $config{'ACCESS_CONF'}" unless $line =~ /^.+;/; my $type = ''; my %access_hsh = (); if ($line =~ /^\s*SOURCE:/) { ### keep track of SOURCE block number; note that this value ### increments whether or not we actually have a valid block ### (so we can keep track of exactly which block within the ### access.conf file). $source_block_num++; $access_hsh{'block_num'} = $source_block_num; my $src_str = ''; if ($line =~ m|^\s*SOURCE:?\s*(.*)\s*;|) { $src_str = $1; ($access_hsh{'SOURCE'}, $access_hsh{'exclude_nets'}) = &parse_nets($src_str); } $i++; $access_hsh{'src_line_num'} = $i; $access_hsh{'src_str'} = $src_str; while (defined $lines[$i] and $lines[$i] !~ /^\s*SOURCE:?/) { my $line = $lines[$i]; $i++; chomp $line; next if $line =~ /^\s*#/; next unless $line =~ /\S/; if ($line =~ /^\s*DATA_COLLECT_MODE:?\s+(\S+);/) { my $mode = $1; if (uc($mode) eq 'PCAP') { $access_hsh{'DATA_COLLECT_MODE'} = $PCAP; } elsif (uc($mode) eq 'FILE_PCAP') { $access_hsh{'DATA_COLLECT_MODE'} = $FILE_PCAP; } elsif (uc($mode) eq 'ULOG_PCAP') { $access_hsh{'DATA_COLLECT_MODE'} = $ULOG_PCAP; } elsif (uc($mode) eq 'ENCRYPT_SEQUENCE') { $access_hsh{'DATA_COLLECT_MODE'} = $ENCRYPT_SEQUENCE; } } elsif ($line =~ /^\s*ENCRYPT_SEQUENCE\s*;/) { $access_hsh{'DATA_COLLECT_MODE'} = $ENCRYPT_SEQUENCE; } elsif ($line =~ /^\s*KEY:?\s*(.*)\s*;/) { unless ($imported_crypt_cbc) { require Crypt::CBC; print STDERR "[+] Crypt::CBC::VERSION ", "$Crypt::CBC::VERSION\n" if $debug; } $imported_crypt_cbc = 1; $access_hsh{'KEY'} = $1; ### pad with zeros to the key size while (length($access_hsh{'KEY'}) < $enc_keysize) { $access_hsh{'KEY'} .= '0'; } } elsif ($line =~ /^\s*GPG_REMOTE_ID:?\s*(.*)\s*;/) { unless ($imported_gpg) { require GnuPG::Interface; print STDERR "[+] GnuPG::Interface::VERSION ", "$GnuPG::Interface::VERSION\n" if $debug; } $imported_gpg = 1; my @arr = split /\s*\,\s*/, $1; for my $gpg_key_id (@arr) { push @{$access_hsh{'GPG_REMOTE_ID'}}, $gpg_key_id; } } elsif ($line =~ /^\s*GPG_DECRYPT_ID:?\s*(.*)\s*;/) { unless ($imported_gpg) { require GnuPG::Interface; print STDERR "[+] GnuPG::Interface::VERSION ", "$GnuPG::Interface::VERSION\n" if $debug; } $imported_gpg = 1; $access_hsh{'GPG_DECRYPT_ID'} = $1; } elsif ($line =~ /^\s*GPG_DECRYPT_PW:?\s*(.*)\s*;/) { unless ($imported_gpg) { require GnuPG::Interface; print STDERR "[+] GnuPG::Interface::VERSION ", "$GnuPG::Interface::VERSION\n" if $debug; } $imported_gpg = 1; $access_hsh{'GPG_DECRYPT_PW'} = $1; } elsif ($line =~ /^\s*GPG_HOME_DIR:?\s*(\S+)\s*;/) { unless ($imported_gpg) { require GnuPG::Interface; print STDERR "[+] GnuPG::Interface::VERSION ", "$GnuPG::Interface::VERSION\n" if $debug; } $imported_gpg = 1; $access_hsh{'GPG_HOME_DIR'} = $1; } elsif ($line =~ /^\s*GPG_NO_OPTIONS:?\s*(\S+);/) { my $val = $1; if ($val =~ /y/i) { $access_hsh{'GPG_NO_OPTIONS'} = 1; } else { $access_hsh{'GPG_NO_OPTIONS'} = 0; } } elsif ($line =~ /^\s*GPG_USE_OPTIONS:?\s*(\S+);/) { my $val = $1; if ($val =~ /y/i) { $access_hsh{'GPG_USE_OPTIONS'} = 1; } else { $access_hsh{'GPG_USE_OPTIONS'} = 0; } } elsif ($line =~ /^\s*GPG_NO_REQUIRE_PREFIX:?\s*(\S+);/) { my $val = $1; if ($val =~ /y/i) { $access_hsh{'GPG_NO_REQUIRE_PREFIX'} = 1; } else { $access_hsh{'GPG_NO_REQUIRE_PREFIX'} = 0; } } elsif ($line =~ /^\s*GPG_PREFIX:?\s*(\S+);/) { $access_hsh{'GPG_PREFIX'} = $1; } elsif ($line =~ /^\s*GPG_PATH:?\s*(\S+);/) { $access_hsh{'GPG_PATH'} = $1; unless (-e $access_hsh{'GPG_PATH'} and -x $access_hsh{'GPG_PATH'}) { die "[*] $access_hsh{'GPG_PATH'} does not exist ", "or could not execute."; } } elsif ($line =~ /^\s*FILE_PCAP\s*;/) { ### used in file pcap mode $access_hsh{'DATA_COLLECT_MODE'} = $FILE_PCAP; } elsif ($line =~ /^\s*ULOG_PCAP\s*;/) { ### used in ulog pcap mode $access_hsh{'DATA_COLLECT_MODE'} = $ULOG_PCAP; } elsif ($line =~ /^\s*PCAP\s*;/) { ### used in pcap mode $access_hsh{'DATA_COLLECT_MODE'} = $PCAP; } elsif ($line =~ /^\s*SHARED_SEQUENCE:?\s*(.*)\s*;/) { $access_hsh{'DATA_COLLECT_MODE'} = $SHARED_SEQUENCE; my $sequence = $1; my @arr = split /\s*\,\s*/, $sequence; for my $port (@arr) { my %hsh = (); if ($port =~ m|tcp/(\d+)|) { %hsh = ('port' => $1, 'proto' => 'tcp'); } elsif ($port =~ m|udp/(\d+)|) { %hsh = ('port' => $1, 'proto' => 'udp'); } elsif ($port =~ m|icmp|) { %hsh = ('port' => -1, 'proto' => 'icmp'); } next unless %hsh; push @{$access_hsh{'SHARED_SEQUENCE'}}, \%hsh; } } elsif ($line =~ /^\s*PORT_OFFSET:?\s*(\d+)\s*;/) { $access_hsh{'PORT_OFFSET'} = $1; } elsif ($line =~ /^\s*OPEN_PORTS:?\s*(.*)\s*;/) { my $open_ports = $1; my @arr = split /\s*\,\s*/, $open_ports; for my $port (@arr) { if ($port =~ m|tcp/(\d+)|i) { $access_hsh{'OPEN_PORTS'}{'tcp'}{$1} = ''; } elsif ($port =~ m|udp/(\d+)|i) { $access_hsh{'OPEN_PORTS'}{'udp'}{$1} = ''; } elsif ($port =~ m|icmp|i) { $access_hsh{'OPEN_PORTS'}{'icmp'}{0} = ''; } } } elsif ($line =~ /^\s*ENABLE_FORWARD_ACCESS\s*;/) { $access_hsh{'ENABLE_FORWARD_ACCESS'} = 1; } elsif ($line =~ /^\s*ENABLE_FORWARD_ACCESS:?\s*(\S+);/) { my $val = $1; if ($val =~ /y/i) { $access_hsh{'ENABLE_FORWARD_ACCESS'} = 1; } else { $access_hsh{'ENABLE_FORWARD_ACCESS'} = 0; } } elsif ($line =~ /^\s*ENABLE_OUTPUT_ACCESS\s*;/) { $access_hsh{'ENABLE_OUTPUT_ACCESS'} = 1; } elsif ($line =~ /^\s*ENABLE_OUTPUT_ACCESS:?\s*(\S+);/) { my $val = $1; if ($val =~ /y/i) { $access_hsh{'ENABLE_OUTPUT_ACCESS'} = 1; } else { $access_hsh{'ENABLE_OUTPUT_ACCESS'} = 0; } } elsif ($line =~ /^\s*ENABLE_EXTERNAL_CMDS:?\s*(\S+);/) { my $val = $1; if ($val =~ /y/i) { $access_hsh{'ENABLE_EXTERNAL_CMDS'} = 1; } else { $access_hsh{'ENABLE_EXTERNAL_CMDS'} = 0; } } elsif ($line =~ /^\s*EXTERNAL_CMD_OPEN:?\s*(.*);/) { $access_hsh{'EXTERNAL_CMD_OPEN'} = $1; $access_hsh{'ENABLE_EXTERNAL_CMDS'} = 1; } elsif ($line =~ /^\s*EXTERNAL_CMD_CLOSE:?\s*(.*);/) { $access_hsh{'EXTERNAL_CMD_CLOSE'} = $1; $access_hsh{'ENABLE_EXTERNAL_CMDS'} = 1; } elsif ($line =~ /^\s*EXTERNAL_CMD_ALARM:?\s*(\d+);/) { $access_hsh{'EXTERNAL_CMD_ALARM'} = $1; } elsif ($line =~ /^\s*REQUIRE_AUTH_METHOD:?\s*(\S+)\s*;/) { $access_hsh{'REQUIRE_AUTH_METHOD'} = lc($1); } elsif ($line =~ /^\s*SHADOW_FILE:?\s*(\S+)\s*;/) { $access_hsh{'SHADOW_FILE'} = $1; } elsif ($line =~ /^\s*KNOCK_INTERVAL:?\s*(\d+)\s*;/) { $access_hsh{'KNOCK_INTERVAL'} = $1; } elsif ($line =~ /^\s*KNOCK_LIMIT:?\s*(\d+)\s*;/) { $access_hsh{'KNOCK_LIMIT'} = $1; } elsif ($line =~ /^\s*PERMIT_CLIENT_PORTS\s*;/) { $access_hsh{'PERMIT_CLIENT_PORTS'} = 1; } elsif ($line =~ /^\s*PERMIT_CLIENT_PORTS:?\s*(\S+);/) { my $val = $1; if ($val =~ /y/i) { $access_hsh{'PERMIT_CLIENT_PORTS'} = 1; } else { $access_hsh{'PERMIT_CLIENT_PORTS'} = 0; } } elsif ($line =~ /^\s*PERMIT_CLIENT_TIMEOUT\s*;/) { $access_hsh{'PERMIT_CLIENT_TIMEOUT'} = 1; } elsif ($line =~ /^\s*PERMIT_CLIENT_TIMEOUT:?\s*(\S+);/) { my $val = $1; if ($val =~ /y/i) { $access_hsh{'PERMIT_CLIENT_TIMEOUT'} = 1; } else { $access_hsh{'PERMIT_CLIENT_TIMEOUT'} = 0; } } elsif ($line =~ /^\s*ENABLE_CMD_EXEC\s*;/) { $access_hsh{'ENABLE_CMD_EXEC'} = 1; } elsif ($line =~ /^\s*ENABLE_CMD_EXEC:?\s*(\S+);/) { my $val = $1; if ($val =~ /y/i) { $access_hsh{'ENABLE_CMD_EXEC'} = 1; } else { $access_hsh{'ENABLE_CMD_EXEC'} = 0; } } elsif ($line =~ /^\s*DISABLE_FW_ACCESS\s*;/) { $access_hsh{'DISABLE_FW_ACCESS'} = 1; } elsif ($line =~ /^\s*DISABLE_FW_ACCESS:?\s*(\S+);/) { my $val = $1; if ($val =~ /y/i) { $access_hsh{'DISABLE_FW_ACCESS'} = 1; } else { $access_hsh{'DISABLE_FW_ACCESS'} = 0; } } elsif ($line =~ /^\s*REQUIRE_SOURCE_ADDRESS:?\s*(.*)\s*;/) { my $str = $1; if ($str =~ /y/i) { $str = ''; ### don't allow the client to set 0.0.0.0 } elsif ($str =~ /n/i) { $str = '0.0.0.0'; ### allow the client to set 0.0.0.0 } ### we are setting specific allowed networks for the internal ### allow IP's (i.e. with -a or -R on the client side) ($access_hsh{'REQUIRE_SOURCE_ADDRESS'}, $access_hsh{'require_src_addr_exceptions'}) = &parse_nets($str); } elsif ($line =~ /^\s*INTERNAL_NET_ACCESS:?\s*(.*)\s*;/) { ### for --Forward-access restrictions to internal IP addresses ($access_hsh{'INTERNAL_NET_ACCESS'}, $access_hsh{'internal_net_exceptions'}) = &parse_nets($1); } elsif ($line =~ /^\s*CMD_REGEX:?\s*(.*)\s*;/) { $access_hsh{'CMD_REGEX'} = qr|$1|; } elsif ($line =~ /^\s*FW_ACCESS_TIMEOUT:?\s*(\d+)\s*;/) { $access_hsh{'FW_ACCESS_TIMEOUT'} = $1; } elsif ($line =~ /^\s*MAX_ACCESS_TIMEOUT:?\s*(\d+)\s*;/) { $access_hsh{'FW_ACCESS_TIMEOUT'} = $1; } elsif ($line =~ /^\s*REQUIRE_OS:?\s*(.*)\s*;/) { $access_hsh{'REQUIRE_OS'} = $1; } elsif ($line =~ /^\s*REQUIRE_OS_REGEX:?\s*(.*)\s*;/) { $access_hsh{'REQUIRE_OS_REGEX'} = $1; } elsif ($line =~ /^\s*REQUIRE_USERNAME:?\s*(.*)\s*;/) { $access_hsh{'REQUIRE_USERNAME'} = $1; } elsif ($line =~ /^\s*MIN_TIME_DIFF:?\s*(\d+)\s*;/) { $access_hsh{'MIN_TIME_DIFF'} = $1; } elsif ($line =~ /^\s*MAX_TIME_DIFF:?\s*(\d+)\s*;/) { $access_hsh{'MAX_TIME_DIFF'} = $1; } elsif ($line =~ /^\s*RESTRICT_INTF:?\s*(\w+)\s*;/) { $access_hsh{'RESTRICT_INTF'} = $1; } } $i--; } unless (defined $access_hsh{'PERMIT_CLIENT_PORTS'}) { $access_hsh{'PERMIT_CLIENT_PORTS'} = 0; } unless (defined $access_hsh{'PERMIT_CLIENT_TIMEOUT'}) { $access_hsh{'PERMIT_CLIENT_TIMEOUT'} = 0; } if (&validate_src_access_hsh(\%access_hsh)) { push @access, \%access_hsh; $valid_ctr++; } if (defined $access_hsh{'REQUIRE_USERNAME'}) { my @users = split /\s*,\s*/, $access_hsh{'REQUIRE_USERNAME'}; for my $user (@users) { push @{$access_hsh{'VALID_USERS'}}, $user; } } } if ($valid_ctr == 0) { die "[*] No valid SOURCE blocks defined in $config{'ACCESS_CONF'} ", "(review syslog for more info). Exiting."; } &dump_config() if $debug; if ($debug and $verbose) { for (my $i=0; $i<=$#access; $i++) { &dump_access($access[$i], $i); } } &logr('[+]', 'imported access directives ' . "($valid_ctr SOURCE definitions).", $NO_MAIL); return; } sub parse_nets() { my $net_str = shift; my @include_nets = (); my @exclude_nets = (); $net_str =~ s|\!\s+|!|g; for my $str (split /\s*,\s*/, $net_str) { if ($str =~ m|$ip_re/$ip_re| or $str =~ m|$ip_re/\d{1,2}| or $str =~ m|$ip_re| or $str =~ m|any|i) { if ($str =~ /any/i) { if ($str =~ m|!|) { ### ipv4_in_network('0.0.0.0', $someip) always matches push @exclude_nets, '0.0.0.0'; } else { push @include_nets, '0.0.0.0'; } } else { if ($str =~ m|!|) { push @exclude_nets, &ip_info_only($str); } else { push @include_nets, &ip_info_only($str); } } } else { ### allow the string "NONE" unless ($net_str =~ m|none|i) { die qq|[*] Improper "$str" in SOURCE line |, qq|in $config{'ACCESS_CONF'}|; } } } return \@include_nets, \@exclude_nets; } sub ip_info_only() { my $str = shift; my $ip_info = ''; if ($str =~ m|($ip_re/$ip_re)|) { $ip_info = $1; } elsif ($str =~ m|($ip_re/\d{1,2})|) { $ip_info = $1; } elsif ($str =~ m|($ip_re)|) { $ip_info = $1; } die "[*] Could not parse IP information from: $str" unless $ip_info; return $ip_info; } sub dump_config() { my $rv = 0; print STDERR "\n", localtime() . " [+] Dumping config from: $config_file\n"; for my $var (sort keys %config) { my $str = $config{$var}; ### sanitize sensitive information $str = '(removed)' if $var eq 'EMAIL_ADDRESSES'; $str = '(removed)' if $var eq 'HOSTNAME'; $str = '(removed)' if $var eq 'BLACKLIST'; $str = '(removed)' if $var eq 'GPG_DEFAULT_HOME_DIR'; printf STDERR "%-30s %s\n", $var, $str; } print STDERR "\n", localtime() . " [+] Command paths:\n\n"; for my $var (sort keys %cmds) { printf STDERR "%-30s %s\n", $var, $cmds{$var}; } return $rv; } sub dump_access() { my ($access_hr, $num) = @_; print STDERR localtime() . " SOURCE block: $num\n"; for my $key (keys %access_keys) { next unless defined $access_hr->{$key}; if ($key eq 'KEY') { ### never print out symmetric keys print STDERR "$key: (removed)\n"; } elsif ($key eq 'GPG_DECRYPT_PW') { ### never print out gpg passwords print STDERR "$key: (removed)\n"; } elsif ($key eq 'GPG_DECRYPT_ID') { if ($include_all_config_data) { print STDERR "$key: $access_hr->{$key}\n"; } else { print STDERR "$key: (removed)\n"; } } elsif ($key eq 'GPG_REMOTE_ID') { if ($include_all_config_data) { print STDERR "$key: $access_hr->{$key}\n"; } else { print STDERR "$key: (removed)\n"; } } elsif ($key eq 'GPG_HOME_DIR') { if ($include_all_config_data) { print STDERR "$key: $access_hr->{$key}\n"; } else { print STDERR "$key: (removed)\n"; } } elsif ($key eq 'OPEN_PORTS' or $key eq 'SOURCE' or $key eq 'REQUIRE_SOURCE_ADDRESS' or $key eq 'require_src_addr_exceptions' or $key eq 'INTERNAL_NET_ACCESS' or $key eq 'internal_net_exceptions' or $key eq 'REQUIRE_SOURCE_ADDRESS') { print STDERR "$key: ", Dumper $access_hr->{$key}; } else { print STDERR "$key: $access_hr->{$key}\n"; } } return; } sub import_override_configs() { my @override_configs = split /,/, $override_config_str; for my $file (@override_configs) { die "[*] Override config file $file does not exist" unless -e $file; &import_config($file); } return; } sub import_config() { my $config_file = shift; open C, "< $config_file" or die "[*] Could not open ", "config file $config_file: $!"; my @lines = ; close C; for my $line (@lines) { chomp $line; next if $line =~ /^\s*#/; next unless $line =~ /\S/; die "[*] No semicolon ending found for line: ", "$line in $config_file" unless $line =~ /^.+;/; if ($line =~ /^\s*(\S+)\s+(.*?)\;/) { my $varname = $1; my $val = $2; if ($varname =~ /^(\w+)Cmd$/) { ### found a command $cmds{$1} = $val unless defined $cmds{$1}; } else { $config{$varname} = $val unless defined $config{$varname}; } } } return; } sub validate_src_access_hsh() { my $access_hr = shift; my $src_line = 0; my $gpg_mode = 0; if (defined $access_hr->{'SOURCE'}) { $src_line = $access_hr->{'src_line_num'}; } else { die "[*] $config{'ACCESS_CONF'}: missing SOURCE variable."; } if (not defined $access_hr->{'OPEN_PORTS'} and not $access_hr->{'PERMIT_CLIENT_PORTS'}) { unless ($access_hr->{'EXTERNAL_CMD_OPEN'} or ($config{'FIREWALL_TYPE'} eq 'external_cmd' and $config{'EXTERNAL_CMD_OPEN'})) { die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ", "missing\n OPEN_PORTS and PERMIT_CLIENT_PORTS is disabled."; } } if (not defined $access_hr->{'REQUIRE_SOURCE_ADDRESS'}) { ### allow 0.0.0.0 ($access_hr->{'REQUIRE_SOURCE_ADDRESS'}, $access_hr->{'require_src_addr_exceptions'}) = &parse_nets('0.0.0.0'); } if (not defined $access_hr->{'INTERNAL_NET_ACCESS'}) { ### allow 0.0.0.0 ($access_hr->{'INTERNAL_NET_ACCESS'}, $access_hr->{'internal_net_exceptions'}) = &parse_nets('0.0.0.0'); } ### default to SPA mode via standard pcap $access_hr->{'DATA_COLLECT_MODE'} = $PCAP unless defined $access_hr->{'DATA_COLLECT_MODE'}; ### only allow forwarding access if ENABLE_IPT_FORWARDING is enabled if ($access_hr->{'ENABLE_FORWARD_ACCESS'} and $config{'ENABLE_IPT_FORWARDING'} eq 'N') { die "[*] $config{'ACCESS_CONF'}: SOURCE: (line: $src_line) ", "ENABLE_FORWARD_ACCESS\n enabled, but ", "ENABLE_IPT_FORWARDING disabled in fwknop.conf."; } if ($access_hr->{'DATA_COLLECT_MODE'} == $ENCRYPT_SEQUENCE) { unless (defined $access_hr->{'KEY'}) { die "[*] $config{'ACCESS_CONF'}: SOURCE: (line: $src_line) ", "missing KEY\n variable for encrypt_seq collection mode."; } unless (defined $access_hr->{'PORT_OFFSET'}) { &logr('[-]', "$config{'ACCESS_CONF'}: SOURCE: (line: $src_line) " . "missing PORT_OFFSET, defaulting to $enc_port_offset.", $NO_MAIL); $access_hr->{'PORT_OFFSET'} = $enc_port_offset; } } elsif ($access_hr->{'DATA_COLLECT_MODE'} == $SHARED_SEQUENCE) { unless (defined $access_hr->{'OPEN_PORTS'}) { die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ", "missing\n OPEN_PORTS variable."; } } elsif ($access_hr->{'DATA_COLLECT_MODE'} == $PCAP or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP) { if (defined $access_hr->{'GPG_AGENT_INFO'} and not defined $access_hr->{'GPG_DECRYPT_PW'}) { $access_hr->{'GPG_DECRYPT_PW'} = ''; } unless (defined $access_hr->{'KEY'} or (defined $access_hr->{'GPG_REMOTE_ID'} and defined $access_hr->{'GPG_DECRYPT_ID'} and defined $access_hr->{'GPG_DECRYPT_PW'})) { die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ", "missing KEY or\n (GPG_DECRYPT_ID, GPG_DECRYPT_PW, or ", "GPG_REMOTE_ID) variable for pcap collection mode."; } if (defined $access_hr->{'GPG_DECRYPT_ID'} or defined $access_hr->{'GPG_DECRYPT_PW'} or defined $access_hr->{'GPG_HOME_DIR'}) { unless (defined $access_hr->{'GPG_REMOTE_ID'}) { die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ", "missing\n GPG_REMOTE_ID variable."; } } $gpg_mode = 1 if defined $access_hr->{'GPG_REMOTE_ID'} and not $cmdl_disable_gpg; if ($gpg_mode) { $access_hr->{'GPG_PREFIX'} = $gpg_default_prefix unless defined $access_hr->{'GPG_PREFIX'}; $access_hr->{'GPG_PATH'} = '' unless defined $access_hr->{'GPG_PATH'}; $access_hr->{'GPG_NO_OPTIONS'} = 0 unless defined $access_hr->{'GPG_NO_OPTIONS'}; $access_hr->{'GPG_USE_OPTIONS'} = 0 unless defined $access_hr->{'GPG_USE_OPTIONS'}; unless ($access_hr->{'GPG_PATH'}) { &check_commands({'gpg' => ''}, {}); } } if (defined ($access_hr->{'REQUIRE_AUTH_METHOD'})) { unless (lc($access_hr->{'REQUIRE_AUTH_METHOD'}) eq 'crypt') { die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ", "invalid\n REQUIRE_AUTH_METHOD, must be set to 'crypt'."; } unless (defined $access_hr->{'SHADOW_FILE'}) { $access_hr->{'SHADOW_FILE'} = '/etc/shadow'; } } if (defined $access_hr->{'EXTERNAL_CMD_OPEN'} and $access_hr->{'EXTERNAL_CMD_OPEN'}) { unless (defined $access_hr->{'EXTERNAL_CMD_CLOSE'} and $access_hr->{'EXTERNAL_CMD_CLOSE'}) { die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ", "invalid\n Cannot define EXTERNAL_CMD_OPEN without also ", "defining EXTERNAL_CMD_CLOSE"; } } else { $access_hr->{'EXTERNAL_CMD_OPEN'} = ''; } if (defined $access_hr->{'EXTERNAL_CMD_CLOSE'} and $access_hr->{'EXTERNAL_CMD_CLOSE'}) { unless (defined $access_hr->{'EXTERNAL_CMD_OPEN'} and $access_hr->{'EXTERNAL_CMD_OPEN'}) { die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ", "invalid\n Cannot define EXTERNAL_CMD_CLOSE without also ", "defining EXTERNAL_CMD_OPEN"; } } else { $access_hr->{'EXTERNAL_CMD_CLOSE'} = ''; } if ($access_hr->{'EXTERNAL_CMD_OPEN'}) { $access_hr->{'EXTERNAL_CMD_ALARM'} = $EXTERNAL_CMD_ALARM unless defined $access_hr->{'EXTERNAL_CMD_ALARM'}; } } else { die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ", "missing valid DATA_COLLECT_MODE\n key (must be one of ", "ENCRYPT_SEQUENCE, SHARED_SEQUENCE, FILE_PCAP, ULOG_PCAP, or PCAP)."; } if (defined $access_hr->{'MIN_TIME_DIFF'} and defined $access_hr->{'MAX_TIME_DIFF'}) { if ($access_hr->{'MAX_TIME_DIFF'} < $access_hr->{'MIN_TIME_DIFF'}) { die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ", "MAX_TIME_DIFF\n cannot be less than MIN_TIME_DIFF."; } } if (defined $access_hr->{'KNOCK_INTERVAL'}) { if ($access_hr->{'KNOCK_INTERVAL'} < 0) { die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ", "KNOCK_INTERVAL\n must be greater than or equal to zero."; } } else { unless ($access_hr->{'DATA_COLLECT_MODE'} == $PCAP or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP) { &logr('[-]', "$config{'ACCESS_CONF'}: SOURCE (line: $src_line) " . "missing KNOCK_INTERVAL, defaulting to $knock_interval.", $NO_MAIL); $access_hr->{'KNOCK_INTERVAL'} = $knock_interval; } } if (defined $access_hr->{'FW_ACCESS_TIMEOUT'}) { if ($access_hr->{'FW_ACCESS_TIMEOUT'} < 0) { die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ", "FW_ACCESS_TIMEOUT\n must be greater than or equal to zero."; } } else { if (defined $access_hr->{'PERMIT_CLIENT_TIMEOUT'}) { ### in this case we will derive the timeout from the SPA ### packet. $access_hr->{'FW_ACCESS_TIMEOUT'} = 0; } else { &logr('[-]', "$config{'ACCESS_CONF'}: SOURCE (line: $src_line) " . "missing FW_ACCESS_TIMEOUT, defaulting to $default_access_timeout", $NO_MAIL); $access_hr->{'FW_ACCESS_TIMEOUT'} = $default_access_timeout; } } if (defined $access_hr->{'KNOCK_LIMIT'}) { if ($access_hr->{'KNOCK_LIMIT'} < 0) { die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ", "KNOCK_LIMIT\n must be greater than or equal to zero."; } } if ($gpg_mode and not $cmdl_disable_gpg) { unless (defined $access_hr->{'GPG_HOME_DIR'}) { $access_hr->{'GPG_HOME_DIR'} = $config{'GPG_DEFAULT_HOME_DIR'}; } unless (-d $access_hr->{'GPG_HOME_DIR'}) { die "[*] $config{'ACCESS_CONF'}: GnuPG directory " . "$access_hr->{'GPG_HOME_DIR'} does not exist."; } } if (defined $access_hr->{'KEY'} and $access_hr->{'KEY'} =~ /_?_CHANGEME_?_/) { die "[*] $config{'ACCESS_CONF'}: Update the KEY variable ". "from the default of __CHANGEME__"; } return 1; } sub handle_command_line() { ### make Getopts case sensitive Getopt::Long::Configure('no_ignore_case'); die "[*] Use --help for usage information.\n" unless (GetOptions( 'config=s' => \$config_file, 'access-conf=s' => \$access_conf_file, 'os' => \$os_fprint_only, 'intf=s' => \$cmdline_intf, 'Count=i' => \$packet_limit, 'fw-log=s' => \$os_ipt_log, 'fw-list' => \$fw_list, 'fw-flush' => \$fw_flush, 'fw-del-chains' => \$ipt_del_chains, 'fw-del-ip=s' => \$fw_del_ip, 'fw-type=s' => \$fw_type, 'gpg-agent-info=s' => \$gpg_agent_info, 'gpg-no-options' => \$gpg_no_options, 'gpg-use-options' => \$gpg_use_options, 'no-gpg' => \$cmdl_disable_gpg, 'debug' => \$debug, 'Kill' => \$stop_daemons, 'Restart' => \$restart, 'Status' => \$status, 'Override-config=s' => \$override_config_str, 'Linux-cooked-intf' => \$PCAP_COOKED_INTF, 'Include-all-config' => \$include_all_config_data, 'Test-mode' => \$test_mode, 'knoptmCmd=s' => \$cmdline_knoptm, 'fwknop_servCmd=s' => \$cmdline_fwknop_serv, 'fwkserv-debug-file=s' => \$fwkserv_debug_file, 'fwkserv-debug-pidname' => \$fwkserv_include_pidname, 'knoptm-debug-file=s' => \$knoptm_debug_file, 'knoptm-debug-pidname' => \$knoptm_include_pidname, 'spa-dump-packets=s' => \$spa_dump_packets, 'LC_ALL=s' => \$cmdline_locale, 'locale=s' => \$cmdline_locale, 'Lib-dir=s' => \$lib_dir, 'no-LC_ALL' => \$no_locale, 'no-locale' => \$no_locale, 'no-FKO-module' => \$skip_fko_module, 'test-FKO-exists' => \$test_fko_exists, 'Dump-config' => \$dump_config, 'verbose' => \$verbose, 'Version' => \$print_version, 'help' => \$print_help )); return; } ### check paths to commands and attempt to correct if any are wrong. sub check_commands() { my ($include_hr, $exclude_hr) = @_; if ($debug and $verbose) { print STDERR "[+] check_commands() include/exclude hrefs:\n", Dumper($include_hr), Dumper $exclude_hr; } my @path = qw( /bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin ); for my $cmd (keys %cmds) { if (keys %$include_hr) { next unless defined $include_hr->{$cmd}; } if (keys %$exclude_hr) { next if defined $exclude_hr->{$cmd}; } if ($cmd eq 'iptables') { next unless $config{'FIREWALL_TYPE'} eq 'iptables'; } elsif ($cmd eq 'ipfw') { next unless $config{'FIREWALL_TYPE'} eq 'ipfw'; } if ($cmd eq 'mknod') { next unless $config{'AUTH_MODE'} eq 'KNOCK'; } if ($cmd eq 'mail' or $cmd eq 'sendmail') { next if $config{'ALERTING_METHODS'} =~ /noe?mail/i; } unless (-x $cmds{$cmd}) { my $found = 0; if ($debug) { print STDERR "[-] $cmd not located/executable at $cmds{$cmd}\n"; } PATH: for my $dir (@path) { if (-x "${dir}/${cmd}") { $cmds{$cmd} = "${dir}/${cmd}"; $found = 1; last PATH; } } if ($found) { if ($debug) { print STDERR " Found $cmd at $cmds{$cmd}\n"; } } else { die "[*] Could not find $cmd anywhere. Please edit the\n", "config section in $config_file to include the path to\n", "$cmd." unless $cmd eq 'sendmail'; } } if (-x $cmds{$cmd}) { if ($cmd eq 'sendmail') { $use_sendmail = 1; } } else { die "[*] Command $cmd is located at $cmds{$cmd}, but ", "is not executable by uid: $<" unless $cmd eq 'sendmail'; } } return; } sub sendmail() { my $subject = shift; $subject =~ s/\"//g; if ($use_sendmail) { open SMAIL, "| $cmds{'sendmail'} -t" or die "[*] Could not execute $cmds{'sendmail'}: $!"; print SMAIL "From: $config{'EMAIL_ADDRESSES'}\n", "To: $config{'EMAIL_ADDRESSES'}\n", "Subject: $subject\n\n"; close SMAIL; } else { open MAIL, qq{| $cmds{'mail'} -s "$subject" $config{'EMAIL_ADDRESSES'} } . "> /dev/null" or die "[*] Could not send mail: $cmds{'mail'} -s " . "$subject\" $config{'EMAIL_ADDRESSES'}: $!"; close MAIL; } return; } sub spa_dump_packets() { my $rv = 0; &import_access(); print "[+] Reading in encoded/encrypted SPA packets ", "from file: $spa_dump_packets\n"; open F, "< $spa_dump_packets" or die "[*] Could not open $spa_dump_packets: $!"; while () { next unless /\S/; chomp; &SPA_check_grant_access('127.0.0.1', length($_), $_); } close F; return $rv; } sub uniquepid() { if (-e $config{'FWKNOP_PID_FILE'}) { my $caller = $0; open PIDFILE, "< $config{'FWKNOP_PID_FILE'}"; my $pid = ; close PIDFILE; chomp $pid; if (kill 0, $pid) { # fwknopd is already running die "[*] fwknopd (pid: $pid) is already running! Exiting.\n"; } } return; } sub writepid() { open P, "> $config{'FWKNOP_PID_FILE'}" or die "[*] Could not open ", "$config{'FWKNOP_PID_FILE'}: $!"; print P $$, "\n"; close P; chmod 0600, $config{'FWKNOP_PID_FILE'}; return; } sub writecmdline() { my $args_cp_aref = shift; open C, "> $config{'FWKNOP_CMDLINE_FILE'}" or die "[*] Could not open ", "$config{'FWKNOP_CMDLINE_FILE'}: $!"; print C "@$args_cp_aref\n"; close C; chmod 0600, $config{'FWKNOP_CMDLINE_FILE'}; return; } sub stop_fwknop() { &logr('[+]', 'shutting down fwknop daemons', $NO_MAIL); ### must kill knopwatchd first since if not, it might try to restart ### any of the other two daemons. for my $name qw(knopwatchd knopmd knoptm fwknop_serv fwknopd) { &stop_daemon($name, $LOG_VERBOSE); } return 0; } sub restart() { my $cmdline = ''; if (-e $config{'FWKNOP_CMDLINE_FILE'}) { open CMD, "< $config{'FWKNOP_CMDLINE_FILE'}" or die "[*] Could not open $config{'FWKNOP_CMDLINE_FILE'}: $!"; $cmdline = ; close CMD; chomp $cmdline; } ### stop any running fwknop daemons. &stop_fwknop(); print "[+] Restarting fwknop daemons.\n"; if ($cmdline) { open FWKNOPD, "| $cmds{'fwknopd'} $cmdline" or die "[*] Could not ", "execute $cmds{'fwknopd'} $cmdline"; close FWKNOPD; } else { open FWKNOPD, "| $cmds{'fwknopd'}" or die "[*] Could not ", "execute $cmds{'fwknopd'}"; close FWKNOPD; } return 0; } sub status() { for my $pidname qw(knopwatchd knopmd knoptm fwknopd) { my $pidfile = $pid_files{$pidname}; if (-e $pidfile) { open PIDFILE, "< $pidfile" or die '[*] Could not open ', "$pidfile: $!"; my $pid = ; close PIDFILE; chomp $pid; if (kill 0, $pid) { print "[+] $pidname is running as pid: $pid\n"; } else { my $print = 1; if (($config{'AUTH_MODE'} =~ /PCAP/ or $config{'AUTH_MODE'} eq 'SOCKET') and $pidname eq 'knopmd') { $print = 0; } print "[+] $pidname is not currently running.\n" if $print; } } else { my $print = 1; if (($config{'AUTH_MODE'} =~ /PCAP/ or $config{'AUTH_MODE'} eq 'SOCKET') and $pidname eq 'knopmd') { $print = 0; } print "[+] $pidname pidfile does not exist.\n" if $print; } } return 0; } sub fwknop_init() { ### import any override config files first &import_override_configs() if $override_config_str; ### import config &import_config($config_file); ### expand any embedded vars within config values &expand_vars({'EXTERNAL_CMD_OPEN' => '', 'EXTERNAL_CMD_CLOSE' => ''}); ### set __NONE__ values to '' &set_null_vars(); $config{'PCAP_INTF'} = $cmdline_intf if $cmdline_intf; $PCAP_COOKED_INTF = 1 if $config{'PCAP_INTF'} eq 'any'; ### make sure all the vars we need are actually in the config file. &required_vars(); ### import fwknop perl modules &import_perl_modules(); ### validate config &validate_config(); ### make sure command paths are correct unless ($os_fprint_only) { &check_commands({}, {'gpg' => '', 'gpg2' => '', 'mail' => ''}); unless ($use_sendmail) { &check_commands({'mail' => ''}, {}); } } if ($fw_del_ip) { die "[*] $fw_del_ip does not look like an IP address" unless $fw_del_ip =~ /$ip_re/; } if ($config{'FIREWALL_TYPE'} eq 'iptables') { ### --fw-list, lists rules in FWKNOP iptables chains exit &ipt_list() if $fw_list; ### --fw-flush, flush rules in FWKNOP iptables chains exit &ipt_flush() if $fw_flush; ### --fw-del-ip exit &ipt_del_ip() if $fw_del_ip; ### build iptables config from IPT_INPUT_ACCESS, IPT_OUTPUT_ACCESS, ### IPT_FORWARD_ACCESS, IPT_DNAT_ACCESS, and IPT_MASQ_ACCESS keywords &build_ipt_config(); } elsif ($config{'FIREWALL_TYPE'} eq 'ipfw') { ### --fw-list, lists all ipfw rules exit &ipfw_list() if $fw_list; ### --fw-flush (not actually supported yet for ipfw firewalls) exit &ipfw_flush() if $fw_flush; ### --fw-del-ip exit &ipfw_del_ip() if $fw_del_ip; } elsif ($config{'FIREWALL_TYPE'} eq 'external_cmd') { if ($fw_list) { if (-x $cmds{'iptables'}) { exit &ipt_list(); } elsif (-x $cmds{'ipfw'}) { exit &ipfw_list(); } } } %pid_files = ( 'knopwatchd' => $config{'KNOPWATCHD_PID_FILE'}, 'knoptm' => $config{'KNOPTM_PID_FILE'}, 'knopmd' => $config{'KNOPMD_PID_FILE'}, 'fwknop_serv' => $config{'TCPSERV_PID_FILE'}, 'fwknopd' => $config{'FWKNOP_PID_FILE'} ); ### --Kill exit &stop_fwknop() if $stop_daemons; ### --Restart exit &restart() if $restart; ### --Status exit &status() if $status; ### --spa-dump-packets (dumps decrypted SPA packets out on stdout) exit &spa_dump_packets() if $spa_dump_packets; ### make sure there is not another fwknopd process already running. &uniquepid() unless $os_fprint_only; &logr('[+]', "starting fwknopd v$version (file revision: $rev_num)", $NO_MAIL); print STDERR "[+] Start time: [" . localtime() . "]\n" if $debug; if ($config{'FIREWALL_TYPE'} eq 'iptables') { ### always remove any existing rules &ipt_flush() if $config{'FLUSH_IPT_AT_INIT'} eq 'Y'; ### alert the user if there are no state tracking rules loaded ### in the iptables policy to keep connections open &ipt_check_stateful_rule(); } elsif ($config{'FIREWALL_TYPE'} eq 'ipfw') { ### alert the user if there are no state tracking rules loaded ### in the ipfw policy to keep connections open &ipfw_check_stateful_rule(); &ipfw_disable_set($config{'IPFW_SET_NUM'}) if $ipfw_is_dynamic; } if ($packet_limit) { die "[*] -C must be greater than zero" unless $packet_limit > 0; } if ($config{'AUTH_MODE'} eq 'KNOCK') { ### legacy port knocking mode if ($config{'ENABLE_SYSLOG_FILE'} eq 'Y') { $fw_data_file = $config{'IPT_SYSLOG_FILE'}; die "[*] IPT_SYSLOG_FILE $config{'IPT_SYSLOG_FILE'} does not exist" unless -e $fw_data_file; } else { $fw_data_file = $config{'FW_DATA_FILE'}; unless (-e "$fw_data_file") { print "[+] Creating $fw_data_file file\n"; open F, "> $fw_data_file" or die "[*] Could not open ", "$fw_data_file: $!"; close F; chmod 0600, $fw_data_file or die "[*] Could ", "not chmod(0600, $fw_data_file): $!"; chown 0, 0, $fw_data_file or die "[*] Could not ", "chown 0,0,$fw_data_file: $!"; } unless (-e $config{'KNOPMD_FIFO'}) { system "$cmds{'mknod'} -m 600 $config{'KNOPMD_FIFO'} p"; } } ### import passive OS fingerprints (based on p0f) &import_p0f_sigs(); } if ($os_fprint_only) { if ($os_ipt_log) { $fw_data_file = $os_ipt_log; } print "[+] Parsing iptables log: $fw_data_file\n"; } else { ### import access directives &import_access(); if ($dump_config) { &dump_config(); print STDERR "\n", localtime() . " [+] Dumping access ", "config: $config{'ACCESS_CONF'}\n\n"; for (my $i=0; $i<=$#access; $i++) { &dump_access($access[$i], $i); } exit 0; } unless ($debug) { my $pid = fork(); exit 0 if $pid; die "[*] $0: Couldn't fork: $!" unless defined $pid; POSIX::setsid() or die "[*] $0: Can't start a new session: $!"; } for my $dir qw(/var/lib /var/run) { next if -d $dir; mkdir $dir, 0755 or die "[*] Could not mkdir $dir: $!"; } for my $dir qw( FWKNOP_DIR FWKNOP_ERR_DIR FWKNOP_RUN_DIR FWKNOP_LIB_DIR ) { next if -d $config{$dir}; mkdir $config{$dir}, 0500 or die "[*] Could not mkdir $config{$dir}: $!"; } ### write our pid out to disk &writepid(); ### write a copy of our command line out to disk &writecmdline(\@args_cp) unless $debug; ### start knopmd and knopwatchd here (if they are already running ### it is ok, another instance will not be started). if ($config{'AUTH_MODE'} ne 'KNOCK') { ### make sure knopmd is not running &stop_daemon('knopmd', $LOG_QUIET); ### see if we need to start the fwknop_serv TCP server. The only ### real application of this is when running SPA packets over the ### Tor network. if ($config{'ENABLE_TCP_SERVER'} eq 'Y' or $config{'ENABLE_UDP_SERVER'} eq 'Y') { if ($config{'AUTH_MODE'} =~ /PCAP/ and $config{'PCAP_FILTER'} ne 'NONE' and $config{'PCAP_FILTER'} !~ /^\s*port\s+62201/ and $config{'PCAP_FILTER'} !~ /tcp\s+port\s+62201/) { &logr('[-]', "ENABLE_TCP_SERVER is enabled, but " . "PCAP_FILTER may not accept TCP/62201", $SEND_MAIL); } ### restart fwknop_serv &stop_daemon('fwknop_serv', $LOG_QUIET); my $fws_cmd = "$cmds{'fwknop_serv'} -c $config_file"; $fws_cmd .= " --locale $cmdline_locale" if $cmdline_locale; $fws_cmd .= " --Override-config $override_config_str" if $override_config_str; $fws_cmd .= " --Debug-include-pidname" if $fwkserv_include_pidname; print STDERR localtime() . " [+] Executing: $fws_cmd\n" if $debug; open FWSERV, "| $fws_cmd" or die "[*] Could not ", "execute $fws_cmd"; close FWSERV; } } else { unless ($config{'ENABLE_SYSLOG_FILE'} eq 'Y') { print STDERR " [+] Executing: $cmds{'knopmd'}\n" if $debug; open KNOPMD, "| $cmds{'knopmd'}" or die "[*] Could not execute $cmds{'knopmd'}"; close KNOPMD; } } ### knoptm removes firewall rules if we are running in PCAP mode ### so tell it not to exit my $cmd = $cmds{'knoptm'}; if ($debug and $config{'ENABLE_VOLUNTARY_EXITS'} eq 'Y') { $cmd .= ' --no-voluntary-exits'; } ### append interface for running interface existence checks ### (ppp interfaces might go away and reappear) $cmd .= " -i $config{'PCAP_INTF'}"; $cmd .= " --fw-type $config{'FIREWALL_TYPE'}" if $fw_type; $cmd .= " --locale $cmdline_locale" if $cmdline_locale; $cmd .= " --Override-config $override_config_str" if $override_config_str; $cmd .= " --Debug-include-pidname" if $knoptm_include_pidname; print STDERR localtime() . " [+] Executing: $cmd -c $config_file\n" if $debug; ### always start knoptm open KNOPTM, "| $cmd -c $config_file" or die "[*] Could not execute ", "$cmd -c $config_file"; close KNOPTM; ### always start knopwatchd except for debugging mode unless ($debug) { $cmd = $cmds{'knopwatchd'}; $cmd .= " -c $config_file"; $cmd .= " -O $override_config_str" if $override_config_str; open KNOPWATCHD, "| $cmd" or die "[*] Could not ", "execute $cmds{'knopwatchd'}"; close KNOPWATCHD; } } if ($config{'AUTH_MODE'} ne 'KNOCK') { &import_digests() if $config{'ENABLE_DIGEST_PERSISTENCE'} eq 'Y'; } if ($config{'AUTH_MODE'} eq 'SOCKET') { unlink $config{'FWKNOP_SERV_SOCK'} if -e $config{'FWKNOP_SERV_SOCK'}; } ### Install signal handlers for debugging and for reaping zombie ### processes. $SIG{'__WARN__'} = \&warn_handler; $SIG{'__DIE__'} = \&die_handler; $SIG{'CHLD'} = \&REAPER; if ($debug) { print STDERR localtime() . " [+] Set SIGCHLD handler to: " . \&REAPER . "\n"; print STDERR localtime() . " [+] Set __WARN__ handler to: " . \&warn_handler . "\n"; print STDERR localtime() . " [+] Set __DIE__ handler to: " . \&die_handler . "\n"; } &handle_locale(); return; } sub handle_locale() { $config{'LOCALE'} = $cmdline_locale if $cmdline_locale; if ($config{'LOCALE'} ne 'NONE' and not $no_locale) { ### set LC_ALL env variable $ENV{'LC_ALL'} = $config{'LOCALE'}; } return; } sub ipfw_list() { print "[+] Listing fwknop 'allow' or 'DISABLED' rules ", "from ipfw policy...\n\n"; my $ipfw_policy_ar = &ipfw_dump_policy(); for (@$ipfw_policy_ar) { print if /\spass\s/ or /\sallow\s/ or /DISABLED/; } return 0; } sub ipfw_dump_policy() { my $cmd = "$cmds{'ipfw'} -dS list"; my @ipfw_policy = (); open IPFW, "$cmd |" or die "[*] Could not execute $cmd: $!"; @ipfw_policy = ; close IPFW; return \@ipfw_policy; } sub ipfw_policy_print() { my $print_dst = shift; my $ipfw_policy_ar = &ipfw_dump_policy(); if ($print_dst == $STDOUT) { print for @$ipfw_policy_ar; } elsif ($print_dst == $STDERR) { print STDERR for @$ipfw_policy_ar; } else { die "[*] Invalid print dst: $print_dst"; } return; } sub ipt_list() { if ($verbose) { print "[+] Dumping iptables policy...\n"; for my $table qw/filter nat/ { system "$cmds{'iptables'} -t $table -v -n -L"; } return 0; } else { ### build iptables config from IPT_INPUT_ACCESS, IPT_OUTPUT_ACCESS, ### IPT_FORWARD_ACCESS, IPT_DNAT_ACCESS, IPT_MASQUERADE_ACCESS keywords &build_ipt_config(); &dump_ipt_policy(); } return 0; } sub dump_ipt_policy() { print STDERR localtime() . " [+] dump_ipt_policy()\n" if $debug; my $ipt = &get_iptables_chainmgr_obj($ZERO_SLEEP); print "[+] Listing rules in fwknop chains...\n"; for my $hr (@ipt_config) { my $table = $hr->{'table'}; my $to_chain = $hr->{'to_chain'}; if ($ipt->chain_exists($table, $to_chain)) { my ($rv, $out_ar, $err_ar) = $ipt->run_ipt_cmd("$cmds{'iptables'} -t " . "$table -v -n -L $to_chain"); if ($rv and $out_ar) { print for @$out_ar; print "\n"; } } else { print "[-] Table: $table, chain: $to_chain, does not exist\n"; } } undef $ipt; return 0; } sub ipt_del_ip() { &build_ipt_config(); my $ipt = &get_iptables_chainmgr_obj($ZERO_SLEEP); print "[+] Deleting ACCEPT src $fw_del_ip rules from fwknop chains...\n"; if (@ipt_config) { for my $hr (@ipt_config) { next unless $hr; my $table = $hr->{'table'}; my $to_chain = $hr->{'to_chain'}; if ($ipt->chain_exists($table, $to_chain)) { for (;;) { my ($rulenum, $chain_rules) = $ipt->find_ip_rule( $fw_del_ip, '0.0.0.0/0', $table, $to_chain, 'ACCEPT', {}); last unless $rulenum; print " $cmds{'iptables'} -D $to_chain $rulenum\n"; $ipt->run_ipt_cmd("$cmds{'iptables'} -D $to_chain $rulenum"); } } } } undef $ipt; return 0; } sub ipfw_flush() { print "[+] Flushing ipfw policies not supported yet.\n"; return 0; } sub ipt_flush() { &build_ipt_config(); my $ipt = &get_iptables_chainmgr_obj($ZERO_SLEEP); if ($fw_flush) { print "[+] Flushing iptables fwknop chains...\n"; } else { &logr('[+]', 'flushing existing iptables fwknop chains', $NO_MAIL); } if (@ipt_config) { for my $hr (@ipt_config) { next unless $hr; my $table = $hr->{'table'}; my $from_chain = $hr->{'from_chain'}; my $to_chain = $hr->{'to_chain'}; if ($ipt->chain_exists($table, $to_chain)) { my ($rv, $out_ar, $err_ar) = $ipt->flush_chain($table, $to_chain); if ($rv) { $ipt->delete_chain($table, $from_chain, $to_chain) if $ipt_del_chains; print "[+] Flushed: $to_chain\n" if $fw_flush; } else { if ($fw_flush) { print "[-] Could not flush: $to_chain\n"; print for @$err_ar; } } } else { print "[-] Chain: $to_chain does not exist.\n" if $fw_flush; } } } else { print "[-] No valid IPT_AUTO_CHAIN keywords.\n" if $fw_flush; } undef $ipt; return 0; } sub stop_daemon() { my ($name, $log_flag) = @_; print STDERR localtime() . " [+] Stopping $name daemon...\n" if $debug or $verbose; unless (-e $pid_files{$name}) { if ($log_flag == $LOG_VERBOSE) { my $print = 1; if ($config{'AUTH_MODE'} =~ /PCAP/ and $name eq 'knopmd') { $print = 0; } if ($name eq 'fwknop_serv' and $config{'ENABLE_TCP_SERVER'} eq 'N' and $config{'ENABLE_UDP_SERVER'} eq 'N') { $print = 0; } print "[-] pid file $pid_files{$name} does not " . "exist for $name\n" if $print; } return; } open PID, "< $pid_files{$name}" or die "[*] Could not open $pid_files{$name}: $!"; my $pid = ; close PID; chomp $pid; if (kill 0, $pid) { if ($log_flag == $LOG_VERBOSE) { print "[+] $name is running (pid: $pid), stopping daemon\n"; } kill 9, $pid unless kill 15, $pid; sleep 1; if (kill 0, $pid) { die "[*] Could not kill $name process (pid: $pid)"; } } else { if ($log_flag == $LOG_VERBOSE) { my $print = 1; if ($config{'AUTH_MODE'} =~ /PCAP/ and $name eq 'knopmd') { $print = 0; } print "[-] $name is not running\n" if $print; } } unlink $pid_files{$name}; return; } ### write a message to syslog (leaves off $prefix, which assigns a ### "type" to the message, when writing syslog; might add it later sub logr() { my ($prefix, $msg, $send_email) = @_; if ($debug) { print STDERR localtime() . " $prefix $msg\n"; return; } ### see if we need to send an email if ($config{'ALERTING_METHODS'} =~ /verbose/ or ($send_email == $SEND_MAIL and $config{'ALERTING_METHODS'} !~ /noe?mail/i)) { &sendmail("$prefix $config{'HOSTNAME'} fwknopd: $msg"); } return if $config{'ALERTING_METHODS'} =~ /no.?syslog/i; ### this is an ugly hack to avoid the 'can't use string as subroutine' ### error because of 'use strict' if ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL7/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL7()); } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL6/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL6()); } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL5/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL5()); } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL4/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL4()); } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL3/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL3()); } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL2/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL2()); } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL1/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL1()); } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL0/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL0()); } if ($config{'SYSLOG_PRIORITY'} =~ /LOG_INFO/i) { syslog(&LOG_INFO(), $msg); } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_DEBUG/i) { syslog(&LOG_DEBUG(), $msg); } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_NOTICE/i) { syslog(&LOG_NOTICE(), $msg); } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_WARNING/i) { syslog(&LOG_WARNING(), $msg); } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_ERR/i) { syslog(&LOG_ERR(), $msg); } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_CRIT/i) { syslog(&LOG_CRIT(), $msg); } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_ALERT/i) { syslog(&LOG_ALERT(), $msg); } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_EMERG/i) { syslog(&LOG_EMERG(), $msg); } closelog(); return; } sub psyslog_errs() { my $aref = shift; if ($debug) { for my $msg (@$aref) { print STDERR localtime() . " $msg\n"; } } return if $config{'ALERTING_METHODS'} =~ /no.?syslog/i; ### this is an ugly hack to avoid the 'can't use string as subroutine' ### error because of 'use strict' if ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL7/i) { openlog($config{'SYSLOG_IDENTITY'},&LOG_DAEMON(), &LOG_LOCAL7()); } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL6/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL6()); } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL5/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL5()); } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL4/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL4()); } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL3/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL3()); } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL2/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL2()); } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL1/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL1()); } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL0/i) { openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL0()); } if ($config{'SYSLOG_PRIORITY'} =~ /LOG_INFO/i) { for (my $i=0; $i<5 && $i<=$#$aref; $i++) { syslog(&LOG_INFO(), $aref->[$i]); } } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_DEBUG/i) { for (my $i=0; $i<5 && $i<=$#$aref; $i++) { syslog(&LOG_DEBUG(), $aref->[$i]); } } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_NOTICE/i) { for (my $i=0; $i<5 && $i<=$#$aref; $i++) { syslog(&LOG_NOTICE(), $aref->[$i]); } } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_WARNING/i) { for (my $i=0; $i<5 && $i<=$#$aref; $i++) { syslog(&LOG_WARNING(), $aref->[$i]); } } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_ERR/i) { for (my $i=0; $i<5 && $i<=$#$aref; $i++) { syslog(&LOG_ERR(), $aref->[$i]); } } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_CRIT/i) { for (my $i=0; $i<5 && $i<=$#$aref; $i++) { syslog(&LOG_CRIT(), $aref->[$i]); } } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_ALERT/i) { for (my $i=0; $i<5 && $i<=$#$aref; $i++) { syslog(&LOG_ALERT(), $aref->[$i]); } } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_EMERG/i) { for (my $i=0; $i<5 && $i<=$#$aref; $i++) { syslog(&LOG_EMERG(), $aref->[$i]); } } closelog(); return; } sub set_null_vars() { for my $var (keys %config) { $config{$var} = '' if $config{$var} eq '__NONE__'; } return; } sub expand_vars() { my $exclude_hr = shift; my $has_sub_var = 1; my $resolve_ctr = 0; while ($has_sub_var) { $resolve_ctr++; $has_sub_var = 0; if ($resolve_ctr >= 20) { die "[*] Exceeded maximum variable resolution counter."; } for my $hr (\%config, \%cmds) { for my $var (keys %$hr) { next if defined $exclude_hr->{$var}; my $val = $hr->{$var}; if ($val =~ m|\$(\w+)|) { my $sub_var = $1; die "[*] sub-ver $sub_var not allowed within same ", "variable $var" if $sub_var eq $var; if (defined $config{$sub_var}) { $val =~ s|\$$sub_var|$config{$sub_var}|; $hr->{$var} = $val; } else { die "[*] sub-var \"$sub_var\" not defined in ", "config for var: $var."; } $has_sub_var = 1; } } } } return; } sub build_ipt_config() { return if $build_ipt_config; $build_ipt_config = 1; print STDERR localtime() . " [+] Building iptables config info.\n" if $debug; ### for the FWKNOP_INPUT chain if (&parse_ipt_var(\%ipt_input, $config{'IPT_INPUT_ACCESS'})) { push @ipt_config, \%ipt_input; } if ($config{'ENABLE_IPT_OUTPUT'} eq 'Y' or $fw_list or $fw_flush) { ### for the optional FWKNOP_OUTPUT chain if (&parse_ipt_var(\%ipt_output, $config{'IPT_OUTPUT_ACCESS'})) { push @ipt_config, \%ipt_output; } } if ($config{'ENABLE_IPT_FORWARDING'} eq 'Y' or $config{'ENABLE_IPT_LOCAL_NAT'} eq 'Y' or $fw_list or $fw_flush) { ### for the FWKNOP_FORWARD chain if (&parse_ipt_var(\%ipt_forward, $config{'IPT_FORWARD_ACCESS'})) { push @ipt_config, \%ipt_forward; } ### for the FWKNOP_PREROUTING chain if (&parse_ipt_var(\%ipt_prerouting, $config{'IPT_DNAT_ACCESS'})) { push @ipt_config, \%ipt_prerouting; } ### for the FWKNOP_POSTROUTING chain if ($config{'ENABLE_IPT_SNAT'} eq 'Y') { if ($config{'SNAT_TRANSLATE_IP'} =~ /^$ip_re$/) { if (&parse_ipt_var(\%ipt_postrouting, $config{'IPT_SNAT_ACCESS'})) { push @ipt_config, \%ipt_postrouting; } } else { if (&parse_ipt_var(\%ipt_postrouting, $config{'IPT_MASQUERADE_ACCESS'})) { push @ipt_config, \%ipt_postrouting; } } } } return; } sub parse_ipt_var() { my ($hr, $ipt_line) = @_; unless ($imported_iptables_modules) { require IPTables::ChainMgr; if ($debug) { print STDERR "[+] IPTables::ChainMgr::VERSION ", "$IPTables::ChainMgr::VERSION\n"; } $imported_iptables_modules = 1; } my $ipt = &get_iptables_chainmgr_obj($ZERO_SLEEP); my @block = split /\s*,\s*/, $ipt_line; if ($#block == 4 or $#block == 6) { if ($#block == 4) { ### ACCEPT, src, filter, INPUT, FWKNOP_INPUT; $hr->{'target'} = $block[0]; $hr->{'direction'} = $block[1]; $hr->{'table'} = $block[2]; $hr->{'from_chain'} = $block[3]; $hr->{'to_chain'} = $block[4]; $hr->{'jump_rule_position'} = 1; $hr->{'auto_rule_position'} = 1; ### this is the old format; generate a warning my $msg = "the IPT_AUTO_CHAIN variable in fwknop.conf " . "needs to be updated to set the jump rule position and " . "the auto rule position; defaulting both to 1."; &logr('[-]', $msg, $NO_MAIL); print STDERR localtime() . " [-] build_ipt_config(): $msg\n" if $debug; } else { ### ACCEPT, src, filter, INPUT, 1, FWKNOP_INPUT, 1; $hr->{'target'} = $block[0]; $hr->{'direction'} = $block[1]; $hr->{'table'} = $block[2]; $hr->{'from_chain'} = $block[3]; $hr->{'jump_rule_position'} = $block[4]; $hr->{'to_chain'} = $block[5]; $hr->{'auto_rule_position'} = $block[6]; } unless ($hr->{'direction'} eq 'src' or $hr->{'direction'} eq 'dst' or $hr->{'direction'} eq 'both') { my $msg = "invalid direction $hr->{'direction'} " . "in IPT_AUTO_CHAIN keyword"; &logr('[-]', $msg, $NO_MAIL); die "[-] build_ipt_config(): $msg\n"; } if ($hr->{'from_chain'} eq $hr->{'to_chain'}) { my $msg = "cannot have identical from_chain and to_chain " . "in IPT_AUTO_CHAIN keyword"; &logr('[-]', $msg, $NO_MAIL); die "[-] build_ipt_config(): $msg\n"; } my ($rv, $out_ar, $err_ar) = $ipt->chain_exists($hr->{'table'}, $hr->{'from_chain'}); unless ($rv) { my $msg = "invalid IPT_AUTO_CHAIN keyword, " . "$hr->{'table'} $hr->{'from_chain'} chain does not exist."; &logr('[-]', $msg, $NO_MAIL); if ($hr->{'from_chain'} eq 'FORWARD' or $hr->{'from_chain'} eq 'PREROUTING') { ### usually fwknop is used against the INPUT chain, so ### don't die in this case undef $ipt; return 0; } die "[-] build_ipt_config(): $msg\n"; } } else { my $msg = "invalid IPT_AUTO_CHAIN variable: $ipt_line"; &logr('[-]', $msg, $NO_MAIL); die "[-] build_ipt_config(): $msg\n"; } undef $ipt; return 1; } sub diskwrite_digest() { my ($digest, $src_ip) = @_; print STDERR localtime() . " [+] Calculated digest: $digest for SPA ", "packet from: $src_ip\n" if $debug; open F, ">> $config{'DIGEST_FILE'}" or die "[*] Could not open ", "$config{'DIGEST_FILE'}: $!"; if ($config{'ENABLE_DIGEST_INCLUDE_SRC'} eq 'Y') { print F "$src_ip $digest [" . localtime() . "]\n"; } else { print F $digest, "\n"; } close F; return; } sub import_perl_modules() { my $mod_paths_ar = &get_mod_paths(); if ($#$mod_paths_ar > -1) { ### /usr/lib/fwknop/ exists push @$mod_paths_ar, @INC; splice @INC, 0, $#$mod_paths_ar+1, @$mod_paths_ar; } if ($debug) { print STDERR "[+] import_perl_modules(): The \@INC array:\n"; print STDERR "$_\n" for @INC; } ### see if the FKO module is installed unless ($skip_fko_module or $config{'ENABLE_FKO_MODULE'} eq 'N') { eval { require FKO }; unless ($@) { $use_fko_module = 1; if ($debug or $test_fko_exists) { print STDERR localtime() . " [+] Using FKO module.\n"; } exit 0 if $test_fko_exists; } } unless ($config{'ALERTING_METHODS'} =~ /no.?syslog/i) { require Unix::Syslog; Unix::Syslog->import(qw(:subs :macros)); if ($debug) { print STDERR "[+] Unix::Syslog::VERSION $Unix::Syslog::VERSION\n"; } } require Net::IPv4Addr; Net::IPv4Addr->import(qw/ipv4_in_network/); if ($debug) { print STDERR "[+] Net::IPv4Addr::VERSION $Net::IPv4Addr::VERSION\n"; } return; } sub get_mod_paths() { my @paths = (); $config{'FWKNOP_MOD_DIR'} = $lib_dir if $lib_dir; unless (-d $config{'FWKNOP_MOD_DIR'}) { my $dir_tmp = $config{'FWKNOP_MOD_DIR'}; $dir_tmp =~ s|lib/|lib64/|; if (-d $dir_tmp) { $config{'FWKNOP_MOD_DIR'} = $dir_tmp; } else { return []; } } opendir D, $config{'FWKNOP_MOD_DIR'} or die "[*] Could not open $config{'FWKNOP_MOD_DIR'}: $!"; my @dirs = readdir D; closedir D; push @paths, $config{'FWKNOP_MOD_DIR'}; for my $dir (@dirs) { ### get directories like "/usr/lib/fwknop/x86_64-linux" next unless -d "$config{'FWKNOP_MOD_DIR'}/$dir"; push @paths, "$config{'FWKNOP_MOD_DIR'}/$dir" if $dir =~ m|linux| or $dir =~ m|thread| or (-d "$config{'FWKNOP_MOD_DIR'}/$dir/auto"); } return \@paths; } sub import_digests() { %digest_store = (); for my $digest_file ($config{'DIGEST_FILE'}, "$config{'FWKNOP_DIR'}/md5sums") { next unless -e $digest_file; open F, "< $digest_file" or die "[*] Could not open ", "$digest_file: $!"; while () { if (/^\s*($ip_re)\s+(\S+)\s+\[.{15,25}\s\d{4}\]/) { ### 127.0.0.1 36wD3+KXHLuqqp18D0qODA [Wed Nov 28 09:13:31 2007] ### Date tracking was added in 1.8.4 $digest_store{$2} = $1; } elsif (/^\s*($ip_re)\s+(\S+)$/) { ### 127.0.0.1 36wD3+KXHLuqqp18D0qODA ### version 1.8.3 includes the source IP address for each ### SPA packet (unless ENABLE_DIGEST_INCLUDE_SRC is disabled) $digest_store{$2} = $1; } elsif (/^\s*(\S+)$/) { $digest_store{$1} = ''; } } close F; } if ($debug) { print STDERR localtime() . " [+] digest_store hash: \n", Dumper(\%digest_store); } &logr('[+]', "imported previous tracking digests from disk " . "cache: $config{'DIGEST_FILE'}", $NO_MAIL); return; } sub hex_dump() { my $data = shift; my @chars = split //, $data; my $ctr = 0; my $ascii_str = ''; for my $char (@chars) { if ($ctr % 16 == 0) { print STDERR " $ascii_str\n" if $ascii_str; printf STDERR " 0x%.4x: ", $ctr; $ascii_str = ''; } printf STDERR "%.2x", ord($char); if ((($ctr+1) % 2 == 0) and ($ctr % 16 != 0)) { print STDERR ' '; } if ($char =~ /[^\x20-\x7e]/) { $ascii_str .= '.'; } else { $ascii_str .= $char; } $ctr++; } if ($ascii_str) { my $remainder = 1; if ($ctr % 16 != 0) { $remainder = 16 - $ctr % 16; if ($remainder % 2 == 0) { $remainder = 2*$remainder + int($remainder/2) + 1; } else { $remainder = 2*$remainder + int($remainder/2) + 2; } } print STDERR ' 'x$remainder, $ascii_str; } print STDERR "\n"; return; } sub validate_config() { die qq([*] Invalid EMAIL_ADDRESSES value: "$config{'EMAIL_ADDRESSES'}") unless $config{'EMAIL_ADDRESSES'} =~ /\S+\@\S+/; ### translate commas into spaces $config{'EMAIL_ADDRESSES'} =~ s/\s*\,\s/ /g; unless ($config{'FIREWALL_TYPE'} eq 'iptables' or $config{'FIREWALL_TYPE'} eq 'ipfw' or $config{'FIREWALL_TYPE'} eq 'external_cmd') { die "[*] FIREWALL_TYPE must be either 'iptables', 'ipfw', ", "or 'external_cmd'"; } unless ($config{'AUTH_MODE'} eq 'KNOCK' or $config{'AUTH_MODE'} eq 'FILE_PCAP' or $config{'AUTH_MODE'} eq 'ULOG_PCAP' or $config{'AUTH_MODE'} eq 'PCAP' or $config{'AUTH_MODE'} eq 'SOCKET') { die "[*] AUTH_MODE must be either KNOCK, FILE_PCAP, ", "ULOG_PCAP, PCAP, or SOCKET"; } if ($config{'AUTH_MODE'} eq 'KNOCK' and $config{'FIREWALL_TYPE'} eq 'ipfw') { die "[*] Port knocking mode (see AUTH_MODE var) not yet supported on ", "ipfw firewalls."; } unless ($config{'MIN_GNUPG_MSG_SIZE'} =~ /^\d+$/) { die "[*] Variable MIN_GNUPG_MSG_SIZE must be a ", "positive integer value."; } &check_ip_forward_value() if $config{'ENABLE_IPT_FORWARDING'} eq 'Y'; if ($fw_type) { die "[*] --fw-type must be 'iptables', 'ipfw', or 'external_cmd'" unless $fw_type eq 'iptables' or $fw_type eq 'ipfw' or $fw_type eq 'external_cmd'; $config{'FIREWALL_TYPE'} = $fw_type if $fw_type; } $config{'ACCESS_CONF'} = $access_conf_file if $access_conf_file; ### handle BLACKLIST variable ($blacklist_ar, $blacklist_exclude_ar) = &parse_nets($config{'BLACKLIST'}); if ($cmdline_knoptm) { ### used by the test suite $cmds{'knoptm'} = $cmdline_knoptm; } if ($cmdline_fwknop_serv) { ### used by the test suite $cmds{'fwknop_serv'} = $cmdline_fwknop_serv; } my $found_digest = 0; my $use_md5 = 0; my $use_sha1 = 0; my $use_sha256 = 0; unless ($use_fko_module) { if ($config{'DIGEST_TYPE'} eq 'ALL') { $found_digest = 1; $use_md5 = 1; $use_sha1 = 1; $use_sha256 = 1; } else { if ($config{'DIGEST_TYPE'} =~ /SHA256/) { $found_digest = 1; $use_sha256 = 1; } if ($config{'DIGEST_TYPE'} =~ /SHA1/) { $found_digest = 1; $use_sha1 = 1; } if ($config{'DIGEST_TYPE'} =~ /MD5/) { $found_digest = 1; $use_md5 = 1; } } unless ($found_digest) { die "[*] DIGEST_TYPE must be one of ALL, SHA256, SHA1, or MD5"; } } ### an old fwknop client can send an SPA packet with an ### MD5 sum if ($use_md5) { require Digest::MD5; Digest::MD5->import(qw(md5_base64)); print STDERR "[+] Digest::MD5::VERSION $Digest::MD5::VERSION\n" if $debug; } if ($use_sha1 or $use_sha256) { require Digest::SHA; if ($use_sha1 and $use_sha256) { Digest::SHA->import(qw(sha1_base64 sha256_base64)); } elsif ($use_sha1) { Digest::SHA->import(qw(sha1_base64)); } elsif ($use_sha256) { Digest::SHA->import(qw(sha256_base64)); } print STDERR "[+] Digest::SHA::VERSION $Digest::SHA::VERSION\n" if $debug; } for my $var qw/IPT_EXEC_SLEEP/ { die "[*] var $var must contain a digit." unless &is_digit($config{$var}); } $PCAP_COOKED_INTF = 1 if $config{'ENABLE_COOKED_INTF'} eq 'Y'; die "[*] IPFW_SET_NUM must be a digit between 0 and 31" unless &is_digit($config{'IPFW_SET_NUM'}); if ($config{'IPFW_SET_NUM'} < 0 or $config{'IPFW_SET_NUM'} > 31) { die "[*] IPFW_SET_NUM must be a digit between 0 and 31"; } die "[*] IPFW_DYNAMIC_INTERVAL must be a digit greater than zero" unless &is_digit($config{'IPFW_DYNAMIC_INTERVAL'}); if ($config{'IPFW_DYNAMIC_INTERVAL'} <= 0) { die "[*] IPFW_DYNAMIC_INTERVAL must be a digit greater than zero"; } return; } sub check_ip_forward_value() { if (-e $config{'PROC_IP_FORWARD_FILE'}) { open F, "< $config{'PROC_IP_FORWARD_FILE'}" or die "[*] Could not ", "open $config{'PROC_IP_FORWARD_FILE'}: $!"; my $forward_val = ; close F; chomp $forward_val; unless ($forward_val == 1) { my $msg = "ENABLE_IPT_FORWARDING is enabled, but IP forwarding " . "is disabled in $config{'PROC_IP_FORWARD_FILE'}"; if ($config{'ENABLE_PROC_IP_FORWARD'} eq 'Y') { $msg .= ', enabling'; open F, "> $config{'PROC_IP_FORWARD_FILE'}" or die "[*] Could not ", "open $config{'PROC_IP_FORWARD_FILE'}: $!"; print F "1\n"; close F; } &logr('[-]', $msg, $SEND_MAIL); } } else { &logr('[-]', "$config{'PROC_IP_FORWARD_FILE'} does not exist, " . "but ENABLE_IPT_FORWARDING is enabled", $SEND_MAIL); $config{'ENABLE_IPT_FORWARDING'} = 'N'; } return; } sub get_iptables_chainmgr_obj() { my $ipt_sleep = shift; my %ipt_opts = ( 'iptables' => $cmds{'iptables'}, 'iptout' => $config{'IPT_OUTPUT_FILE'}, 'ipterr' => $config{'IPT_ERROR_FILE'}, 'ipt_alarm' => $config{'IPT_CMD_ALARM'}, 'ipt_exec_style' => $config{'IPT_EXEC_STYLE'}, 'sigchld_handler' => \&REAPER ); $ipt_opts{'debug'} = 1 if $debug; $ipt_opts{'verbose'} = 1 if $verbose and not $test_mode; $ipt_opts{'ipt_exec_sleep'} = $ipt_sleep if $ipt_sleep > 0; my $ipt = new IPTables::ChainMgr(%ipt_opts) or die '[*] Could not acquire IPTables::ChainMgr object.'; return $ipt; } sub die_handler() { $die_msg = shift; return; } ### write all warnings to a logfile sub warn_handler() { $warn_msg = shift; return; } sub write_die_msg() { open D, ">> $config{'FWKNOP_ERR_DIR'}/fwknopd.die" or die "[*] Could not open $config{'FWKNOP_ERR_DIR'}/fwknopd.die: $!"; print D scalar localtime(), " fwknopd v$version (file " . "rev: $rev_num) pid: $$ $die_msg"; close D; $die_msg = ''; return; } sub write_warn_msg() { open D, ">> $config{'FWKNOP_ERR_DIR'}/fwknopd.warn" or die "[*] Could not open $config{'FWKNOP_ERR_DIR'}/fwknopd.warn: $!"; print D scalar localtime(), " fwknopd v$version (file " . "rev: $rev_num) pid: $$ $warn_msg"; close D; $warn_msg = ''; return; } sub REAPER { my $pid; while(($pid = waitpid(-1,WNOHANG)) > 0) { # could add code to something with the borked pid here } $SIG{'CHLD'} = \&REAPER; return; } sub is_digit() { my $str = shift; return 1 if $str =~ /^\d+$/; return 0; } sub base64_equals_padding() { my $msg = shift; my $padding = ''; if ($debug) { print STDERR localtime() . " [+] base64_equals_padding() msg len: " . length($msg) . "\n"; } return 1, $padding if $msg =~ /=$/; ### base64 encoding pads encoded data to a multiple of four ### with '=' chars, but the fwknop client strips these out ### before sending to make it more difficult to detect SPA ### traffic my $remainder = 4 - length($msg) % 4; if ($remainder == 3) { ### not possible for valid base64 data - should only have ### pad with one or two '=' chars print STDERR localtime() . " [-] base64_equals_padding() ", "msg would require three '=' chars - invalid base64 data\n" if $debug; return 0, $padding; } unless ($remainder == 4) { $padding .= '='x$remainder; } return 1, $padding; } sub null_func() { return; } sub collect_warn_die_msgs() { &write_die_msg() if $die_msg; &write_warn_msg() if $warn_msg; return; } sub required_vars() { for my $var qw(FW_DATA_FILE SLEEP_INTERVAL FWKNOP_DIR FWKNOP_PID_FILE KNOPMD_PID_FILE KNOPWATCHD_PID_FILE FWKNOP_CMDLINE_FILE P0F_FILE ACCESS_CONF MAX_HOPS EMAIL_ADDRESSES ALERTING_METHODS IPT_INPUT_ACCESS IPT_FORWARD_ACCESS AUTH_MODE PCAP_CMD_TIMEOUT ENABLE_PCAP_PROMISC PCAP_FILTER KNOPTM_IP_TIMEOUT_SOCK ENABLE_DIGEST_PERSISTENCE DIGEST_FILE FLUSH_IPT_AT_INIT PCAP_INTF FWKNOP_ERR_DIR FWKNOP_RUN_DIR FWKNOP_LIB_DIR ENABLE_TCP_SERVER TCPSERV_PORT TCPSERV_PID_FILE IPT_OUTPUT_FILE IPT_ERROR_FILE ENABLE_SPA_PACKET_AGING MAX_SPA_PACKET_AGE REQUIRE_SOURCE_ADDRESS KNOPTM_IPT_OUTPUT_FILE KNOPTM_IPT_ERROR_FILE FIREWALL_TYPE IPFW_RULE_NUM SYSLOG_IDENTITY SYSLOG_FACILITY SYSLOG_PRIORITY MIN_GNUPG_MSG_SIZE ENABLE_DIGEST_INCLUDE_SRC ENABLE_COOKED_INTF ENABLE_VOLUNTARY_EXITS EXIT_INTERVAL KNOPTM_SYSLOG_IDENTITY KNOPTM_SYSLOG_FACILITY KNOPTM_SYSLOG_PRIORITY IPFW_SET_NUM ENABLE_IPT_FORWARDING ENABLE_IPT_OUTPUT IPT_OUTPUT_ACCESS IPT_DNAT_ACCESS IPT_SNAT_ACCESS IPT_MASQUERADE_ACCESS BLACKLIST SNAT_TRANSLATE_IP PROC_IP_FORWARD_FILE ENABLE_PROC_IP_FORWARD MIN_SPA_PKT_LEN ENABLE_IPT_LOCAL_NAT LOCALE ENABLE_SYSLOG_FILE IPT_SYSLOG_FILE FWKNOP_MOD_DIR MAX_SNIFF_BYTES IPT_CMD_ALARM IPT_EXEC_STYLE IPT_EXEC_SLEEP IPT_EXEC_TRIES EXTERNAL_CMD_OPEN EXTERNAL_CMD_CLOSE EXTERNAL_CMD_ALARM ENABLE_EXT_CMD_PREFIX EXT_CMD_PREFIX ENABLE_EXTERNAL_CMDS ENABLE_SPA_OVER_HTTP IPFW_DYNAMIC_INTERVAL ENABLE_INTF_CHECKS INTF_CHECKS_INTERVAL ENABLE_INTF_RUNNING_CHECK ENABLE_INTF_EXISTS_CHECK ENABLE_INTF_BYTES_CHECK ENABLE_FKO_MODULE FWKNOP_SERV_SOCK ENABLE_UDP_SERVER UDPSERV_PORT ) { die "[*] Required variable $var is not defined in $config_file" unless defined $config{$var}; } return; } sub usage() { my $exit_status = shift; print <<_HELP_; fwknopd - Single Packet Authorization daemon [+] Version: $version (file revision: $rev_num) By Michael Rash (mbr\@cipherdyne.org) URL: http://www.cipherdyne.org/fwknop/ Usage: fwknopd [options] Options: -c, --config - Specify path to config file instead of using the default path: $config_file -a, --access-conf - Specify path to access.conf file. -i, --intf - Manually specify interface on which to sniff. -T, --Test-mode - Run in testing mode for compatibility with the fwknop test suite (sets the PCAP_FILTER var to a standard default). --fw-list - List all active fwknop firewall rules. --fw-flush - Flush all active fwknop firewall rules. --fw-del-chains - Delete all fwknop iptables chains (must also use --fw-flush). --fw-del-ip - Delete accept or allow rules from the firewall policy. --fw-type - Manually specify the firewall type from the command line (usually only used by the fwknop test suite). -C, --Count - Exit after processing SPA packets. -O, --Override-config - Allow config variables from the normal $config_file to be superseded with values from the specified file(s). -K, --Kill - Kill all running fwknopd processes. -R, --Restart - Restart all running fwknopd processes. -S, --Status - Displays the status of any currently running fwknopd processes. --gpg-agent-info - Specify the value for the GPG_AGENT_INFO environment variable as returned by 'gpg-agent --daemon'. --gpg-no-options - In GnuPG mode, instruct GnuPG to not use the local ~/.gnupg/options file for config parameters. --no-gpg - Disable all GnuPG usages even if GPG_* variables are defined in the access.conf file. -I, --Include-all-config - Show all configuration data (including key information) when running in --debug and --verbose mode. --Linux-cooked-intf - Force fwknopd to assume that the sniffing interface is a "Linux Cooked" interface. This is useful when fwknopd uses a version of Net::Pcap that does not implement the pcap_datalink_val_to_name() function or have the pcap_datali.al file. -o, --os - Parse iptables logs and fingerprint operating systems from which tcp SYN packets have been logged. --fw-log - Specify path to iptables logfile. This is used only when running in --os mode. --Lib-dir - Path to the perl modules directory (not usually necessary). -d, --debug - Run fwknopd in debugging mode. --locale - Manually define a locale setting. --no-locale - Don't set the locale to anything (the default is the "C" locale from the LOCALE variable in the fwknop.conf file). --no-FKO-module - Revert to older perl implementation even if the FKO module is installed. --test-FKO-exists - See if the FKO module is available to use and exit (this is used by the fwknop test suite). -v, --verbose - Verbose mode. -V, --Version - Display version and exit. -h, --help - Print help and exit. _HELP_ exit $exit_status; }