diff --git a/pandora_agents/pc/Linux/pandora_agent.conf b/pandora_agents/pc/Linux/pandora_agent.conf index 0a3a48950c..6284c748f9 100644 --- a/pandora_agents/pc/Linux/pandora_agent.conf +++ b/pandora_agents/pc/Linux/pandora_agent.conf @@ -300,3 +300,7 @@ module_plugin grep_log /var/log/syslog Syslog ssh #module_exec echo 5 #module_description Postcondition test module #module_end + +# This plugin runs several security checks in a Linux system + +#module_plugin pandora_security_check \ No newline at end of file diff --git a/pandora_agents/unix/plugins/pandora_security_check b/pandora_agents/unix/plugins/pandora_security_check new file mode 100644 index 0000000000..0ad845639b --- /dev/null +++ b/pandora_agents/unix/plugins/pandora_security_check @@ -0,0 +1,631 @@ +#!/usr/bin/perl +################################################################################ +# Author: Enrique Martin Garcia +# Copyright: 2023, PandoraFMS +# Maintainer: Operations department +# Version: 1.0 +################################################################################ + +use strict; +use warnings; + +use Getopt::Long; +use File::Basename; +use File::Spec; +use Digest::MD5 qw(md5_hex); +use Scalar::Util 'looks_like_number'; +use Socket; + +# Define signal handlers +sub sigint_handler { + print STDERR "\nInterrupted by user\n"; + exit 0; +} + +sub sigterm_handler { + print STDERR "Received SIGTERM signal.\n"; + exit 0; +} + +$SIG{INT} = \&sigint_handler; +$SIG{TERM} = \&sigterm_handler; + +# Add lib dir path +my $lib_dir = File::Spec->catdir(dirname($0), 'lib'); +unshift @INC, $lib_dir; + +### +# GLOBALS +################## + +my %options = (); + +my $modules_group = 'Security'; + +my $b_ports = 'PORTS'; +my $b_files = 'FILES'; +my $b_passwords = 'PASSWORDS'; + +my @blocks = ($b_ports, $b_files, $b_passwords); +my $configuration_block; + +my $integrity_file = '/tmp/' . md5_hex(File::Spec->rel2abs($0)) . '.integrity'; + +# Enable all checks by default +my $check_selinux = 1; +my $check_ssh_root_access = 1; +my $check_ssh_root_keys = 1; +my $check_ports = 1; +my $check_files = 1; +my $check_passwords = 1; + +# Include all values for checks by default +my $include_defaults = 1; + +# Initialize check lists +my @l_ports = ( + 80, + 22 +); +my @l_files = ( + '/etc/shadow', + '/etc/passwd', + '/etc/hosts', + '/etc/resolv.conf', + '/etc/ssh/sshd_config', + '/etc/rsyslog.conf' +); + +my @l_passwords = ( + '123456', + '12345678', + '123456789', + '12345', + '1234567', + 'password', + '1password', + 'abc123', + 'qwerty', + '111111', + '1234', + 'iloveyou', + 'sunshine', + 'monkey', + '1234567890', + '123123', + 'princess', + 'baseball', + 'dragon', + 'football', + 'shadow', + 'soccer', + 'unknown', + '000000', + 'myspace1', + 'purple', + 'fuckyou', + 'superman', + 'Tigger', + 'buster', + 'pepper', + 'ginger', + 'qwerty123', + 'qwerty1', + 'peanut', + 'summer', + '654321', + 'michael1', + 'cookie', + 'LinkedIn', + 'whatever', + 'mustang', + 'qwertyuiop', + '123456a', + '123abc', + 'letmein', + 'freedom', + 'basketball', + 'babygirl', + 'hello', + 'qwe123', + 'fuckyou1', + 'love', + 'family', + 'yellow', + 'trustno1', + 'jesus1', + 'chicken', + 'diamond', + 'scooter', + 'booboo', + 'welcome', + 'smokey', + 'cheese', + 'computer', + 'butterfly', + '696969', + 'midnight', + 'princess1', + 'orange', + 'monkey1', + 'killer', + 'snoopy ', + 'qwerty12 ', + '1qaz2wsx ', + 'bandit', + 'sparky', + '666666', + 'football1', + 'master', + 'asshole', + 'batman', + 'sunshine1', + 'bubbles', + 'friends', + '1q2w3e4r', + 'chocolate', + 'Yankees', + 'Tinkerbell', + 'iloveyou1', + 'abcd1234', + 'flower', + '121212', + 'passw0rd', + 'pokemon', + 'StarWars', + 'iloveyou2', + '123qwe', + 'Pussy', + 'angel1' +); + +### +# ARGS PARSER +################## + +my $HELP = <] + [--conf ] + +Optional arguments: + -h, --help Show this help message and exit + --check_selinux {0,1} Enable/Disable check SElinux module + --check_ssh_root_access {0,1} Enable/Disable check SSH root access module + --check_ssh_root_keys {0,1} Enable/Disable check SSH root keys module + --check_ports {0,1} Enable/Disable check ports module + --check_files {0,1} Enable/Disable check files module + --check_passwords {0,1} Enable/Disable check passwords module + --include_defaults {0,1} Enable/Disable default plugin checks for ports, files and passwords + --integrity_file Path to integrity check file + Default: $integrity_file + --conf Path to plugin configuration file + Available configuration blocks: + [$b_ports], [$b_files] and [$b_passwords] + Content example: + [$b_ports] + 3306 + 443 + [$b_files] + /etc/httpd/httpd.conf + /etc/my.cnf + [$b_passwords] + pandora + PANDORA + P4nd0r4 + +EO_HELP + +sub help { + my ($extra_message) = @_; + print $HELP; + print $extra_message if defined($extra_message); + exit 0; +} + +sub parse_bool_arg { + my ($arg, $default) = @_; + + if (defined $options{$arg}) { + if (looks_like_number($options{$arg}) && ($options{$arg} == 1 || $options{$arg} == 0)) { + return $options{$arg}; + } else { + help("Invalid value for argument: $arg\n"); + } + } else { + return $default; + } +} + +# Parse arguments +GetOptions( + "help|h" => \$options{help}, + "check_selinux=s" => \$options{check_selinux}, + "check_ssh_root_access=s" => \$options{check_ssh_root_access}, + "check_ssh_root_keys=s" => \$options{check_ssh_root_keys}, + "check_ports=s" => \$options{check_ports}, + "check_files=s" => \$options{check_files}, + "check_passwords=s" => \$options{check_passwords}, + "include_defaults=s" => \$options{include_defaults}, + "integrity_file=s" => \$options{integrity_file}, + "conf=s" => \$options{conf} +); + +help() if ($options{help}); + +$check_selinux = parse_bool_arg('check_selinux', $check_selinux); +$check_ssh_root_access = parse_bool_arg('check_ssh_root_access', $check_ssh_root_access); +$check_ssh_root_keys = parse_bool_arg('check_ssh_root_keys', $check_ssh_root_keys); +$check_ports = parse_bool_arg('check_ports', $check_ports); +$check_files = parse_bool_arg('check_files', $check_files); +$check_passwords = parse_bool_arg('check_passwords', $check_passwords); + +$include_defaults = parse_bool_arg('include_defaults', $include_defaults); + +if (!$include_defaults) { + @l_ports = (); + @l_files = (); + @l_passwords = (); +} + +$integrity_file = $options{integrity_file} if defined $options{integrity_file}; + +parse_configuration($options{conf}) if defined $options{conf}; + +### +# FUNCTIONS +################## + +# Function to parse configuration file +sub parse_configuration { + my ($conf_file) = @_; + + open my $conf_fh, '<', $conf_file or die "Error opening configuration file [$conf_file]: $!\n"; + + while (my $line = <$conf_fh>) { + chomp $line; + $line =~ s/^\s+//; + $line =~ s/\s+$//; + + if ($line =~ /^\[($b_ports|$b_files|$b_passwords)\]$/) { + $configuration_block = $1; + } + elsif ($configuration_block) { + if ($configuration_block eq $b_ports) { + push @l_ports, $line; + } + elsif ($configuration_block eq $b_files) { + push @l_files, $line; + } + elsif ($configuration_block eq $b_passwords) { + push @l_passwords, $line; + } + } + } + + close $conf_fh; +} + +# Function to print module XML to STDOUT +sub print_xml_module { + my ($m_name, $m_type, $m_desc, $m_value) = @_; + + print "\n"; + print "\t\n"; + print "\t$m_type\n"; + print "\t\n"; + print "\t\n"; + print "\t$modules_group\n"; + print "\n"; +} + +# Make unique array +sub uniq { + my %seen; + return grep { !$seen{$_}++ } @_; +} + +### +# MAIN +################## + +# Check SELinux status +if ($check_selinux) { + my $value = 0; + my $desc = 'SELinux is disabled.'; + + my $output = `sestatus 2> /dev/null`; + if ($? == 0) { + if ($output =~ /SELinux status: enabled/) { + $value = 1; + $desc = 'SELinux is enabled.'; + } + } else { + $value = 0; + $desc = 'Can not determine if SELinux is enabled.'; + } + + print_xml_module('SELinux status', 'generic_proc', $desc, $value); +} + +# Check if SSH allows root access +if ($check_ssh_root_access) { + my $value = 1; + my $desc = 'SSH does not allow root access.'; + + my $ssh_config_file = '/etc/ssh/sshd_config'; + if (-e $ssh_config_file && open my $ssh_fh, '<', $ssh_config_file) { + while (my $line = <$ssh_fh>) { + chomp $line; + $line =~ s/^\s+//; + $line =~ s/\s+$//; + next if $line =~ /^$/ or $line =~ /^#/; + my ($option, $val) = split /\s+/, $line, 2; + if ($option eq 'PermitRootLogin' && lc($val) ne 'no') { + $value = 0; + $desc = 'SSH config allows root access.'; + last; + } + } + close $ssh_fh; + } else { + $value = 0; + $desc = 'Can not read '.$ssh_config_file.' to check if root access allowed.'; + } + + print_xml_module('SSH root access status', 'generic_proc', $desc, $value); +} + +# Specific function for recursive directory check +sub find_files { + my ($dir) = @_; + + my @files = (); + + opendir my $dh, $dir or return; + while (my $file = readdir $dh) { + next if $file eq '.' or $file eq '..'; + + my $file_path = File::Spec->catfile($dir, $file); + if (-f $file_path) { + push @files, $file_path; + } elsif (-d $file_path) { + push @files, find_files($file_path); + } + } + closedir $dh; + + return @files; +} + +# Check if /root has SSH keys +if ($check_ssh_root_keys) { + my $value = 1; + my $desc = 'SSH root keys not found.'; + + my $ssh_keys = {'private' => [], 'public' => []}; + + my $ssh_dir = '/root/.ssh'; + my @all_files = find_files($ssh_dir); + foreach my $file (@all_files) { + if (open my $fh, '<:raw', $file) { + my $content = ''; + while(my $l = <$fh>) { + $content .= $l; + } + if ($content) { + my ($filename, $directories) = fileparse($file); + if ($content =~ /-----BEGIN RSA PRIVATE KEY-----.*?-----END RSA PRIVATE KEY-----/s) { + push @{$ssh_keys->{'private'}}, $file; + } elsif ($content =~ /ssh-rsa/ && $filename ne 'known_hosts' && $filename ne 'authorized_keys') { + push @{$ssh_keys->{'public'}}, $file; + } + } + } + } + + if (@{$ssh_keys->{'private'}} > 0 || @{$ssh_keys->{'public'}} > 0) { + $value = 0; + $desc = "SSH root keys found:\n" . join("\n", @{$ssh_keys->{'private'}}, @{$ssh_keys->{'public'}}); + } + + print_xml_module('SSH root keys status', 'generic_proc', $desc, $value); +} + +# Check authorized ports +if ($check_ports) { + my $value = 1; + my $desc = 'No unauthorized ports found.'; + + my @open_ports; + my @not_allowed_ports; + + my @net_tcp_files = ('/proc/net/tcp', '/proc/net/tcp6'); + foreach my $net_tcp_file (@net_tcp_files) { + if (-e $net_tcp_file && open my $tcp_fh, '<', $net_tcp_file) { + while (my $line = <$tcp_fh>) { + chomp $line; + my @parts = split /\s+/, $line; + if (scalar @parts >= 12) { + my $local_addr_hex = (split /:/, $parts[2])[0]; + my $local_port_hex = (split /:/, $parts[2])[1]; + my $state = $parts[4]; + + # Check if the connection is in state 0A (listening) + if ($state eq "0A") { + my $local_addr_4 = join('.', reverse split(/\./, inet_ntoa(pack("N", hex($local_addr_hex))))); + my $local_addr_6 = join(':', map { hex($_) } unpack("(A4)*", $local_addr_hex)); + + # Skip localhost listening ports + if ($local_addr_4 eq "127.0.0.1" || $local_addr_6 eq "0:0:0:0:0:0:0:1") { + next; + } + + my $local_port = hex($local_port_hex); + push @open_ports, $local_port; + } + } + } + close $tcp_fh; + } + } + @open_ports = uniq(@open_ports); + + my %allowed_ports; + foreach my $port (@l_ports) { + $allowed_ports{$port} = 1; + } + + foreach my $port (@open_ports) { + if (!exists $allowed_ports{$port}) { + push @not_allowed_ports, $port; + } + } + + if (@not_allowed_ports) { + $value = 0; + $desc = "Unauthorized ports found:\n" . join("\n", @not_allowed_ports); + } + + print_xml_module('Authorized ports status', 'generic_proc', $desc, $value); +} + +# Check files integrity +if ($check_files) { + my $value = 1; + my $desc = 'No changed files found.'; + + my %integrity; + + my $can_check_files = 0; + + if (-e $integrity_file) { + if (-r $integrity_file && -w $integrity_file) { + # Read integrity file content + open my $integrity_fh, '<', $integrity_file; + while (my $line = <$integrity_fh>) { + chomp $line; + if ($line =~ /^\s*(.*?)=(.*?)\s*$/) { + $integrity{$1} = $2; + } + } + close $integrity_fh; + $can_check_files = 1; + } else { + $value = 0; + $desc = 'Integrity check file can not be read or written: ' . $integrity_file; + } + } else { + if (open my $integrity_fh, '>', $integrity_file) { + close $integrity_fh; + $can_check_files = 1; + } else { + $value = 0; + $desc = 'Integrity check file can not be created: ' . $integrity_file; + } + } + + if ($can_check_files) { + # Check each file integrity + my @errored_files; + my @no_integrity_files; + + # Create unique check files list + @l_files = uniq(@l_files); + + foreach my $file (@l_files) { + my $file_key = md5_hex($file); + if (open my $fh, '<:raw', $file) { + my $md5 = Digest::MD5->new; + $md5->addfile($fh); + my $file_md5 = $md5->hexdigest; + chomp $file_md5; + close $fh; + + if (exists $integrity{$file_key} && $integrity{$file_key} ne $file_md5) { + push @no_integrity_files, $file; + } + $integrity{$file_key} = $file_md5; + } else { + push @errored_files, $file; + } + } + + # Overwrite integrity file content + open my $file_handle, '>', $integrity_file; + print $file_handle map { "$_=$integrity{$_}\n" } keys %integrity; + close $file_handle; + + # Check module status + if (@no_integrity_files) { + $value = 0; + $desc = "Changed files found:\n" . join("\n", @no_integrity_files); + } + + if (@errored_files) { + $value = 0; + $desc .= "\nUnable to check integrity of some files:\n" . join("\n", @errored_files); + } + } + + print_xml_module('Files check status', 'generic_proc', $desc, $value); +} + +# Check weak passwords +if ($check_passwords) { + my $value = 1; + my $desc = 'No insecure passwords found.'; + + # Create unique check passwords list + @l_passwords = uniq(@l_passwords); + + my @insecure_users; + + my $shadow_file = '/etc/shadow'; + if (-e $shadow_file && -r $shadow_file) { + open my $shadow_fh, '<', $shadow_file; + while (my $line = <$shadow_fh>) { + chomp $line; + my ($username, $password_hash, @rest) = split /:/, $line; + + # Skip users with no password hash + if ($password_hash ne "*" && $password_hash ne "!!" && $password_hash ne "!locked") { + my $salt = substr($password_hash, 0, rindex($password_hash, '$') + 1); + my $user_hash = crypt($username, $salt); + if ($user_hash eq $password_hash) { + push @insecure_users, $username; + } else { + foreach my $weak_password (@l_passwords) { + my $weak_password_hash = crypt($weak_password, $salt); + + if ($weak_password_hash eq $password_hash) { + push @insecure_users, $username; + last; + } + } + } + } + } + close $shadow_fh; + } else { + $value = 0; + $desc = 'Can not read '.$shadow_file.' to check passwords.'; + } + + if (@insecure_users) { + $value = 0; + $desc = "Users with insecure passwords found:\n" . join("\n", @insecure_users); + } + + print_xml_module('Insecure passwords status', 'generic_proc', $desc, $value); +} \ No newline at end of file