#!/usr/bin/perl # This chunk of stuff was generated by App::FatPacker. To find the original # file's code, look for the end of this BEGIN block or the string 'FATPACK' BEGIN { my %fatpacked; $fatpacked{"SSP/Crit.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'SSP_CRIT'; #!/usr/bin/perl # SSP - System Status Probe (Crit module) # Find and print useful troubleshooting info on cPanel servers =head1 COPYRIGHT This software is Copyright 2024 WebPros International, LLC. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THE SOFTWARE LICENSED HEREUNDER IS PROVIDED "AS IS" AND WEBPROS INTERNATIONAL, LLC D/B/A/ CPANEL (CPANEL) HEREBY DISCLAIMS ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, RELATING TO THE SOFTWARE, ITS THIRD PARTY COMPONENTS, AND ANY DATA ACCESSED THEREFROM, OR THE ACCURACY, TIMELINESS, COMPLETENESS, OR ADEQUACY OF THE SOFTWARE, ITS THIRD PARTY COMPONENTS, AND ANY DATA ACCESSED THEREFROM, INCLUDING THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. CPANEL DOES NOT WARRANT THAT THE SOFTWARE OR ITS THIRD PARTY COMPONENTS ARE ERROR-FREE OR WILL OPERATE WITHOUT INTERRUPTION. IF THE SOFTWARE, ITS THIRD PARTY COMPONENTS, OR ANY DATA ACCESSED THEREFROM IS DEFECTIVE, YOU ASSUME THE SOLE RESPONSIBILITY FOR THE ENTIRE COST OF ALL REPAIR OR INJURY OF ANY KIND, EVEN IF CPANEL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DEFECTS OR DAMAGES. NO ORAL OR WRITTEN INFORMATION OR ADVICE GIVEN BY CPANEL, ITS AFFILIATES, LICENSEES, DEALERS, SUB-LICENSORS, AGENTS OR EMPLOYEES SHALL CREATE A WARRANTY OR IN ANY WAY INCREASE THE SCOPE OF ANY WARRANTY. =cut package SSP::Crit; use 5.006; use strict; use File::stat; use warnings; our %CPCONF; sub run { my $self = shift; SSP::Util::init(); if ( SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ) ) { %CPCONF = SSP::Util::get_cpanel_conf(); } $self->check_sysinfo() unless ( SSP::Util::cpanel_version_is(qw( >= 11.100.0.0)) ); $self->check_mysql_skip_grants(); $self->check_for_quota_failed_touch_file(); $self->check_for_root_quota(); $self->check_for_pm2_god_daemon(); } sub check_for_multiple_tech_logins { return unless SSP::Util::i_am('cptech'); # Prefer 'who' over 'w' because of FROM field length limit in 'w' # who -H #NAME LINE TIME COMMENT #root pts/0 2014-07-29 07:24 (192.168.130.1) # we can sometimes get additional text after the IP or hostname #root pts/2 2014-08-07 07:17 (208.74.121.102:S.0) my $who = '/usr/bin/who'; return if !-x $who; my @tech_logins = (); my $header = ""; my $num_logins = 0; for my $line ( split /\n/, SSP::Util::timed_run( 0, $who, '-H' ) ) { if ( $line =~ m{ \A NAME\s+ }xms ) { $header = $line; next; } if ( $line =~ m{ \((.+)\)\Z }xms ) { if ( $1 =~ m{ \A (.*\.)?(cptxoffice\.net|cloudlinux\.com|litespeedtech.com)(:|$) }xms || $1 =~ m{ \A (208\.74\.123\.98|184\.94\.197\.[2-6])(:|$) }xms ) { push( @tech_logins, $line ); $num_logins++; } } } return if $num_logins <= 1; SSP::Util::print_critical(); SSP::Util::print_crit('Multiple tech SSH sessions are active (run "ls /var/cpanel/users/ |grep cptkt" for a complete list of ticket users):'); SSP::Util::print_critical("\n"); SSP::Util::print_critical($header) if $header; SSP::Util::print_critical( join( "\n", @tech_logins ) ); SSP::Util::print_critical(); } sub check_for_lve_environment { return if SSP::Util::i_am('clsolo'); my $hostinfo = SSP::Util::get_hostinfo_href(); # pam_lve 0.2 prints this after su or sudo: # # # /bin/su - # Password: # *************************************************************************** # * * # * !!!! WARNING: YOU ARE INSIDE LVE !!!! * # *IF YOU RESTART ANY SERVICES STABILITY OF YOUR SYSTEM WILL BE COMPROMIZED * # * CHANGE UID OF THE USER YOU ARE USING TO SU/SUDO * # * MORE INFO: * # *http://www.cloudlinux.com/blog/clnews/read-this-if-you-use-su-or-sudo.php* # * * # *************************************************************************** # pam_lve 0.3 won't put wheel users in an LVE after su or sudo: # http://cloudlinux.com/blog/clnews/read-this-if-you-use-su-or-sudo.php #if ( $hostinfo->{'kernel'} =~ /\.lve/ and -x '/usr/sbin/lveps' ) { if ( $hostinfo->{'isLVE'} and -x '/usr/sbin/lveps' ) { if (`/usr/sbin/lveps -p | grep " $$ "`) { ## no critic (Cpanel::ProhibitQxAndBackticks) SSP::Util::print_critical(); SSP::Util::print_crit(" You are inside a CloudLinux LVE - DO *NOT* RESTART ANY SERVICES!\n"); SSP::Util::print_critical(" \\_ The pam_lve configuration may not be excluding the wheel group, or your ssh login user was not in the wheel group."); SSP::Util::print_critical(" \\_ http://docs.cloudlinux.com/index.html?lve_pam_module.html"); SSP::Util::print_critical(); } } } sub check_for_os_release_5 { return unless SSP::Util::os_version_is(qw( < 6 )); SSP::Util::print_crit('CentOS/RHEL/CL 5 (or older): '); SSP::Util::print_critical('This operating system is not supported in WHM 58 and later (OS version 6+ only).'); SSP::Util::print_critical(' \_ Send customer this premade: "MIGRATION - CentOS/RHEL/CL 5 EOL"'); } sub check_for_centos_8 { return unless ( SSP::Util::i_am('centos') && SSP::Util::os_version_is(qw( >= 8 )) ); SSP::Util::print_crit('CentOS/RHEL 8: '); SSP::Util::print_critical('This operating system is EOL as of 12/31/2021.'); SSP::Util::print_critical(' \_ Send customer the following macro "PREDEFS::EOL::CentOS 8"'); } sub check_for_os_release_32bit { my $hostinfo = SSP::Util::get_hostinfo_href(); return unless ( defined( $hostinfo->{'hardware'} ) && $hostinfo->{'hardware'} eq 'i386' ); return unless SSP::Util::os_version_is(qw( >= 6 )); # There is an unofficial CentOS 7 i386 build. SSP::Util::print_crit('CentOS/RHEL/CL i386 (32-bit): '); SSP::Util::print_critical('This operating system is not supported in WHM 58 and later (x86_64 only).'); SSP::Util::print_critical(' \_ Send customer this premade: "MIGRATION - 32-bit CentOS/RHEL/CL EOL"'); } sub check_for_ea3 { return unless SSP::Util::i_am('ea3'); my $hostinfo = SSP::Util::get_hostinfo_href(); SSP::Util::print_crit('EasyApache 3 DETECTED! '); SSP::Util::print_critical('Support has ended as of 2018. - ONLY SUPPORT TO EA4 IS PERMITTED ON THIS SERVER!'); SSP::Util::print_critical(' \_ Send customer this premade: "MIGRATION - EA3 EOL-FINAL"'); } sub check_for_root_quota { my $quota_output = SSP::Util::timed_run( 0, 'quota', 'root' ); return unless ($quota_output); return if ( grep { m/none|Cannot open quotafile|No such file or directory/ } $quota_output ); SSP::Util::print_crit('QUOTA ISSUE: '); SSP::Util::print_critical("root account has quotas enabled! Can cause many issues."); } sub check_sysinfo { return if ( SSP::Util::i_am('ubuntu') ); return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); return unless my $hostinfo = SSP::Util::get_hostinfo_href(); my $sysinfo_config = '/var/cpanel/sysinfo.config'; my $rebuild = 0; if ( !-e $sysinfo_config ) { SSP::Util::print_crit('sysinfo: '); SSP::Util::print_critical('does not exist, run /scripts/gensysinfo to fix'); } else { open my $sysinfo_fh, '<', $sysinfo_config; while (<$sysinfo_fh>) { chomp; if (m{ \A rpm_arch=(.*) }xms) { if ( $hostinfo->{'hardware'} ne $1 ) { $rebuild = 1; } } if (m{ \A release=(.*) }xms) { if ( SSP::Util::_get_run_var('os_version') ne $1 ) { $rebuild = 1; } } if (m{ \A ises=(.*) }xms) { if ( SSP::Util::_get_run_var('os_ises') ne $1 ) { $rebuild = 1; } } } close $sysinfo_fh; } if ( $rebuild == 1 ) { SSP::Util::print_crit('sysinfo: '); SSP::Util::print_critical('/var/cpanel/sysinfo.config contains errors -- run /scripts/gensysinfo to fix'); } } sub check_mysql_skip_grants { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); return unless SSP::Util::exists_process_cmd( qr{mysqld}xms, 'mysql' ); # Test with very unique password, if it works, 99% skip-grants is enabled my $fake_pass = 'KsA9tsRzaoa01dWl1dJBv9WtyA5Ahe7T2qrzVt7MPNa9IrdnEsRVI8raUUOtQ8Xls1SroikQy4gm1ohU'; my $ver = SSP::Util::timed_run( 0, 'mysql', '--user=root', '--password=' . $fake_pass, '-NBe', 'SELECT version()' ); if ($ver) { SSP::Util::print_crit('Database: '); SSP::Util::print_critical('Root database access was obtained with a random password, is \'skip-grant-tables\' enabled?'); SSP::Util::print_critical( "\t \\_ " . 'Verify \'skip-grant-tables\' is enabled and inform the client of the inherent risks of running database servers (such as MariaDB/MySQL) without grants' ); } } sub check_for_eula { return unless ( SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ) ); return unless ( -d '/var/cpanel/activate' ); my $EULA_PM_FILE = '/usr/local/cpanel/Whostmgr/Setup/EULA.pm'; if ( !-e $EULA_PM_FILE ) { $EULA_PM_FILE = '/usr/local/cpanel/Whostmgr/API/1/EULA.pm'; } my $EULA_VERSION; open( my $fh, '<', $EULA_PM_FILE ); while (<$fh>) { next unless $_ =~ m/WHMEULA/; ($EULA_VERSION) = ( split( /\'/, $_ ) )[1]; last; } close($fh); opendir( my $dir_fh, '/var/cpanel/activate' ) or return; my @dir_contents = grep { /CPWHMEULA|EULACPWHM/ } readdir $dir_fh; closedir $dir_fh; my $eula_tf_exists = grep { /$EULA_VERSION/ } @dir_contents; return 1 if ($eula_tf_exists); SSP::Util::print_crit('Latest EULA: '); SSP::Util::print_critical('has not yet been accepted! Please cease all work, logout, and send PREDEFS::TICKETS::EULA Not Accepted'); } sub check_ubuntu_release_value { my $file = '/etc/update-manager/release-upgrades'; return unless ( -f $file ); open( my $fh, '<', "$file" ); while (<$fh>) { next unless ( $_ =~ m/^Prompt=/ ); my ($release_val) = ( split( /=/, $_ ) )[1]; chomp($release_val); if ( $release_val ne "never" ) { SSP::Util::print_crit('Ubuntu Release Value '); SSP::Util::print_critical( 'is set to something other than "never". cPanel recommends this value is set to "never" in the ' . $file . ' file.' ); } } close($fh); } sub check_for_ubuntu_less_than_102 { return unless ( SSP::Util::i_am('ubuntu') ); return unless ( SSP::Util::cpanel_version_is(qw( < 11.102 )) ); SSP::Util::print_crit('Ubuntu '); SSP::Util::print_critical('is experimental in this version of cPanel & WHM. You need to be on 102+ to get support'); return; } sub check_for_quota_failed_touch_file { # For now, this is only going to be on Ubuntu servers running 102+ return unless ( SSP::Util::i_am('ubuntu') ); return unless ( SSP::Util::cpanel_version_is(qw( > 11.102 )) ); if ( -e '/var/cpanel/quota_broken' ) { SSP::Util::print_crit('Ubuntu Quota Issues: '); SSP::Util::print_critical('All automatic attempts to enable quotas have failed - See: https://go.cpanel.net/ubuntuquotas'); } } sub check_for_pm2_god_daemon { if ( SSP::Util::exists_process_cmd( qr{ God Daemon }xms, 'root' ) || ( -d '/root/.pm2' ) ) { SSP::Util::print_crit('Unsafe process running as root: '); SSP::Util::print_critical('pm2 process running as "God Daemon or hidden directory /root.pm2 found". Send PREDEF::Security::Unsafe_PM2'); } } sub check_for_failed_install { my $logfile = '/var/log/cpanel-install.log'; return unless ( -e $logfile ); my $fStat = stat($logfile); my $FileSize = $fStat->size; return if ( $FileSize > 10485760 ); ## CX-898 return if the log file is greater than 10MB. return if ( SSP::Util::timed_run( 0, 'grep', 'Thank you for installing cPanel & WHM', $logfile ) ); open( my $fh, '<', $logfile ); while (<$fh>) { chomp; if ( $_ =~ m{\(FATAL\)} ) { SSP::Util::print_crit('Possible cPanel install failure: '); SSP::Util::print_critical('cpanel-install.log contains FATAL errors, verify installation! All output below may be irrelevant!'); last; } } close($fh); } 1; SSP_CRIT $fatpacked{"SSP/Info.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'SSP_INFO'; #!/usr/bin/perl # SSP - System Status Probe (info module) # Find and print useful troubleshooting info on cPanel servers =head1 COPYRIGHT This software is Copyright 2024 WebPros International, LLC. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THE SOFTWARE LICENSED HEREUNDER IS PROVIDED "AS IS" AND WEBPROS INTERNATIONAL, LLC D/B/A/ CPANEL (CPANEL) HEREBY DISCLAIMS ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, RELATING TO THE SOFTWARE, ITS THIRD PARTY COMPONENTS, AND ANY DATA ACCESSED THEREFROM, OR THE ACCURACY, TIMELINESS, COMPLETENESS, OR ADEQUACY OF THE SOFTWARE, ITS THIRD PARTY COMPONENTS, AND ANY DATA ACCESSED THEREFROM, INCLUDING THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. CPANEL DOES NOT WARRANT THAT THE SOFTWARE OR ITS THIRD PARTY COMPONENTS ARE ERROR-FREE OR WILL OPERATE WITHOUT INTERRUPTION. IF THE SOFTWARE, ITS THIRD PARTY COMPONENTS, OR ANY DATA ACCESSED THEREFROM IS DEFECTIVE, YOU ASSUME THE SOLE RESPONSIBILITY FOR THE ENTIRE COST OF ALL REPAIR OR INJURY OF ANY KIND, EVEN IF CPANEL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DEFECTS OR DAMAGES. NO ORAL OR WRITTEN INFORMATION OR ADVICE GIVEN BY CPANEL, ITS AFFILIATES, LICENSEES, DEALERS, SUB-LICENSORS, AGENTS OR EMPLOYEES SHALL CREATE A WARRANTY OR IN ANY WAY INCREASE THE SCOPE OF ANY WARRANTY. =cut package SSP::Info; use 5.006; use strict; use warnings; use Term::ANSIColor qw(:constants); $Term::ANSIColor::AUTORESET = 1; use File::Find; our $OPT_SKIP_NETWORKING; # Disable network calls our $ORIGINAL_PATH; our $CPANEL_VERSION_FILE = '/usr/local/cpanel/version'; our %CPCONF; # cpanel.config our %MEMOIZE_CACHE; our $OPT_TIMEOUT; our $RUN_STATE; sub run { my $self = shift; SSP::Util::init(); if ( SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ) ) { %CPCONF = SSP::Util::get_cpanel_conf(); } $self->print_hostname(); $self->print_os(); $self->check_for_ubuntu_cloudlinux(); $self->check_for_ubuntu_upgradable(); $self->print_kernel_and_cpu(); $self->print_kernelcare_info(); $self->check_if_reboot_needed(); $self->print_cpanel_info(); $self->check_for_cpanel_update(); $self->check_for_elevate(); $self->check_lts_autofixer(); $self->print_uptime(); $self->check_for_license_info(); $self->check_for_cpcloud_instance(); $self->check_for_linked_server_nodes(); $self->check_for_systemd(); $self->get_autossl_provider(); $self->print_apache_info(); $self->check_for_phpini_directives(); $self->print_lsws_info(); $self->print_ea4_php_configuration(); $self->check_for_clustering(); $self->check_for_remote_mysql(); $self->print_nameserver_daemon(); $self->check_for_unbound_stub_resolver(); $self->check_for_nginx_unlimited_memory_touchfile(); $self->check_for_exim_ipv6_sort_bias_touchfile(); $self->check_for_public_resolvers(); $self->print_mysql_version(); $self->check_if_mysqlupgrade_is_running(); $self->print_backups_info(); $self->check_metadata_vacuum(); $self->print_mailserver_info(); $self->print_ftpserver_info(); $self->print_exim_info(); $self->print_roundcube_db(); $self->check_for_custom_webtemplates(); $self->check_for_custom_restoremodules(); $self->check_for_custom_zonetemplates(); $self->check_selinux_status(); $self->check_imunify_config(); } ############################## # BEGIN [INFO] CHECKS ############################## sub print_hostname { my $hostname = SSP::Util::get_hostname(); my $hostname_resolves = SSP::Util::check_hostname_resolution($hostname); my $hostname_multiple = SSP::Util::check_if_hostname_has_multiple_a_records($hostname); my $external_ip_address = SSP::Util::get_external_ip(); my $ssl_info = ( SSP::Util::get_hostname_ssl_info() ) ? SSP::Util::get_hostname_ssl_info() : ""; SSP::Util::print_info('Hostname: '); SSP::Util::print_normal( $hostname . $ssl_info ); SSP::Util::print_warning(qq{\t\\_ too long. More than 60 characters will cause issues with databases}) if ( length($hostname) > 60 ); SSP::Util::print_warning(qq{\t\\_ may not be a FQDN ( https://support.cpanel.net/hc/en-us/articles/360044460594 )}) if ( $hostname !~ /(([\w-]+)\.([\w-]+)){1,2}(\.(\w+)){1,2}/ ); SSP::Util::print_warning(qq{\t\\_ $hostname does not resolve to licensed IP address [$external_ip_address]}) if ( !$hostname_resolves ); SSP::Util::print_warning(qq{\t\\_ Multiple A records found and Cookie IP Validation strict is set - Seeing Token Mismatch errors in PHPMyAdmin?}) if ($hostname_multiple); } sub print_os { return unless my $hostinfo = SSP::Util::get_hostinfo_href(); my $install_info = ''; if ( defined $hostinfo->{'installtime'} ) { $install_info = ' [ Installed: ' . $hostinfo->{'installtime'} . ' ]'; } SSP::Util::print_info('OS: '); my $clsolo_description = ( SSP::Util::i_am('clsolo') ) ? " (CloudLinux Solo)" : ""; SSP::Util::print_normal( SSP::Util::_get_run_var('os_release') . $clsolo_description . BOLD CYAN ' [ ' . $hostinfo->{'environment'} . ' ]' . $install_info ); my $cache_dir = '/var/cpanel/caches/Cpanel-OS'; return if ( !-d $cache_dir ); my $CpanelOS = SSP::Util::timed_run( 2, 'ls', '-l', $cache_dir ); my $experimentalOS = ( grep { /\|2020/ } $CpanelOS ) ? SSP::Util::print_warning(qq{\t\\_ experimental-os flag was passed at install! - Unsupported and unexpected results may occur!}) : ""; print $experimentalOS; } sub check_for_ubuntu_upgradable { return unless ( SSP::Util::i_am('ubuntu') ); my $list_upgradable = SSP::Util::timed_run( 5, 'apt', 'list', '--upgradable' ); my @list_upgradable = split /\n/, $list_upgradable; my $cnt = 0; foreach my $upgradable_pkg (@list_upgradable) { chomp($upgradable_pkg); $cnt++ if ( $upgradable_pkg =~ m/upgradable from/ ); } SSP::Util::print_normal("\t\\_ There are $cnt packages that are upgradable - Send PREDEF::Ubuntu::has_upgradable_packages") unless ( $cnt == 0 ); } sub check_for_ubuntu_cloudlinux { return unless ( SSP::Util::i_am('ubuntu') ); return unless ( -s '/opt/cloudlinux/cl_edition' ); SSP::Util::print_info('CloudLinux Subsystem Found: '); SSP::Util::print_normal("Features such as LVE, CageFS, alt-php may be installed on this Ubuntu server."); } sub print_kernel_and_cpu { return unless my $hostinfo = SSP::Util::get_hostinfo_href(); return unless my $cpuinfo = SSP::Util::get_cpuinfo_href(); my ($hasLVE) = ( $hostinfo->{isLVE} ) ? "[LVE]" : "\b"; SSP::Util::print_info('Kernel/CPU: '); SSP::Util::print_normal("$hostinfo->{'kernel'} $hasLVE $hostinfo->{'hardware'} $hostinfo->{'environment'} $cpuinfo->{'model'} w/ $cpuinfo->{'numcores'} core(s)"); if ( SSP::Util::missing_open_opath_flag() ) { SSP::Util::print_warning("\t \\_ This kernel does not support O_PATH flag with open() which can cause service failure with systemd, see UPS-177"); } if ( $hostinfo->{'environment'} eq 'virtuozzo' && $hostinfo->{'kernel'} eq '2.6.32-042stab113.11' ) { SSP::Util::print_warning("\t \\_ This kernel has broken quota support [ https://bugs.openvz.org/browse/OVZ-6661 ]"); } } sub print_kernelcare_info { return unless SSP::Util::i_am('kernelcare'); my $kcarectl_path = '/usr/bin/kcarectl'; my $kcarectl_info = "Installed"; my $license_output; my $hasImunify360; my $patchset_output; my $uname_output; if ( -x $kcarectl_path ) { chomp( $license_output = SSP::Util::timed_run( 0, $kcarectl_path, '--license-info' ) ); if ( $license_output =~ /Valid license found/ ) { $kcarectl_info .= ' and licensed'; } chomp( $patchset_output = SSP::Util::timed_run( 0, $kcarectl_path, '--info' ) ); if ( $patchset_output =~ m/free/ ) { $kcarectl_info .= ' (Free patchset detected)'; } else { if ( -x '/usr/bin/imunify360-agent' ) { my $i360Version = SSP::Util::timed_run( 8, '/usr/bin/imunify360-agent', 'version', '--json' ); $hasImunify360 = ( grep { /imunify360/ } $i360Version ); } if ($hasImunify360) { $kcarectl_info .= ' (licensed via Imunify360)'; } else { $kcarectl_info .= ' (license not detected)'; } } chomp( $uname_output = SSP::Util::timed_run( 0, $kcarectl_path, '--uname' ) ); if ( ( $uname_output =~ /^\d+\.\d+\.\d+/ ) && ( $uname_output !~ /\n/ ) ) { $kcarectl_info .= ' [ ' . $uname_output . ' ]'; } } SSP::Util::print_info('KernelCare: '); SSP::Util::print_normal($kcarectl_info); } sub check_if_reboot_needed { return unless ( SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ) ); my $raw = SSP::Util::timed_run( 6, '/sbin/whmapi1', 'system_needs_reboot', '--output=json' ); my $json_output = SSP::Util::get_json_href($raw); if ( $json_output->{data}->{needs_reboot} ) { SSP::Util::print_info('Reboot Needed: '); SSP::Util::print_normal("A reboot is recommended as there are updates that haven't been installed yet."); } } sub print_cpanel_info { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly', 'wp2' ); my $cpupdate_conf = SSP::Util::get_cpupdate_conf(); my $whm_info = SSP::Util::get_whm_install_info(); my $cpanel_tier = defined $cpupdate_conf->{CPANEL} ? $cpupdate_conf->{CPANEL} : 'Unknown (could not open/read /etc/cpupdate.conf ?)'; my $last_update = defined $whm_info->{'lastupdatetime_epoch'} ? sprintf '%.1f', ( time() - $whm_info->{'lastupdatetime_epoch'} ) / 86400 : 'UNKNOWN'; my $birthday = defined $whm_info->{'installversion'} ? $whm_info->{'installversion'} . ' on ' : ''; $birthday .= defined $whm_info->{'installtime'} ? $whm_info->{'installtime'} : ''; my $product = "cPanel"; $product = "WP2" if ( SSP::Util::i_am('wp2') ); my $output = SSP::Util::_get_run_var('cpanel_original_version') . ' (' . uc($cpanel_tier) . ' tier)' . " Last update: $last_update days ago"; $output .= " [ Installed: $birthday ]" if length $birthday; SSP::Util::print_info("$product Info: "); SSP::Util::print_normal($output); # Check for expired version my ( $parent_ver, $major_ver ) = split( /\./, SSP::Util::_get_run_var('cpanel_numeric_version'), 3 ); my $expire_info; if ( defined $parent_ver and defined $major_ver ) { $major_ver++ if $major_ver % 2; # Bump odd dev versions $expire_info = 'has expired.' if $major_ver < 62; # TIERS.json file only contains 62+ return unless my $version_href = SSP::Util::get_tier_info_for_version_href( $parent_ver . '.' . $major_ver ); $expire_info = 'has expired (version is not a named or LTS tier in TIERS.json)' if ( !keys %{$version_href} || ( !exists $version_href->{'named'} && !exists $version_href->{'is_lts'} ) ); if ( exists $version_href->{'expires'} && $version_href->{'expires'} =~ /\A \d+ \Z/xms ) { my @months = ( 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ); if ( $version_href->{'expires'} <= time() ) { my ( $day, $month, $year ) = ( localtime( $version_href->{'expires'} ) )[ 3, 4, 5 ]; $expire_info = 'ended on ' . $months[$month] . " " . $day . ", " . ( $year + 1900 ); } } } if ($expire_info) { SSP::Util::print_crit("$product Info: "); SSP::Util::print_critical( "Support for this version of WHM/cPanel " . $expire_info ); my $hasELS = check_for_els(); if ( -e "/var/cpanel/update_blocks.config" ) { my $gradual_update_block = 0; open my $blocker_fh, '<', '/var/cpanel/update_blocks.config'; while (<$blocker_fh>) { chomp; next unless ( $_ =~ m/gradually distribute upgrades/ ); $gradual_update_block = 1; last; } close $blocker_fh; if ( $gradual_update_block == 0 ) { SSP::Util::print_critical(' \_ Send customer this premade: "EOL version of cPanel"') unless ($hasELS); } } SSP::Util::print_normal("\t\\_ ELS: cPanel Extended Lifecycle Support is in effect.") if ($hasELS); SSP::Util::print_critical(' \_ Some SSP output may be irrelevant, incomplete, or inaccurate for EOL versions!') unless ($hasELS); return if ($hasELS); if ( SSP::Util::i_am('cptech') ) { my $epochtime = "1722384001"; ## July 31, 2024 my $currenttime = time(); SSP::Util::print_critical(' \_ cPanel Support Analysts should ignore this critical warning until July 31, 2024 when ELS is officially available.') if ( $currenttime < $epochtime ); } } } sub check_for_els { # No need to continue unless CentOS 7 or CloudLinux 7 return 0 unless ( SSP::Util::i_am('centos') ); # No need to continue if os version is < 7 or >= 8 either. return 0 if ( SSP::Util::os_version_is(qw( >= 8)) || SSP::Util::os_version_is(qw( < 7 )) ); # Also no need to continue if cPanel version is not equal to 110. return 0 unless ( SSP::Util::cpanel_version_is(qw( > 11.110.0)) ); # If both of the below files exist, then return true (1) if ( -e '/etc/yum.repos.d/centos7-els.repo' || -e '/etc/yum/vars/elstoken' ) { return 1 if ( -e '/etc/yum/vars/elstoken' ); open( my $fh, '<', '/etc/yum.repos.d/centos7-els.repo' ); while (<$fh>) { chomp; if ( $_ =~ m{enabled=1} ) { return 1; } } close($fh); return 0; } else { return 0; ## if we get here, all tests above have basically failed and we assume ELS is NOT installed. } } sub check_for_cpanel_update { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); return unless my $cpupdate_conf = SSP::Util::get_cpupdate_conf(); return unless defined $cpupdate_conf->{CPANEL}; my ( $available_tier_version, $local_tier_name ); my $match = 0; if ( SSP::Util::_get_run_var('cpanel_numeric_version') eq 'UNKNOWN' ) { SSP::Util::print_info('Update check: '); SSP::Util::print_warning("unknown or old cPanel version, check $CPANEL_VERSION_FILE"); return; } my $tiers = SSP::Util::get_tiers_file(); return unless $tiers; my @tiers = split /\n/, $tiers; for my $line (@tiers) { if ( $line =~ m{ \A (.*) : (\d+\.\d+\.\d+\.\d+) \z }xms ) { my $tier = $1; $available_tier_version = $2; if ( $tier =~ /^$cpupdate_conf->{CPANEL}$/i ) { $match = 1; last; } } } if ( $match == 0 ) { SSP::Util::print_info('Update check: '); SSP::Util::print_warning("server is configured to use an unknown tier ($cpupdate_conf->{CPANEL})"); return; } if ( SSP::Util::cpanel_version_is( '<', $available_tier_version ) ) { SSP::Util::print_info('Update check: '); SSP::Util::print_warning( "UPDATE AVAILABLE (" . SSP::Util::_get_run_var('cpanel_original_version') . " -> $available_tier_version)" ); } } sub check_lts_autofixer { my $file = '/etc/cpupdate.conf.update_config_prefs'; return unless -f $file; my $cpupdate_conf = SSP::Util::get_cpupdate_conf($file); my $version = defined( $cpupdate_conf->{'CPANEL'} ) ? $cpupdate_conf->{'CPANEL'} : 'UNKNOWN'; SSP::Util::print_info('LTS Autofixer: '); SSP::Util::print_warning("updated this server from tier \"$version\", see TECH-848"); } sub print_uptime { my $uptime = SSP::Util::timed_run( 0, 'uptime' ); chomp $uptime if $uptime; $uptime = $uptime ? $uptime : 'UNKNOWN'; SSP::Util::print_info('Uptime: '); SSP::Util::print_normal($uptime); } sub check_for_license_info { SSP::Warn::check_for_license_status_json(); ## 92+ my %license; my $host = 'verify.cpanel.net'; my $helper_url = "https://" . $host . '/app/verify?ip='; my $file_is_solo = SSP::Util::license_file_is_solo(); my $file_is_dnsonly = SSP::Util::license_file_is_dnsonly(); my $dnsonly_touchfile = '/var/cpanel/dnsonly'; my $license_query = SSP::Util::get_license_info(); my $license_href = SSP::Util::get_json_href($license_query); my $external_ip_address = SSP::Util::get_external_ip(); my $external_license_address = SSP::Util::get_external_license_ip(); if ( defined($external_ip_address) ) { $helper_url .= $external_ip_address; foreach my $key ( @{ $license_href->{current} } ) { if ( defined( $key->{'status'} ) && $key->{'status'} eq 1 ) { ++$license{ $key->{'product'} }; ++$license{ $key->{'product'} } if ( defined( $key->{'package'} ) && $key->{'package'} eq 'ONE TIME FEE' ); } } if ( my $licenses = join " ", map { "[$_]" } sort keys %license ) { SSP::Util::print_info('License: '); SSP::Util::print_normal("$external_ip_address has $licenses"); } else { SSP::Util::print_info('License: '); SSP::Util::print_normal("Not found on verify.cpanel.net, or request timed out -- verify at ${helper_url}"); } foreach my $key ( sort keys %license ) { if ( $license{$key} > 1 ) { SSP::Util::print_info('License: '); SSP::Util::print_normal("[$key] may be a one-time license -- manually verify at [ ${helper_url} ]"); } } if ( ( defined($external_license_address) ) && !( $external_ip_address eq $external_license_address ) ) { SSP::Util::print_crit('License: '); SSP::Util::print_critical("external IP detected via port 80 [ $external_ip_address ] does not match IP detected via port 2089 [ $external_license_address ] which can result in unexpected cPanel license update behavior."); } } if ($file_is_solo) { SSP::Util::print_info('License: '); SSP::Util::print_normal('cPanel Solo (limited to 1 account)'); } if ( SSP::Util::cpanel_version_is(qw ( >= 11.68.0.25 )) ) { if ( not $file_is_dnsonly and -e $dnsonly_touchfile ) { SSP::Util::print_crit('License: '); SSP::Util::print_critical("$dnsonly_touchfile exists but installed license file is not DNSONLY. Try using '/usr/local/cpanel/cpkeyclt --force' to update license."); } if ( $file_is_dnsonly and not -e $dnsonly_touchfile ) { SSP::Util::print_crit('License: '); SSP::Util::print_critical("Installed license file is DNSONLY but $dnsonly_touchfile does not exist. Try using '/usr/local/cpanel/cpkeyclt --force' to update license."); } } my ( $chk_cl, $chk_kc ) = 0; if ( SSP::Util::i_am('cloudlinux') and not exists( $license{'CloudLinux'} ) ) { SSP::Util::print_warn('CloudLinux: '); SSP::Util::print_warning(qq{may not be licensed through cPanel - verify at $helper_url - use "LICENSE - CloudLinux Not Licensed through cPanel" if relevant}); $chk_cl = 1; } my $patchset_output; chomp( $patchset_output = SSP::Util::timed_run( 0, '/usr/bin/kcarectl', '--info' ) ); if ( SSP::Util::i_am('kernelcare') and not exists( $license{'KernelCare'} ) and ( !$patchset_output =~ m/free/ ) ) { SSP::Util::print_warn('KernelCare: '); SSP::Util::print_warning(qq{may not be licensed through cPanel - verify at $helper_url - use "LICENSE - Kernelcare Not Licensed through cPanel" if relevant}); $chk_kc = 1; } if ( $chk_cl or $chk_kc ) { check_cloudlinux_license_site(); } if ( SSP::Util::i_am('litespeed') and not exists( $license{'LiteSpeed'} ) ) { SSP::Util::print_warn('LiteSpeed '); SSP::Util::print_warning(qq{MAY NOT BE LICENSED! - verify at $helper_url - use "LICENSE - LiteSpeed Not Licensed through cPanel" if relevant}); } if ( SSP::Util::i_am('wptk') ) { my $wptk_version_file = '/usr/local/cpanel/3rdparty/wp-toolkit/build.json'; return unless ( -e $wptk_version_file ); return unless ( open( my $version_fh, '<', $wptk_version_file ) ); my $raw; while (<$version_fh>) { $raw .= $_; } close($version_fh); my $wptk_version_info = SSP::Util::get_json_href($raw); my $wptk_build = ( $wptk_version_info->{buildNumber} ) ? $wptk_version_info->{buildNumber} : "build"; my $wptk_version = ( $wptk_version_info->{version} ) ? $wptk_version_info->{version} : "Unknown Version"; SSP::Util::print_info('WP Toolkit version: '); SSP::Util::print_normal( $wptk_version . "-" . $wptk_build ); } if ( SSP::Util::i_am('wp2') ) { SSP::Util::print_info('WP Squared: '); SSP::Util::print_normal("License and installation detected."); } } sub check_cloudlinux_license_site { my $external_ip_address = SSP::Util::get_external_ip(); my @product_array = ( { cl_product_code => 'CL', cl_product => 'CloudLinux OS Shared', }, { cl_product_code => 'CLP', cl_product => 'CloudLinux OS Shared Pro', }, { cl_product_code => 'CLS', cl_product => 'CloudLinux OS Solo', }, { cl_product_code => 'AWP_CDN', cl_product => 'AccelerateWP CDN', }, { cl_product_code => 'AWP_PREMIUM', cl_product => 'AccelerateWP PREMIUM', }, { cl_product_code => 'KC', cl_product => 'KernelCare', }, { cl_product_code => 'KCP', cl_product => 'KernelCare Plus', }, { cl_product_code => 'KCE', cl_product => 'KernelCare Enterprise', }, { cl_product_code => 'IM_1', cl_product => 'Imunify360 Single-User', }, { cl_product_code => 'IM_5', cl_product => 'Imunify360 5 Users', }, { cl_product_code => 'IM_30', cl_product => 'Imunify360 30 Users', }, { cl_product_code => 'IM_250', cl_product => 'Imunify360 250 Users', }, { cl_product_code => 'IM_UN', cl_product => 'Imunify360 Unlimited Users', }, { cl_product_code => 'IMAVP', cl_product => 'ImunifyAV+', }, ); my %product_hash = map { $_->{cl_product_code} => { cl_product => $_->{cl_product} } } @product_array; SSP::Util::print_warning( MAGENTA qq{Checking at cln.cloudlinux.com for a valid license for $external_ip_address...} ); my $valid_cl_found = 0; my $cl_license_query = SSP::Util::get_cl_license_info(); my $cl_license_info = SSP::Util::get_json_href($cl_license_query); for my $cl_instances ( @{ $cl_license_info->{instances} } ) { SSP::Util::print_warning( CYAN "\t\\_ Product: " . GREEN $product_hash{ $cl_instances->{product} }->{cl_product} . CYAN " License Type: " . GREEN $cl_instances->{type} . CYAN " Registered: " . GREEN scalar localtime( $cl_instances->{registered_utc} ) ); $valid_cl_found = 1; } if ( $valid_cl_found == 0 ) { SSP::Util::print_warning( RED "\t\\_ None - Does this server have a valid license?" ); } } sub check_for_systemd { return if ( -e '/usr/bin/systemctl' or -e '/bin/systemctl' ) and ( -e '/usr/lib/systemd/systemd' or -e '/lib/systemd/systemd' ); SSP::Util::print_info('Non-Systemd: '); SSP::Util::print_normal('You should always Use /scripts/restartsrv_* to restart services.'); } sub check_for_linked_server_nodes { my $linked_nodes_json = SSP::Util::get_linked_server_nodes(); my $showheader = 0; for my $linked_nodes ( @{ $linked_nodes_json->{data}->{payload} } ) { SSP::Util::print_info("Linked Server Nodes:\n") unless ($showheader); $showheader = 1; my $node_alias = ( exists $linked_nodes->{alias} ) ? $linked_nodes->{alias} : ""; my $node_hostname = ( exists $linked_nodes->{hostname} ) ? $linked_nodes->{hostname} : ""; my $node_version = ( exists $linked_nodes->{version} ) ? $linked_nodes->{version} : ""; SSP::Util::print_normal( "\t\\_ " . $node_hostname . " [ Friendly Name: " . $node_alias . " ]" . " [ Version: " . $node_version . " ]" ); if ( SSP::Util::cpanel_version_is( '>', $node_version ) ) { print YELLOW "\t\\_ Updates failing? Make sure Linked Server Node is updated before updating this server! [CPANEL-42393]\n"; } } } sub print_apache_info { return unless SSP::Util::i_am_one_of( 'cpanel', 'ea4' ); my $apache_version = SSP::Util::get_apache_version_href(); my $output; $output .= "[ EA4 ] " if SSP::Util::i_am('ea4'); if ( not defined $apache_version->{'version'} or not defined $apache_version->{'built'} or not defined $apache_version->{'ea_version'} ) { $output .= 'could not determine Apache info!'; } else { $output .= "[ $apache_version->{'version'} ] [ $apache_version->{'built'} w/ $apache_version->{'ea_version'} ]"; } my ( $apache_uptime, $apache_generations ); my $apache_configured_port = 80; my $attempted_port = ( split( ':', $CPCONF{'apache_port'} ) )[1]; if ($attempted_port) { $apache_configured_port = $attempted_port; } my $apache_status = SSP::Util::_http_get( Host => '127.0.0.1', Port => $apache_configured_port, Path => '/whm-server-status', MultiHomed => 0, Timeout => 5 ); if ( not $OPT_SKIP_NETWORKING ) { if ($apache_status) { my @apache_status = split /\n/, $apache_status; for my $line (@apache_status) { if ( $line =~ m{ Server \s uptime: \s+ (.*) }xms ) { $apache_uptime = 'Up ' . $1; } if ( $line =~ m{ Parent \s Server \s Generation: (.*) }xms ) { $apache_generations = $1 . ' generation(s)'; } } $output .= ' [ ' . $apache_uptime . ' ]' if defined $apache_uptime; $output .= ' [ ' . $apache_generations . ' ]' if defined $apache_generations; } else { my $warning = ""; if ( $apache_configured_port == 80 ) { $warning = 'Is Apache up/slow to respond? (failed: http://127.0.0.1/whm-server-status). '; } else { $warning = 'Is Apache up/slow to respond? (failed: http://127.0.0.1:' . $apache_configured_port . '/whm-server-status). '; } my $ports = SSP::Util::get_lsof_port_href(); if ( exists $ports->{'80'} ) { $warning .= 'Something is listening on port 80.'; } else { $warning .= 'Nothing is listening on port 80'; } SSP::Util::print_info('Apache: '); SSP::Util::print_warning($warning); } } if ($output) { SSP::Util::print_info('Apache: '); SSP::Util::print_normal($output); } my %apache_ports; my %root_httpd; my $ports = SSP::Util::get_lsof_port_href(); my $procs = SSP::Util::get_process_pid_href(); my $listeningservice = ""; while ( my ( $portnum, $aref ) = each(%$ports) ) { for my $href (@$aref) { next unless $href->{USER} eq "root"; if ( $href->{CMD} eq "cpsrvd" && $portnum == 80 ) { $listeningservice = RED "cpsrvd "; $apache_ports{$portnum} = 0; last; } next unless $href->{CMD} eq "httpd"; my $pid = $href->{PID}; if ( defined $procs->{$pid} and $procs->{$pid}->{ETIMES} > 60 ) { next if $procs->{$pid}->{ARGS} =~ m{ \A /apache/bin/httpd }xms; # Ignore these - see TECH-334 $root_httpd{$pid} = 1; } $apache_ports{$portnum} = 1; } } if ( scalar keys(%apache_ports) ) { SSP::Util::print_info('Apache: '); SSP::Util::print_normal( $listeningservice . BOLD CYAN 'is listening on ports [ ' . join( " ", sort( keys(%apache_ports) ) ) . ' ]' ); } check_for_ea_nginx(); if ( scalar keys(%root_httpd) > 1 ) { my $pids = scalar keys(%root_httpd) > 4 ? 'More than 4!' : join( ' ', sort( keys(%root_httpd) ) ); SSP::Util::print_warn('Apache: '); SSP::Util::print_warning( 'multiple root httpd processes (more than 60 seconds old) found [ ' . $pids . ' ] -- See TECH-314.' ); } } sub print_lsws_info { return unless SSP::Util::i_am('cpanel'); return unless my ( $lsws_full_version, $lsws_numeric_version ) = @{ SSP::Util::get_lsws_version_aref() }; SSP::Util::print_info('LiteSpeed Web Server: '); my %lshttpd_ports = (); my $ports = SSP::Util::get_lsof_port_href(); while ( my ( $portnum, $aref ) = each(%$ports) ) { for my $href (@$aref) { next if not $href->{USER} eq "root"; next if not $href->{CMD} eq "litespeed"; $lshttpd_ports{$portnum} = 1; } } my $lsws_ports_output = ""; if ( scalar keys(%lshttpd_ports) ) { $lsws_ports_output = YELLOW "listening on [ " . CYAN join( ' ', sort( keys(%lshttpd_ports) ) ) . " ]"; } my $lsws_update_available = check_for_lsws_update(); $lsws_update_available = "" unless ($lsws_update_available); my $kernel_pid_max = ""; $kernel_pid_max = SSP::Util::get_kernel_pid_max(); SSP::Util::print_normal( "$lsws_full_version " . $lsws_ports_output . " " . $lsws_update_available ); SSP::Util::print_warning(qq{\t\\_ non-Enterprise editions of LiteSpeed are NOT directly supported}) unless ( $lsws_full_version =~ /Enterprise/ ); SSP::Util::print_warning(qq{\t\\_ whm-server-status is incompatible with LiteSpeed}); SSP::Util::print_warning( qq{\t\\_ } . $kernel_pid_max ) unless ( !$kernel_pid_max ); } sub check_for_ea_nginx { my $rpms = SSP::Util::get_rpm_href(); if ( exists $rpms->{'ea-nginx'} ) { # We support ea-nginx installs my $ea_nginx_mode = ( -e '/etc/nginx/ea-nginx/enable.standalone' ) ? "standalone mode" : "reverse proxy mode"; my $ea_nginx_cache_file = '/etc/nginx/ea-nginx/cache.json'; return unless ( -e $ea_nginx_cache_file ); return unless ( open( my $version_fh, '<', $ea_nginx_cache_file ) ); my $raw; while (<$version_fh>) { $raw .= $_; } close($version_fh); my $ea_nginx_cache_data = SSP::Util::get_json_href($raw); my $ea_nginx_cache_enabled = ( $ea_nginx_cache_data->{enabled} ) ? "Enabled" : "Disabled"; my $eaNginxVer; $eaNginxVer = SSP::Util::timed_run( 2, 'rpm', '-q', '--queryformat', '%{Version}', 'ea-nginx' ) unless ( SSP::Util::i_am('ubuntu') ); $eaNginxVer = SSP::Util::timed_run( 2, 'dpkg-query', '-W', '-f${Version}', 'ea-nginx' ) if ( SSP::Util::i_am('ubuntu') ); $eaNginxVer =~ s/\-.*//g if ( SSP::Util::i_am('ubuntu') ); SSP::Util::print_info('ea-nginx '); SSP::Util::print_normal("cPanel supported ea-nginx $eaNginxVer is running in $ea_nginx_mode [ Global Caching: $ea_nginx_cache_enabled ]"); } } sub check_for_elevate { return unless ( SSP::Util::i_am_one_of( 'cpanel', 'cloudlinux' ) ); # No need to continue unless CentOS 7 or CloudLinux 7 return unless ( SSP::Util::i_am_one_of( 'centos', 'cloudlinux' ) ); # No need to continue if os version is < 7 or >= 8 either. return if ( SSP::Util::os_version_is(qw( >= 8)) || SSP::Util::os_version_is(qw( < 7 )) ); # Also no need to continue if cPanel version is not equal to 110. return unless ( SSP::Util::cpanel_version_is(qw( > 11.110.0)) ); # CX-975 - Get version and vendor version. return unless ( open( my $fh, '<', '/usr/local/cpanel/scripts/elevate-cpanel' ) ); my $elevate_vendor = "cPanel"; while (<$fh>) { chomp; next unless ( $_ =~ m/DEFAULT_ELEVATE_BASE_URL/ ); $elevate_vendor = "CloudLinux" if ( $_ =~ m/cloudlinux/ ); last; } close($fh); my $elevate_url = 'https://raw.githubusercontent.com/cpanel/elevate/release'; my $latest_elevate_version = SSP::Util::timed_run( 3, 'curl', '-s', "$elevate_url/version" ); chomp($latest_elevate_version); my $elevate_version = SSP::Util::timed_run( 2, '/usr/local/cpanel/scripts/elevate-cpanel', '--version' ); chomp($elevate_version); SSP::Util::print_info( 'ELevate Vendor: ' . CYAN $elevate_vendor . GREEN " [Current Server Version: " . WHITE $elevate_version . GREEN "] [Latest Version Available: " . WHITE $latest_elevate_version . GREEN "]\n" ); SSP::Util::print_normal( RED "\t\\_ Incorrect vendor. Please download from: " . GREEN "$elevate_url/elevate-cpanel" ) if ( $elevate_vendor eq 'CloudLinux' ); SSP::Util::print_normal( RED "\t\\_ Incorrect version. Please download from: " . GREEN "$elevate_url/elevate-cpanel" ) if ( $elevate_version != $latest_elevate_version ); return unless ( -e '/var/cpanel/elevate' ); return unless ( open( my $elevate_fh, '<', '/var/cpanel/elevate' ) ); my $raw; while (<$elevate_fh>) { $raw .= $_; } close($elevate_fh); my $elevate_data = SSP::Util::get_json_href($raw); my $elevate_status = YELLOW "UNKNOWN/NOT YET STARTED"; if ( defined $elevate_data->{status} && $elevate_data->{status} eq 'running' ) { SSP::Util::print_crit('ELevate Conversion IN PROGRESS!!'); SSP::Util::print_critical("\n\t\\_ ALL ANALYSTS!! DO NOT RUN ANY COMMANDS ON THIS SERVER\n\t\\_ Unless you are on the migration team"); return; } $elevate_status = RED "FAILED!" if ( defined $elevate_data->{status} && $elevate_data->{status} eq 'failed' ); $elevate_status = GREEN "SUCCESSFUL" if ( defined $elevate_data->{status} && $elevate_data->{status} eq 'success' ); SSP::Util::print_info('ELevate Conversion File Found: '); SSP::Util::print_normal( "cPanel ELevate was checked/executed on this server. Status: " . $elevate_status ); } sub check_for_lsws_update { return unless SSP::Util::i_am('cpanel'); return unless my ( $lsws_full_version, $lsws_numeric_version ) = @{ SSP::Util::get_lsws_version_aref() }; return if $lsws_numeric_version eq "unknown"; return unless $lsws_full_version =~ /Enterprise/; my $reply = SSP::Util::_http_get( Host => 'update.litespeedtech.com', Path => '/ws/latest.php', MultiHomed => 0, Timeout => 5 ); return unless defined $reply; my $available_lsws_version; my @lsws_data = split /\n/, $reply; for (@lsws_data) { if (m{ \A LSWS=(\d+\.\d+\.\d+) \z }xms) { $available_lsws_version = $1; last; } } return unless $available_lsws_version; if ( SSP::Util::version_compare( $lsws_numeric_version, '<', $available_lsws_version ) ) { return RED "UPDATE AVAILABLE ($lsws_numeric_version -> $available_lsws_version)"; } } sub print_ea4_php_configuration { return unless SSP::Util::i_am('ea4'); my $info = 'UNKNOWN'; my $fpm_jail_toggle = '/var/cpanel/feature_toggles/apachefpmjail'; my $ea4_php = SSP::Util::get_installed_ea4_php_href(); my $modules = SSP::Util::get_apache_modules_href(); SSP::Util::print_info('PHP Default: '); if ( defined($ea4_php) && defined( $ea4_php->{default} ) && defined( $ea4_php->{ $ea4_php->{default} }->{release_version} ) && defined( $ea4_php->{ $ea4_php->{default} }->{handler} ) ) { $info = '[ EA4 ]'; $info .= " [ $ea4_php->{ $ea4_php->{default} }->{release_version} ( $ea4_php->{default} ) ]"; $info .= " [ $ea4_php->{ $ea4_php->{default} }->{handler} ]"; $info .= RED "\n\t\\_ Default PHP Version should not be alt-php. Always use a ea-php version." if ( $ea4_php->{default} =~ m{alt-php} ); } SSP::Util::print_normal($info); check_for_eol_php_versions(); ## TECH-1343 check_cpanel_php_fpm_status(); ## CX-240 if ( -e $fpm_jail_toggle ) { SSP::Util::print_info('PHP-FPM: '); SSP::Util::print_normal( $fpm_jail_toggle . ' exists, PHP-FPM will jail PHP scripts for users that have Jailed or Disabled shells.' ); if ( defined $modules and not( defined $modules->{'ruid2_module'} and defined $CPCONF{'jailapache'} and $CPCONF{'jailapache'} == 1 ) ) { SSP::Util::print_warn('PHP-FPM: '); SSP::Util::print_warning('Jail is enabled without mod_ruid2 and/or Jail Apache Virtual Hosts tweak setting enabled, these MUST also be enabled for proper functioning unless EA-5524 is resolved.'); } } if ( defined $CPCONF{'jailapache'} and $CPCONF{'jailapache'} == 1 ) { SSP::Util::print_warn('Experimental Jail Apache: '); SSP::Util::print_warning('Hosts tweak setting is enabled. Seeing unexplained 404 errors?'); } } sub check_for_eol_php_versions { return if ( SSP::Util::i_am('cloudlinux') ); ## CloudLinux with LVE Manager uses patched ea-phpXX modules. return if ( SSP::Util::i_am('ubuntu') ); ## See: https://cloudlinux.zendesk.com/hc/en-us/articles/6312144825372 return if ( -e '/etc/yum.repos.d/imunify360-ea-php-hardened.repo' ); ## Return if Imunify360 w/HardenedPHP is installed. my @dir_contents = glob('/etc/cpanel/ea4/recommendations/ea-php*/eol.json'); my @eol_php_versions; foreach my $phpver (@dir_contents) { chomp($phpver); $phpver =~ s/\/etc\/cpanel\/ea4\/recommendations\///; $phpver =~ s/\/eol\.json//; push( @eol_php_versions, $phpver ); } my $vhost_cnt; my $showHeader = 0; foreach my $eol_php_version (@eol_php_versions) { $vhost_cnt = 0; my $raw = SSP::Util::timed_run( 3, '/sbin/whmapi1', 'php_get_vhosts_by_version', "version=$eol_php_version", '--output=json' ); my $json_output = SSP::Util::get_json_href($raw); for my $eol_php_vhosts ( @{ $json_output->{data}->{vhosts} } ) { $vhost_cnt++; } if ( $vhost_cnt > 0 ) { SSP::Util::print_crit("There are active sites running EOL PHP Versions - Use PREDEFS::EOL::PHP\n") unless ($showHeader); $showHeader = 1; } SSP::Util::print_critical("\t\\_ $vhost_cnt sites are running EOL PHP Version: $eol_php_version") unless ( $vhost_cnt == 0 ); } } sub check_cpanel_php_fpm_status { my $cpanel_php_fpm_disable = '/etc/cpanel_php_fpmdisable'; return unless ( SSP::Util::i_am('cpanel') ); SSP::Util::print_info('cPanel PHP-FPM: '); return SSP::Util::print_normal('disabled') if ( -e $cpanel_php_fpm_disable ); my $php_fpm_path = "php-fpm: master process (/usr/local/cpanel/etc/php-fpm.conf)"; my $rawJSON = SSP::Util::timed_run( 3, '/usr/sbin/whmapi1', 'servicestatus', 'service=cpanel_php_fpm', '--output=json' ); my $json_output = SSP::Util::get_json_href($rawJSON); return SSP::Util::print_normal('running') if ( $json_output->{data}->{service}->[0]->{running} ); SSP::Util::print_warning('NOT running!'); } sub check_for_clustering { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); return unless -e '/var/cpanel/useclusteringdns'; SSP::Util::print_info('DNS Clustering: '); SSP::Util::print_normal('is enabled'); my $cluster_dir = '/var/cpanel/cluster/root/config'; my @dir_contents; my @cluster_members; if ( -d $cluster_dir ) { opendir( my $dir_fh, $cluster_dir ); @dir_contents = grep { !/^\.\.?$/ } readdir $dir_fh; closedir $dir_fh; } chdir $cluster_dir or return; for my $dirent (@dir_contents) { next if ( $dirent =~ m{^CDN} ); my ( $cluster_member, $cluster_member_hostname, $cluster_member_role ); my %cluster_conf; # only active cluster members have -dnsrole files if ( $dirent =~ m{ \A (.+)-dnsrole \z }xms ) { $cluster_member = $1; if ( open my $file_fh, '<', $cluster_member ) { local $/; %cluster_conf = map { ( split( /=/, $_, 2 ) )[ 0, 1 ] } split( /\n/, readline($file_fh) ); close $file_fh; } $cluster_member_hostname = defined $cluster_conf{host} ? $cluster_conf{host} : '?'; if ( $cluster_member =~ m{ \A (vps\.net|softlayer) \z}xmsi ) { $cluster_member_hostname = ''; } if ( open my $file_fh, '<', "${cluster_member}-dnsrole" ) { while (<$file_fh>) { $cluster_member_role = $_; chomp $cluster_member_role; } close $file_fh; } $cluster_member_role = defined $cluster_member_role ? $cluster_member_role : '?'; if ( -e "$cluster_dir/$cluster_member-state.json" ) { my $raw = SSP::Util::timed_run( 4, 'cat', "$cluster_dir/$cluster_member-state.json" ); my $json_output = SSP::Util::get_json_href($raw); my ( $dnshost, $dnsserverinfo ); my $trusted_relationship = RED "[ Not Trusted ]"; if ( $json_output->{hostname} && $json_output->{host} ) { $dnshost = $json_output->{hostname} . ' - ' . $json_output->{host}; } if ( $json_output->{has_reverse_trust} ) { $trusted_relationship = GREEN "[ Trusted ]"; } $dnsserverinfo = ""; if ( $json_output->{dns_server} && $json_output->{dns_version} ) { $dnsserverinfo = CYAN "DNS Server: " . YELLOW ucfirst( $json_output->{dns_server} ) . CYAN ' Ver: ' . YELLOW $json_output->{dns_version}; } my $dnserror = ""; if ( $json_output->{error} ) { $dnserror = CYAN $dnsserverinfo . "Last known error (if any): " . RED $json_output->{error}; } $trusted_relationship = '' if ( $json_output->{dnsrole} eq 'write-only' ); push @cluster_members, MAGENTA $dnshost . ' [ ' . YELLOW ucfirst( $json_output->{dnsrole} ) . MAGENTA ' ]' . ' ' . $trusted_relationship . ' ' . $dnsserverinfo . ' ' . $dnserror; } else { push @cluster_members, $cluster_member_hostname . ' - ' . $cluster_member . ' ' . "[" . YELLOW ucfirst($cluster_member_role) . MAGENTA "] " . GREEN "[Trust: Unknown] " . CYAN "DNS Server: " . YELLOW "Unknown " . CYAN "Ver: " . YELLOW "Unknown"; } } } return unless @cluster_members; @cluster_members = sort @cluster_members; for my $member (@cluster_members) { SSP::Util::print_magenta( "\t \\_ " . $member ); } check_for_uniquedns(); } sub check_for_uniquedns() { # Resellers can set UniqueDNS within their DNS Cluster setup without adding a server. See CX-820 my $clusterdir = '/var/cpanel/cluster'; my $allgood = 0; opendir( my $dir, $clusterdir ); while ( my $reseller = readdir $dir ) { chomp($reseller); next if ( $reseller eq '.' || $reseller eq '..' || $reseller eq 'root' ); if ( -e "$clusterdir/$reseller/uniquedns" ) { my $resellerdir = "$clusterdir/$reseller/config"; opendir( my $rdir, $resellerdir ) or die($!); while ( my $dirent = readdir $rdir ) { chomp($dirent); next if ( $dirent eq '.' || $dirent eq '..' ); $allgood = 1 unless ( "$resellerdir/$dirent" =~ m{ \A (.+)-state.json \z }xms ); } closedir($rdir); if ( $allgood == 0 ) { SSP::Util::print_red("\t \\_ The $reseller reseller has Unique DNS Clustering enabled without DNS servers configured.\n\t\t \\_ See: https://docs.cpanel.net/whm/clusters/dns-cluster/#unique-dns-clustering"); } } } closedir($dir); } sub check_for_remote_mysql { my $mysql_host; my $mysql_is_local; ## obtain mysql host, if exists my $my_cnf = '/root/.my.cnf'; if ( open my $my_cnf_fh, '<', $my_cnf ) { while (<$my_cnf_fh>) { chomp( my $line = $_ ); if ( $line =~ m{ \A host \s* = \s* (?:["']?) ([^"']+) }xms ) { $mysql_host = $1; } } close $my_cnf_fh; } if ($mysql_host) { if ( $mysql_host =~ m{ ( \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} ) }xms ) { return if ( $mysql_host eq '127.0.0.1' ); for my $ipaddr ( @{ SSP::Util::get_local_ipaddrs_aref() } ) { if ( $ipaddr eq $mysql_host ) { $mysql_is_local = 1; last; } } } elsif ( $mysql_host eq 'localhost' or $mysql_host eq SSP::Util::get_hostname() ) { $mysql_is_local = 1; } if ( !$mysql_is_local ) { SSP::Util::print_info('Remote Database Host: '); SSP::Util::print_warning($mysql_host); } } } sub print_nameserver_daemon { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); SSP::Util::print_info('DNS Service: '); SSP::Util::print_normal("PowerDNS") if ( $CPCONF{'local_nameserver_type'} eq 'powerdns' ); SSP::Util::print_normal("BIND") if ( $CPCONF{'local_nameserver_type'} eq 'bind' ); SSP::Util::print_normal("DISABLED") if ( $CPCONF{'local_nameserver_type'} eq 'disabled' ); } sub check_for_unbound_stub_resolver { my $unboundstub = '/var/cpanel/dns_unbound_resolve_mode'; return unless ( -l $unboundstub ); my $link = readlink $unboundstub; if ( $link eq 'stub' ) { SSP::Util::print_info("stub touchfile exists - See TECH-1002\n"); } } sub check_for_nginx_unlimited_memory_touchfile { my $oom_nginx_touchfile = '/etc/cpanel/ea4/option-flags/give-cpsrvd-nginx-operations-unlimited-memory'; return unless ( -e $oom_nginx_touchfile ); SSP::Util::print_info("Touchfile exists for Nginx related cPanel operations to have no memory limits - CPANEL-40846\n"); } sub check_for_exim_ipv6_sort_bias_touchfile { return if ( !-e '/var/cpanel/exim_ipv6_sort_bias' ); SSP::Util::print_info("IPv6 Sort Bias touchfile exists: "); SSP::Util::print_normal("Exim will deliver over IPv6 if both IPv4 and IPv6 are available"); } sub check_for_public_resolvers { return if ( !-e "/etc/resolv.conf" ); my $query_refused = SSP::Util::timed_run( 4, 'dig', '+short', 'TXT', '2.0.0.127.multi.uribl.com' ); return unless ( $query_refused =~ m/Query Refused/ ); SSP::Util::print_info('DNSBL Queries Rate Limited: '); SSP::Util::print_normal('/etc/resolv.conf might contain public DNS resolvers. Seeing issues with RBLs not working?'); SSP::Util::print_normal("\t \\_ See: https://support.cpanel.net/hc/en-us/articles/360053079473"); } sub print_mysql_version { return if ( SSP::Util::i_am('dnsonly') ); SSP::Util::print_info('Database Type and Version: '); my $mysql_full_version = SSP::Util::get_mysql_full_version(); SSP::Util::print_critical("*** UNKNOWN! Is the database server Running? ***") unless ($mysql_full_version); return unless ($mysql_full_version); SSP::Util::print_normal($mysql_full_version); return unless my $mysql_numeric_version = SSP::Util::get_mysql_numeric_version(); if ( defined $CPCONF{'mysql-version'} ) { my $test_version = $CPCONF{'mysql-version'} . '.'; unless ( index( $mysql_numeric_version, $test_version ) == 0 ) { SSP::Util::print_warning( "\t \\_ mysql-version=" . $CPCONF{'mysql-version'} . ' in cpanel.config does not match installed version!' ); } } } sub check_if_mysqlupgrade_is_running { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly', 'almalinux', 'cloudlinux', 'ubuntu' ); my %procs = SSP::Util::grep_process_cmd( qr{ whostmgr - mysqlupgrade }xms, 'root' ); return unless ( keys %procs ); SSP::Util::print_info('Database Upgrade: '); SSP::Util::print_normal( sprintf( 'Found whostmgr - mysqlupgrade running [ %s ]. May cause upcp failures if unresponsive.', join( ' ', keys %procs ) ) ); } sub print_backups_info { return unless SSP::Util::i_am('cpanel'); my $new_backup_conf = SSP::Util::get_new_backup_conf_href(); my %new_dest = (); my ( $new_backups_cron, $new_backups_status ) = ( 0, 'No Config' ); my $warning = 0; my $new_backup_dir = '/var/cpanel/backups/'; if ( defined $new_backup_conf and defined $new_backup_conf->{'BACKUPENABLE'} and $new_backup_conf->{'BACKUPENABLE'} =~ /yes/ ) { my @dir_contents = (); if ( opendir( my $dir_fh, $new_backup_dir ) ) { @dir_contents = readdir $dir_fh; closedir $dir_fh; } for my $dest (@dir_contents) { if ( $dest =~ m{ \.backup_destination \z }xms ) { if ( open( my $destconf_fh, '<', $new_backup_dir . $dest ) ) { local $/ = undef; %{ $new_dest{$dest} } = map { ( split( /:\s/, $_, 2 ) )[ 0, 1 ] } split( /\n/, readline($destconf_fh) ); close $destconf_fh; } } } } if ( defined $new_backup_conf and defined $new_backup_conf->{'BACKUPENABLE'} and $new_backup_conf->{'BACKUPENABLE'} =~ /yes/ ) { my $root_cron_path = '/var/spool/cron/root'; $root_cron_path = '/var/spool/cron/crontabs/root' if ( SSP::Util::i_am('ubuntu') ); if ( open my $file_fh, '<', $root_cron_path ) { while (<$file_fh>) { if (m{ \A [^#] .+ /usr/local/cpanel/bin/backup }xms) { $new_backups_cron = 1; } } close $file_fh; } } if ( defined $new_backup_conf and defined $new_backup_conf->{'BACKUPENABLE'} ) { if ( $new_backup_conf->{'BACKUPENABLE'} =~ /yes/ ) { $new_backups_status = 'Enabled'; if ( defined( $new_backup_conf->{'BACKUPACCTS'} ) && $new_backup_conf->{'BACKUPACCTS'} =~ /yes/ ) { $new_backups_status .= '/WithAccounts'; } elsif ( defined( $new_backup_conf->{'BACKUPACCTS'} ) && $new_backup_conf->{'BACKUPACCTS'} =~ /no/ ) { $new_backups_status .= '/NoAccounts'; } if ( defined( $new_backup_conf->{'BACKUPTYPE'} ) && $new_backup_conf->{'BACKUPTYPE'} =~ /uncompressed/ ) { $new_backups_status .= '/Uncompressed'; } elsif ( defined( $new_backup_conf->{'BACKUPTYPE'} ) && $new_backup_conf->{'BACKUPTYPE'} =~ /compressed/ ) { $new_backups_status .= '/Compressed'; } elsif ( defined( $new_backup_conf->{'BACKUPTYPE'} ) && $new_backup_conf->{'BACKUPTYPE'} =~ /incremental/ ) { $new_backups_status .= '/Incremental'; } else { $new_backups_status .= '/Unknown'; } if ( $new_backups_cron != 1 ) { $new_backups_status .= ' (MISSING CRON!)'; $warning = 1; } } elsif ( $new_backup_conf->{'BACKUPENABLE'} =~ /no/ ) { $new_backups_status = 'Disabled'; } } if ( keys(%new_dest) ) { if ( defined $new_backup_conf and defined $new_backup_conf->{'KEEPLOCAL'} and $new_backup_conf->{'KEEPLOCAL'} =~ /1/ ) { $new_backups_status .= '/RetainLocal'; } else { $new_backups_status .= '/NoRetainLocal'; } } # Is Jetbackup installed? my $JetBackupVersion = "N/A"; if ( SSP::Util::i_am('jetbackup') ) { my $raw; if ( -e '/usr/bin/jetapi' ) { $raw = SSP::Util::timed_run( 0, '/usr/bin/jetapi', 'backup', '-F', 'manageSettingsGeneral', '-O', 'json' ); } if ( -e '/usr/bin/jetbackup5api' ) { $raw = SSP::Util::timed_run( 0, '/usr/bin/jetbackup5api', 'backup', '-F', 'manageSettingsGeneral', '-O', 'json' ); } my $json_output = SSP::Util::get_json_href($raw); if ( defined $json_output->{system}->{version} && defined $json_output->{system}->{tier} ) { $JetBackupVersion = $json_output->{system}->{version} . " (" . $json_output->{system}->{tier} . ")"; } } SSP::Util::print_info('Backups: '); if ($warning) { SSP::Util::print_warning("[New: $new_backups_status] [Jetbackup: $JetBackupVersion]"); } else { SSP::Util::print_normal("[New: $new_backups_status] [Jetbackup: $JetBackupVersion]"); } for my $dest ( sort { $new_dest{$a}->{type} cmp $new_dest{$b}->{type} } keys %new_dest ) { my $type = exists $new_dest{$dest}->{'type'} ? $new_dest{$dest}{'type'} : 'UNKNOWN'; my $disabled = exists $new_dest{$dest}{'disabled'} ? ( $new_dest{$dest}{'disabled'} ? "Yes" : "No" ) : 'UNKNOWN'; my $name = exists $new_dest{$dest}{'name'} ? $new_dest{$dest}{'name'} : 'UNKNOWN'; my $timeoutdest = exists $new_dest{$dest}->{'timeout'} ? $new_dest{$dest}{'timeout'} : 'UNKNOWN'; SSP::Util::print_normal( "\t\t\\_ Remote dest: [Type: " . $type . "] [Disabled: " . $disabled . "] [Name: " . $name . "] [Timeout: " . $timeoutdest . "]" ); if ( $type eq "SFTP" && exists $new_dest{$dest}{'privatekey'} && exists $new_dest{$dest}{'passphrase'} ) { my $key_is_encrypted = 0; if ( open my $privatekey_fh, '<', $new_dest{$dest}->{'privatekey'} ) { while (<$privatekey_fh>) { if (/ENCRYPTED/) { $key_is_encrypted = 1; last; } } close $privatekey_fh; } if ( !$key_is_encrypted ) { SSP::Util::print_warning("\t\t \\_ The SFTP private key is not encrypted but the transport config contains a passphrase. See FB-152341 and FB-152337."); } } } } sub check_metadata_vacuum { return unless ( -s '/var/cpanel/vacuum_metadata.pid' ); if ( !SSP::Util::exists_process_cmd( qr{ vacuum_metadata }xms, 'root' ) ) { SSP::Util::print_warning("\t\\_ /var/cpanel/vacuum_metadata.pid file exists but vacuum_metadata not found in process list."); } } sub print_mailserver_info { return unless SSP::Util::i_am('cpanel'); return unless defined $CPCONF{'mailserver'}; return unless SSP::Util::cpanel_version_is(qw( < 11.53.0.0 )); # 54+ only supports Dovecot SSP::Util::print_info('Mailserver: '); SSP::Util::print_normal( $CPCONF{'mailserver'} ); } sub print_ftpserver_info { return unless SSP::Util::i_am('cpanel'); my $external_ip_address = SSP::Util::get_external_ip(); my $pureftpd_conf = SSP::Util::get_pureftpd_conf_href(); my $proftpd_conf = SSP::Util::get_proftpd_conf_href(); SSP::Util::print_info('FTP Server: '); my $passiveports = ""; my $passiveip = ""; if ( defined( $CPCONF{'ftpserver'} ) ) { if ( $CPCONF{'ftpserver'} eq 'pure-ftpd' ) { if ( defined( $pureftpd_conf->{'passiveportrange'} ) && defined( $pureftpd_conf->{'passiveportrange'}->{value} ) ) { $passiveports = $pureftpd_conf->{'passiveportrange'}->{value}; } if ( defined( $pureftpd_conf->{'forcepassiveip'} ) && defined( $pureftpd_conf->{'forcepassiveip'}->{value} ) ) { $passiveip = $pureftpd_conf->{'forcepassiveip'}->{value}; } } if ( $CPCONF{'ftpserver'} eq 'proftpd' ) { if ( defined( $proftpd_conf->{'passiveports'} ) && defined( $proftpd_conf->{'passiveports'}->{value} ) ) { $passiveports = $proftpd_conf->{'passiveports'}->{value}; } if ( defined( $proftpd_conf->{'masqueradeaddress'} ) && defined( $proftpd_conf->{'masqueradeaddress'}->{value} ) ) { $passiveip = $proftpd_conf->{'masqueradeaddress'}->{value}; } } } my $fwppactive = 0; if ($passiveports) { $passiveports =~ s/\s+/:/; my @fwcommand = SSP::Util::timed_run( 10, '/sbin/iptables', '-nL' ); foreach my $fwline (@fwcommand) { chomp($fwline); if ( $fwline =~ m/$passiveports/ and $fwline =~ m/ACCEPT/ ) { $fwppactive = 1; last; } } } my $passivetext = $passiveports ? "enabled - " . ( $fwppactive ? "allowed in iptables" : "not found in iptables" ) : "not enabled"; if ( $passiveip ne "" && defined($external_ip_address) && $passiveip ne $external_ip_address && defined( $CPCONF{'ftpserver'} ) ) { if ( $CPCONF{'ftpserver'} eq 'proftpd' ) { $passivetext .= " - MasqueradeAddress ( $passiveip ) doesn't match license IP"; } elsif ( $CPCONF{'ftpserver'} eq 'pure-ftpd' ) { $passivetext .= " - ForcePassiveIP ( $passiveip ) doesn't match license IP"; } } if ( defined( $CPCONF{'ftpserver'} ) ) { SSP::Util::print_normal("$CPCONF{ftpserver} ( Passive ports $passivetext )"); } else { SSP::Util::print_warning('missing ftpserver setting in cpanel.config'); } return; } sub print_exim_info { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); return unless my $exim_localopts = SSP::Util::get_exim_localopts_href(); if ( defined $exim_localopts->{acl_delay_unknown_hosts} && $exim_localopts->{acl_delay_unknown_hosts} ) { my $info = '20 second SMTP delay active (by default) for unknown hosts and spam, see DOC-6092.'; my $disabled = ''; if ( defined $exim_localopts->{acl_dont_delay_greylisting_trusted_hosts} && $exim_localopts->{acl_dont_delay_greylisting_trusted_hosts} ) { $disabled .= ' [Greylisting Trusted Hosts]'; } if ( defined $exim_localopts->{acl_dont_delay_greylisting_common_mail_providers} && $exim_localopts->{acl_dont_delay_greylisting_common_mail_providers} ) { $disabled .= ' [Greylisting Common Mail Providers]'; } $info .= ' Disabled for:' . $disabled if length $disabled; SSP::Util::print_info('Exim: '); SSP::Util::print_normal($info); } } sub print_roundcube_db { SSP::Util::print_info('Roundcube: '); SSP::Util::print_normal('using sqlite databases [default]') if ( defined $CPCONF{'roundcube_db'} and $CPCONF{'roundcube_db'} eq 'sqlite' ); SSP::Util::print_normal('using mysql database [deprecated]') if ( defined $CPCONF{'roundcube_db'} and $CPCONF{'roundcube_db'} eq 'mysql' ); SSP::Util::print_normal('using special mysql_plus_sqlite setting for GoDaddy servers only') if ( defined $CPCONF{'roundcube_db'} and $CPCONF{'roundcube_db'} eq 'mysql_plus_sqlite' ); } sub check_for_custom_webtemplates { return unless SSP::Util::i_am('cpanel'); my $template_dir = '/var/cpanel/webtemplates'; return unless -d $template_dir; my $found; find( sub { return unless /\.tmpl$/s; $found = 1; }, $template_dir ); return unless $found; SSP::Util::print_info('Web templates: '); SSP::Util::print_normal("found in ${template_dir} -- https://docs.cpanel.net/whm/account-functions/web-template-editor/"); } sub check_for_custom_restoremodules { return unless SSP::Util::i_am('cpanel'); my $restoremodule_dir = '/var/cpanel/perl/Whostmgr/Transfers/Systems'; return unless -d $restoremodule_dir; my $found; find( sub { return unless /\.pm$/s; $found = 1; }, $restoremodule_dir ); return unless $found; SSP::Util::print_info('Custom Restore Modules: '); SSP::Util::print_normal("found in ${restoremodule_dir} -- can cause issues with restoration process."); } sub check_for_custom_zonetemplates { return unless SSP::Util::i_am('cpanel'); my $template_dir = '/var/cpanel/zonetemplates'; return unless -d $template_dir; my $is_empty = 0; opendir( my $fh, $template_dir ) or return; my @dirents = grep { !/^\.\.?/ } readdir $fh; closedir $fh; return if !@dirents; for my $file (@dirents) { if ( -z "${template_dir}/${file}" ) { $is_empty = 1; last; } } SSP::Util::print_info('Zone templates: '); if ( $is_empty == 1 ) { SSP::Util::print_red("found in $template_dir - some may be empty!"); } else { SSP::Util::print_normal("found in $template_dir"); } } sub check_selinux_status { my @selinux_status = split /\n/, SSP::Util::timed_run( 0, 'sestatus' ); return if !@selinux_status; for my $line (@selinux_status) { if ( $line =~ m{ \A SELinux \s status: \s+ ([^\s\n]+) }xms ) { return if $1 eq "disabled"; } elsif ( $line =~ m{ \A Current \s mode: \s+ ([^\s\n]+) }xms ) { if ( $1 eq "permissive" ) { SSP::Util::print_info('SELinux: '); SSP::Util::print_normal('Permissive'); return; } else { SSP::Util::print_warn('SELinux: '); SSP::Util::print_warning('is ENFORCING!'); return; } } } } sub check_imunify_config { return unless ( -e '/usr/bin/imunify360-agent' ); my $i360_config_file; my $raw = SSP::Util::timed_run( 0, '/usr/bin/imunify360-agent', 'version', '--json' ); my $i360versioninfo = SSP::Util::get_json_href($raw); my $imunify_licenseType = $i360versioninfo->{license}->{license_type}; my $imunify_version = $i360versioninfo->{version}; return unless ($imunify_licenseType); if ( SSP::Util::version_compare( $imunify_version, '<', '5.8' ) ) { $i360_config_file = "/etc/sysconfig/imunify360/imunify360.config"; } else { $i360_config_file = "/etc/sysconfig/imunify360/imunify360-merged.config"; } return unless ( -e $i360_config_file ); eval("use YAML::Syck"); ## no critic (ProhibitStringyEval) return if ($@); my $i360Conf = YAML::Syck::LoadFile($i360_config_file); my $default_action = ( defined $i360Conf->{MALWARE_SCANNING}->{default_action} ) ? $i360Conf->{MALWARE_SCANNING}->{default_action} : "N/A"; my $days_to_keep = ( defined $i360Conf->{MALWARE_CLEANUP}->{keep_original_files_days} ) ? $i360Conf->{MALWARE_CLEANUP}->{keep_original_files_days} : "N/A"; my $webshield_acct_protection = ( defined $i360Conf->{WEBSHIELD}->{panel_protection} ) ? $i360Conf->{WEBSHIELD}->{panel_protection} : "false"; $imunify_licenseType =~ s/Plus/\+/; SSP::Util::print_info("$imunify_licenseType [Version: $imunify_version ] is running: "); SSP::Util::print_normal("Default Action: $default_action"); my @trimFiles; my @quarantineFiles; @trimFiles = glob('/var/imunify360/cleanup_storage/*') unless ( !-d '/var/imunify360/cleanup_storage' ); @quarantineFiles = glob('/home/.imunify.quarantined/*') unless ( !-d '/home/.imunify.quarantined' ); SSP::Util::print_normal("\t\\_ cleanup (trim) files found within /var/imunify360/cleanup_storage [ will be automatically removed within $days_to_keep days ]") unless ( scalar @trimFiles == 0 ); SSP::Util::print_normal("\t\\_ quarantined files found within /home/.imunify.quarantined") unless ( scalar @quarantineFiles == 0 ); if ( $imunify_licenseType eq "imunify360" or $imunify_licenseType eq "imunify360Trial" ) { SSP::Util::print_normal("\t\\_ Issues with installing PECL extensions or running PHP's composer? Try setting Imunify360's Proactive Defense to log mode"); } if ( $webshield_acct_protection eq 'true' ) { SSP::Util::print_info("Imunify360's Webshield: "); SSP::Util::print_normal("cPanel Account Protection is enabled. May cause WebSocket handshake issues in cPanel's Terminal."); } } sub check_for_phpini_directives { return unless ( -d '/etc/cpanel/ea4/phpini_directives' ); my @dir_contents = glob('/etc/cpanel/ea4/phpini_directives/*.yaml'); my $yamlcnt = @dir_contents; if ( $yamlcnt > 0 ) { SSP::Util::print_info("Custom PHPINI: "); SSP::Util::print_normal("$yamlcnt YAML directives found in /etc/cpanel/ea4/phpini_directives\n\t\\_Won't overwrite existing directives, only add to with custom PHP modules."); } } sub check_for_cpcloud_instance { my $homedir_wwwconf = SSP::Util::timed_run( 3, 'grep', 'remote-storage', '/etc/wwwacct.conf' ); return unless ($homedir_wwwconf); chomp($homedir_wwwconf); my ($homedir_value) = ( split( /\s+/, $homedir_wwwconf ) )[1]; my $hasCloud = ( $homedir_value =~ m{remote-storage} ) ? 1 : 0; return unless ($hasCloud); SSP::Util::print_info("cP Cloud Detected\n"); if ( -l '/usr/local/bin/kubectl' ) { my $get_nodes = SSP::Util::timed_run( 5, '/usr/local/bin/kubectl', 'get', 'nodes' ); my @get_nodes = split /\n/, $get_nodes; foreach my $node (@get_nodes) { chomp($node); SSP::Util::print_normal( GREEN "\t\\_ " . $node ); } SSP::Util::print_normal( YELLOW "\t\\_ Run: " . WHITE "kubectl get nodes -o wide" . YELLOW " for more information" ); } else { my $hasCloudDNS = ( -f '/etc/systemd/system/cpsc-service-dns.service' ) ? "Yes" : "No"; my $innodb_cluster = SSP::Util::timed_run( 3, 'mysql', '-BNe', 'SELECT @@hostname;' ); my $hasCloudSQL = ( $innodb_cluster =~ m{cpanel-innodb-cluster} ) ? "Yes" : "No"; SSP::Util::print_normal( RED "\t\\_ Could not find /usr/local/bin/kubectl" ); SSP::Util::print_normal( YELLOW "\t\\_ cPCloud DNS: " . CYAN $hasCloudDNS ); SSP::Util::print_normal( YELLOW "\t\\_ cPCloud Storage: " . CYAN $homedir_value ); SSP::Util::print_normal( YELLOW "\t\\_ cPCloud Database Storage: " . CYAN $hasCloudSQL ); } # check for NFS mount. if ( -e '/proc/fs/nfsfs/servers' ) { open( my $fh, '<', '/proc/fs/nfsfs/servers' ); while (<$fh>) { next if ( substr( $_, 0, 2 ) eq "NV" ); my ( $nfs_ver, $nfs_ip ) = ( split( /\s+/, $_ ) )[ 0, 4 ]; SSP::Util::print_normal( MAGENTA "\t\\_ NFS Version: $nfs_ver IP Address: $nfs_ip" ); } close($fh); } } sub get_autossl_provider { return unless ( SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ) ); return unless ( -s '/var/cpanel/autossl.json' ); my $raw = SSP::Util::timed_run( 3, 'cat', '/var/cpanel/autossl.json' ); my $autossl_data = SSP::Util::get_json_href($raw); return unless ( defined $autossl_data->{provider} ); my $autossl_provider = "Disabled"; SSP::Util::print_info("AutoSSL Provider: "); $autossl_provider = "cPanel by Sectigo" if ( $autossl_data->{provider} eq 'cPanel' ); $autossl_provider = "Let's Encrypt" if ( $autossl_data->{provider} eq 'LetsEncrypt' ); SSP::Util::print_normal($autossl_provider); } ############################## # END [INFO] CHECKS ############################## 1; SSP_INFO $fatpacked{"SSP/Misc.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'SSP_MISC'; #!/usr/bin/perl # SSP5 - System Status Probe (Misc module) # Find and print useful troubleshooting info on cPanel servers =head1 COPYRIGHT This software is Copyright 2024 WebPros International, LLC. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THE SOFTWARE LICENSED HEREUNDER IS PROVIDED "AS IS" AND WEBPROS INTERNATIONAL, LLC D/B/A/ CPANEL (CPANEL) HEREBY DISCLAIMS ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, RELATING TO THE SOFTWARE, ITS THIRD PARTY COMPONENTS, AND ANY DATA ACCESSED THEREFROM, OR THE ACCURACY, TIMELINESS, COMPLETENESS, OR ADEQUACY OF THE SOFTWARE, ITS THIRD PARTY COMPONENTS, AND ANY DATA ACCESSED THEREFROM, INCLUDING THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. CPANEL DOES NOT WARRANT THAT THE SOFTWARE OR ITS THIRD PARTY COMPONENTS ARE ERROR-FREE OR WILL OPERATE WITHOUT INTERRUPTION. IF THE SOFTWARE, ITS THIRD PARTY COMPONENTS, OR ANY DATA ACCESSED THEREFROM IS DEFECTIVE, YOU ASSUME THE SOLE RESPONSIBILITY FOR THE ENTIRE COST OF ALL REPAIR OR INJURY OF ANY KIND, EVEN IF CPANEL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DEFECTS OR DAMAGES. NO ORAL OR WRITTEN INFORMATION OR ADVICE GIVEN BY CPANEL, ITS AFFILIATES, LICENSEES, DEALERS, SUB-LICENSORS, AGENTS OR EMPLOYEES SHALL CREATE A WARRANTY OR IN ANY WAY INCREASE THE SCOPE OF ANY WARRANTY. =cut package SSP::Misc; use 5.006; use strict; use warnings; use Term::ANSIColor qw(:constants); $Term::ANSIColor::AUTORESET = 1; sub run { my $self = shift; $self->check_smtp_processes(); $self->check_for_mailscanner(); $self->check_for_varnish(); $self->check_for_3rdparty_nginx(); $self->check_for_apf(); $self->check_for_csf(); $self->check_for_prm(); $self->check_for_nsiv(); $self->check_for_les(); $self->check_for_1h(); $self->check_for_webmin(); $self->check_for_symantec(); $self->check_for_newrelic(); $self->check_for_multilevel_reseller(); $self->check_for_cpremote(); $self->check_for_whmxtra(); $self->check_for_opt_gsi_tools(); $self->check_for_pyxsoft_antimalware(); $self->check_for_pxshield(); $self->check_for_magicspam(); } sub print_tip { my $tips = SSP::Util::timed_run( 5, 'curl', '-s', 'https://ssp.cpanel.net/sspfunfacts.txt' ); return unless ($tips); my @tips = split /\n/, $tips; my $num = int rand scalar $#tips; print BOLD WHITE ON_BLACK "\tFun fact: $tips[$num]" . RESET . "\n\n"; } sub check_smtp_processes { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); my $ports = SSP::Util::get_lsof_port_href(); return unless scalar keys(%$ports); if ( !defined( $ports->{'25'} ) && !-f '/etc/eximdisable' ) { SSP::Util::print_warn('Exim: '); SSP::Util::print_warning('not disabled and does not appear to be up -- nothing listening on port 25'); } return if !defined( $ports->{'25'} ); my $procs = SSP::Util::get_process_pid_href(); for my $href ( @{ $ports->{'25'} } ) { my $pid = $href->{PID}; next unless defined $procs->{$pid}; my $cmd = $procs->{$pid}->{ARGS}; if ( $href->{PROTO} eq "TCP" && !( $cmd =~ m{ \A /usr/sbin/exim \b }xms ) ) { SSP::Util::print_3rdp('SMTP: '); SSP::Util::print_3rdp2( 'a process other than exim is listening on port 25 [' . $href->{IPV} . ' ' . $href->{IP} . '] [USER: ' . $href->{USER} . '] [CMD: ' . $cmd . '] [PID: ' . $pid . ']' ); } if ( $cmd =~ m{ assp\.pl }xms ) { SSP::Util::print_3rdp('ASSP: '); SSP::Util::print_3rdp2( 'assp.pl is listening on port 25 [PID: ' . $pid . ']' ); SSP::Util::print_3rdp2(' \_ SNI is not supported unless enabled in the plugin: http://www.grscripts.com/howtofaq.html -> "Does ASSP Support SNI?"'); } } } sub check_for_varnish { return unless SSP::Util::i_am('ea4'); return unless my $ports = SSP::Util::get_lsof_port_href(); return unless exists $ports->{'80'}; for my $ref ( @{ $ports->{'80'} } ) { if ( $ref->{'CMD'} =~ m{ \A varnish }xms ) { SSP::Util::print_3rdp('Varnish: '); SSP::Util::print_3rdp2('varnish is listening on port 80, known to break proxy subdomains. See "RareIssues" wiki article'); last; } } } sub check_for_3rdparty_nginx { return unless SSP::Util::i_am('ea4'); my %procs = SSP::Util::grep_process_cmd( qr{ nginx: \s master }xms, 'root' ); return unless grep { $procs{$_}->{ARGS} !~ /imunify360|bitninja/ } keys %procs; # Ignore imunify360 and bitninja nginx process my $rpms = SSP::Util::get_rpm_href(); if ( not exists $rpms->{'ea-nginx'} ) { # We support ea-nginx installs SSP::Util::print_3rdp('nginx: '); SSP::Util::print_3rdp2('A 3rd party NginX is running'); } } sub check_for_mailscanner { return unless SSP::Util::i_am('ea4'); my $running = SSP::Util::exists_process_cmd( qr{ MailScanner }xms, 'mailnull' ) ? 1 : 0; if ($running) { SSP::Util::print_3rdp('MailScanner: '); SSP::Util::print_3rdp2('is running'); } else { my $bin = '/usr/mailscanner/usr/sbin/MailScanner'; return unless ( -e $bin ); SSP::Util::print_3rdp('MailScanner: '); if ( -s '/etc/exim_outgoing.conf' ) { SSP::Util::print_warning('not running but /etc/exim_outgoing.conf file exists'); } my $version = SSP::Util::timed_run_trap_stderr( 0, $bin, '--version' ); return unless ( $version =~ m/failed--compilation/ ); SSP::Util::print_warning('is installed but failed to compile, an update may be required to start the service, see UPS-210'); } } sub check_for_apf { my $chkconfig_apf = SSP::Util::timed_run( 0, 'chkconfig', '--list', 'apf' ); if ($chkconfig_apf) { if ( $chkconfig_apf =~ /3:on/ ) { SSP::Util::print_3rdp('APF: '); SSP::Util::print_3rdp2('installed, may be enabled.'); } } } sub check_for_csf { my $csf_conf_file = '/etc/csf/csf.conf'; return unless -s $csf_conf_file; SSP::Util::print_3rdp('CSF: '); my $lfd = SSP::Util::exists_process_cmd( qr{ lfd }xms, 'root' ) ? 'is' : 'is not'; SSP::Util::print_3rdp2( 'installed, LFD ' . $lfd . ' running' ); my ( %csf_values, $csf_key, $csf_value ); open( my $fh, '<', $csf_conf_file ); while (<$fh>) { chomp($_); next if ( substr( $_, 0, 1 ) eq "#" ); next if ( $_ eq "" ); ( $csf_key, $csf_value ) = ( split( /\s=\s/, $_ ) ); next unless ( defined($csf_key) && defined($csf_value) ); chomp($csf_key); chomp($csf_value); $csf_value =~ s/\"//g; $csf_values{$csf_key} = $csf_value; } close($fh); my %CPCONF; %CPCONF = SSP::Util::get_cpanel_conf(); my $smtpmailgidonly_status = 0; if ( defined $CPCONF{'smtpmailgidonly'} && $CPCONF{'smtpmailgidonly'} == 1 ) { $smtpmailgidonly_status = 1; } if ( defined( $csf_values{'SMTP_BLOCK'} ) ) { if ( $csf_values{'SMTP_BLOCK'} == 0 && $smtpmailgidonly_status ) { # Tweak settings enabled, recommend they disable and use SMTP_BLOCK in csf instead SSP::Util::print_warning(qq{\t\\_ Outgoing SMTP restriction enabled. Please consider using SMTP_BLOCK in CSF instead. See https://go.cpanel.net/smtprestrictions}); } if ( $csf_values{'SMTP_BLOCK'} == 1 && $smtpmailgidonly_status ) { # Both options are enabled - Conflict - Recommend they disable Tweak Setting option SSP::Util::print_warning(qq{\t\\_ Both Outgoing SMTP restrictions and SMTP_BLOCK in CSF enabled - Please consider only using SMTP_BLOCK. See https://go.cpanel.net/smtprestrictions}); } } if ( defined( $csf_values{'LF_SCRIPT_ACTION'} && defined( $csf_values{'LF_SCRIPT_ALERT'} ) ) ) { if ( -x $csf_values{'LF_SCRIPT_ACTION'} && $csf_values{'LF_SCRIPT_ALERT'} == 1 ) { SSP::Util::print_warning(qq{\t\\_ LF_SCRIPT_ACTION is set within /etc/csf/csf.conf and LF_SCRIPT_ALERT is enabled\n\t\\_ Seeing a directory chmod 0 and immutable? - Please review /etc/csf/csf.conf for LF_SCRIPT_ACTION}); } } } sub check_for_prm { if ( -e '/usr/local/prm' ) { SSP::Util::print_3rdp('PRM: '); SSP::Util::print_3rdp2('PRM exists at /usr/local/prm'); } } sub check_for_nsiv { if ( -e '/usr/local/nsiv' ) { SSP::Util::print_3rdp('NSIV: '); SSP::Util::print_3rdp2('Network Socket Inode Validation exists at /usr/local/nsiv'); } } sub check_for_les { if ( -e '/usr/local/sbin/les' ) { SSP::Util::print_3rdp('LES: '); SSP::Util::print_3rdp2('Linux Environment Security is installed at /usr/local/sbin/les'); } } sub check_for_1h { return unless -d '/usr/local/1h'; my $guardian = SSP::Util::exists_process_cmd( qr{ Guardian }xms, 'root' ) ? 'running' : 'not running'; SSP::Util::print_3rdp('1H Software: '); SSP::Util::print_3rdp2("/usr/local/1h exists. Guardian process: [ $guardian ]"); } sub check_for_webmin { return unless my $ports = SSP::Util::get_lsof_port_href(); return unless exists( $ports->{'10000'} ); for my $ref ( @{ $ports->{'10000'} } ) { if ( index( $ref->{'CMD'}, 'miniserv.pl' ) == 0 ) { SSP::Util::print_3rdp('Webmin: '); SSP::Util::print_3rdp2('Webmin is running and is listening on port 10000'); last; } } } sub check_for_symantec { return unless SSP::Util::exists_process_cmd( qr{ symantec_antivirus }xms, 'root' ); SSP::Util::print_3rdp('Symantec: '); SSP::Util::print_3rdp2('found symantec_antivirus in process list'); } sub check_for_haproxy { return unless SSP::Util::exists_process_cmd( qr{ haproxy }xms, 'haproxy' ); SSP::Util::print_3rdp('HAProxy: '); SSP::Util::print_3rdp2('found haproxy in process list'); } sub check_for_newrelic { return unless SSP::Util::exists_process_cmd(qr{ newrelic-daemon }xms); SSP::Util::print_3rdp('newrelic-daemon: '); SSP::Util::print_3rdp2('found in process list. Caused server stability issues in 4396009'); } sub check_for_multilevel_reseller { return unless SSP::Util::i_am('cpanel'); my @ml_plugins = qw/ zamfoo whmreseller whmphp whmamp /; my $cgi_root = '/usr/local/cpanel/whostmgr/docroot/cgi'; foreach my $plugin (@ml_plugins) { if ( -d "$cgi_root/$plugin" ) { SSP::Util::print_3rdp( uc($plugin) . ' ' ); SSP::Util::print_3rdp2('is installed. Multi-level reseller setups are not supported'); } } } sub check_for_cpremote { return unless SSP::Util::i_am('cpanel'); return unless -e '/var/spool/cron/root'; open my $file_fh, '<', '/var/spool/cron/root' or return; while (<$file_fh>) { if (m#/scripts/cpremotebackup#) { SSP::Util::print_3rdp('cpremote: '); SSP::Util::print_3rdp2('installed. third party backup software (cron job found for root)'); last; } } close $file_fh; } sub check_for_whmxtra { return unless SSP::Util::i_am('cpanel'); my $ionsh = '/usr/local/cpanel/whostmgr/docroot/themes/x/xtra/functions/ion.sh'; return if !-f $ionsh; SSP::Util::print_3rdp('WHMXtra: '); SSP::Util::print_3rdp2("$ionsh exists. 'cPanel PHP loader' Tweak Settings or php.ini settings reverted? See 4622167, 4628203"); } sub check_for_opt_gsi_tools { return unless SSP::Util::i_am('cpanel'); my $dir = '/opt/gsi-tools'; return if !-d $dir; SSP::Util::print_3rdp("$dir: "); SSP::Util::print_3rdp2('found! These admin scripts have been known to automatically lock user accounts.'); } sub check_for_pyxsoft_antimalware { return unless SSP::Util::i_am('cpanel'); my $dir = '/usr/share/ilabs_antimalware'; return if !-d $dir; SSP::Util::print_3rdp("$dir: "); SSP::Util::print_3rdp2('found! This has been known to cause failed uploads, segfaults on PHP-FPM, and 500 ISE. See TECH-174'); } sub check_for_pxshield { return unless SSP::Util::i_am('cpanel'); my $file = '/usr/local/cpanel/3rdparty/bin/pxshield_reload'; my $file2 = '/etc/pxshield.ini'; if ( -x $file && -e $file2 ) { SSP::Util::print_3rdp("Pyxsoft Pxshield: "); SSP::Util::print_3rdp2('found! Can block Apache requests as false-positive cyber attacks. See: CX-561'); } } sub check_for_magicspam { return unless SSP::Util::i_am('cpanel'); my $dir = '/etc/magicspam'; return if !-d $dir; my $running = SSP::Util::exists_process_cmd(qr{ magicspam }xms) ? 'and' : 'but not'; SSP::Util::print_3rdp('MagicSpam: '); SSP::Util::print_3rdp2( 'installed ' . "$running" . ' running. See TECH-725' ); } sub sshcheck { no warnings; ## no critic (TestingAndDebugging::ProhibitNoWarnings) my $sshd_settings = SSP::Util::timed_run( 4, 'sshd', '-T' ); my @sshd_settings = split /\n/, $sshd_settings; my %sshd_conf = map { split( /\s+/, $_ ) } $sshd_settings; my $osbrand; if ( !-s '/etc/os-release' ) { $osbrand = "UNKNOWN"; } else { my $osrelease = SSP::Util::timed_run( 3, 'grep', 'PRETTY_NAME', '/etc/os-release' ); ($osbrand) = ( split( '\"', $osrelease ) )[1]; } my $wheel_required = ""; if ( $sshd_conf{'permitrootlogin'} =~ m/^[Pp][Rr][Oo][Hh][Ii][Bb][Ii][Tt]\-[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Ww][Ii][Tt][Hh][Oo][Uu][Tt]\-[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]/ ) { $wheel_required = MAGENTA " [ ssh-key required ] (default)"; } if ( $sshd_conf{'permitrootlogin'} =~ m/^[Ff][Oo][Rr][Cc][Ee][Dd]\-[Cc][Oo][Mm][Mm][Aa][Nn][Dd][Ss]\-[Oo][Nn][Ll][Yy]/ ) { $wheel_required = MAGENTA " [ Very limited SSH access permitted (probably won't work for Support) ]"; } if ( $sshd_conf{'permitrootlogin'} =~ m/^[Nn][Oo]/ ) { if ( $osbrand =~ m/ubuntu/i ) { $wheel_required = MAGENTA " [ A sudo user is required ]"; } else { $wheel_required = MAGENTA " [ A wheel user is required ]"; } } my $chalresp = ""; if ( $sshd_conf{'challengeresponseauthentication'} =~ m/^[Yy][Ee][Ss]/ ) { $chalresp = MAGENTA " [ SHOULD BE NO ]"; } my $passauthno = ""; if ( $sshd_conf{'passwordauthentication'} =~ m/^[Nn][Oo]/ ) { $passauthno = MAGENTA " [ ssh-key will need to be authorized ]"; } my ( @denygroups, @allowgroups, @denyusers, @allowusers ); foreach my $line (@sshd_settings) { chomp($line); next unless ( $line =~ m/denygroups/ ); my ($denygroup) = ( split( /\s+/, $line ) )[1]; push @denygroups, $denygroup . " "; } foreach my $line (@sshd_settings) { chomp($line); next unless ( $line =~ m/allowgroups/ ); my ($allowgroup) = ( split( /\s+/, $line ) )[1]; push @allowgroups, $allowgroup . " "; } foreach my $line (@sshd_settings) { chomp($line); next unless ( $line =~ m/denyusers/ ); my ($denyuser) = ( split( /\s+/, $line ) )[1]; push @denyusers, $denyuser . " "; } foreach my $line (@sshd_settings) { chomp($line); next unless ( $line =~ m/allowusers/ ); my ($allowuser) = ( split( /\s+/, $line ) )[1]; push @allowusers, $allowuser . " "; } my @cpanel_ips = qw( 184.94.197.2 184.94.197.3 184.94.197.4 184.94.197.5 184.94.197.6 208.74.123.98 ); print BOLD BLUE "Current sshd configuration (sshd -T):\n"; print CYAN "\t\\_ Port: " . YELLOW $sshd_conf{'port'} . MAGENTA " [ 22 is default]\n"; print CYAN "\t\\_ Maxauth Tries: " . YELLOW $sshd_conf{'maxauthtries'} . MAGENTA " [6 is default]\n"; print CYAN "\t\\_ Permit Root Login: " . YELLOW $sshd_conf{'permitrootlogin'} . " " . $wheel_required . "\n"; print CYAN "\t\\_ Pubkey Authentication: " . YELLOW $sshd_conf{'pubkeyauthentication'} . "\n"; print CYAN "\t\\_ Password Authentication: " . YELLOW $sshd_conf{'passwordauthentication'} . " " . $passauthno . "\n"; print CYAN "\t\\_ Challenge Response Authentication: " . YELLOW $sshd_conf{'challengeresponseauthentication'} . " " . $chalresp . "\n"; print CYAN "\t\\_ Authorized Keys File: " . YELLOW $sshd_conf{'authorizedkeysfile'} . "\n"; print CYAN "\t\\_ UsePAM: " . YELLOW $sshd_conf{'usepam'} . "\n"; print CYAN "\t\\_ UseDNS " . YELLOW $sshd_conf{'usedns'} . "\n"; print CYAN "\t\\_ Login Grace Time " . YELLOW $sshd_conf{'logingracetime'} . "\n"; print CYAN "\t\\_ X11 Forwarding " . YELLOW $sshd_conf{'x11forwarding'} . "\n"; print CYAN "\t\\_ Deny Groups " . YELLOW @denygroups unless ( !@denygroups ); print "\n" unless ( !@denygroups ); print CYAN "\t\\_ Allow Groups " . YELLOW @allowgroups unless ( !@allowgroups ); print "\n" unless ( !@allowgroups ); print CYAN "\t\\_ Deny Users " . YELLOW @denyusers unless ( !@denyusers ); print "\n" unless ( !@denyusers ); print CYAN "\t\\_ Allow Users " . YELLOW @allowusers unless ( !@allowusers ); print "\n" unless ( !@allowusers ); print "\n"; my $wheel_group; my $wheel_users; if ( $osbrand =~ m/ubuntu/i ) { print BOLD BLUE "Sudo Group Users: "; $wheel_group = SSP::Util::timed_run( 2, 'grep', 'sudo', '/etc/group' ); ($wheel_users) = ( split( /\:/, $wheel_group ) )[3]; } else { print BOLD BLUE "Wheel Group Users: "; $wheel_group = SSP::Util::timed_run( 2, 'grep', 'wheel', '/etc/group' ); ($wheel_users) = ( split( /\:/, $wheel_group ) )[3]; } print CYAN $wheel_users unless ( !$wheel_users ); print "\n"; # Check for custom conf files under /etc/ssh/sshd_config.d/ (currently only found on Ubuntu) if ( -d "/etc/ssh/sshd_config.d/" ) { print BOLD BLUE "Found custom " . YELLOW "/etc/ssh/sshd_config.d/" . BOLD BLUE " directory - Looking for *.conf files:\n"; opendir( my $dir_fh, "/etc/ssh/sshd_config.d/" ); my $conf_found = 0; while ( my $file = readdir($dir_fh) ) { if ( $file =~ m{\.conf} ) { print CYAN "\t\\_ $file\n"; $conf_found = 1; } } closedir $dir_fh; print CYAN "\t\\_ None\n" unless ($conf_found); print "\n"; } print BOLD BLUE "Relevant information within the /etc/sudoers file:\n"; if ( open( my $fh, "<", "/etc/sudoers" ) ) { while (<$fh>) { chomp($_); next if ( $_ =~ m/^#|^$/ ); print CYAN "\t\\_ $_\n" unless ( $_ =~ m{Defaults} ); } close($fh); } else { print RED "Error opening the /etc/sudoers file ($!)\n"; } print "\n"; if ( -e "/var/run/cphulkd_processor.pid" ) { print CYAN "cPHulkd is running...\n"; } else { print CYAN "cPHulkd is not running\n"; } print "\n"; print BOLD BLUE "Checking The Firewall [ Includes Host Access Control on CentOS/CloudLinux/AlmaLinux 8+ ]:\n"; my @firewall_data; my $firewall_data; if ( SSP::Util::i_am_one_of( 'centos', 'cloudlinux', 'almalinux' ) and SSP::Util::os_version_is(qw ( >= 8 )) ) { $firewall_data = SSP::Util::timed_run( 5, 'nft', 'list', 'ruleset', 'inet' ); } if ( SSP::Util::i_am_one_of( 'centos', 'cloudlinux' ) and SSP::Util::os_version_is(qw ( <= 7 )) ) { $firewall_data = SSP::Util::timed_run( 5, 'iptables', '-L', '-n' ); } if ( SSP::Util::i_am('ubuntu') ) { $firewall_data = SSP::Util::timed_run( 5, 'iptables', '-L', '-n' ); } @firewall_data = split /\n/, $firewall_data; my @blocked; my $ip_blocked; my $is_immutable = 0; foreach my $cpanel_auth_ip (@cpanel_ips) { chomp($cpanel_auth_ip); foreach my $ip_rule (@firewall_data) { chomp($ip_rule); if ( $ip_rule =~ m/$cpanel_auth_ip/ && $ip_rule =~ m/DROP|REJECT/i ) { push( @blocked, $ip_rule ); $ip_blocked = 1; last; } } } if ($ip_blocked) { print RED "\t\\_ cPanel IP's may be blocked via firewall/cphulkd\n"; foreach my $block (@blocked) { chomp($block); $block =~ s/^\s+|\s+$//g; print MAGENTA "\t\t\\_ $block\n"; } } else { print GREEN "\t\\_ No firewall DROP/REJECT rules for any cPanel IP's detected\n"; print MAGENTA "\t\\_ (Note: this does not account for possible external firewalls/security devices)\n"; } print "\n"; if ( -f "/etc/hosts.allow" ) { print BOLD BLUE "Checking /etc/hosts.allow file:\n"; my $showHeader = 0; my $issues = 0; if ( open( my $fh, "<", "/etc/hosts.allow" ) ) { my @hostsallow = <$fh>; close($fh); foreach my $line (@hostsallow) { chomp($line); next if ( $line =~ m/^#|^$/ ); my $coloncnt = $line =~ tr/\://; if ( $coloncnt > 2 ) { print RED "$line\n\t\\_ has $coloncnt delimeters. Access will be denied if more than 2 delimiters exist within a line.\n" unless ($showHeader); $issues = 1; } } $showHeader = 0; foreach my $line (@hostsallow) { chomp($line); next if ( $line =~ m/^#|^$/ ); if ( $line =~ m/\: all \:/gi && $line =~ m/DENY/i ) { print RED "$line\n\t\\_ Found ALL line set to deny\n" unless ($showHeader); $showHeader = 1; $issues = 1; } } $showHeader = 0; foreach my $line (@hostsallow) { chomp($line); next if ( $line =~ m/^#|^$/ ); foreach my $cpanel_auth_ip (@cpanel_ips) { chomp($cpanel_auth_ip); if ( $line =~ m/$cpanel_auth_ip/ && $line =~ m/DENY/i ) { print RED "$line\n\t\\_ cPanel IP address found set to deny\n" unless ($showHeader); $showHeader = 1; $issues = 1; } } } close($fh); } print CYAN "\t\\_No issues found within the /etc/hosts.allow file\n" if ( $issues == 0 ); } print "\n"; if ( -e '/etc/security/access.conf' ) { print BOLD BLUE "Checking /etc/security/access.conf file:\n"; my $showHeader = 0; if ( open( my $fh, "<", '/etc/security/access.conf' ) ) { while (<$fh>) { chomp($_); next if ( $_ =~ m/^#|^$/ ); foreach my $cpanel_auth_ip (@cpanel_ips) { chomp($cpanel_auth_ip); if ( $_ =~ m/$cpanel_auth_ip/ && substr( $_, 0, 1 ) eq '-' ) { print RED "\t\\_ cPanel IP address found with access denied (-)\n" unless ($showHeader); $showHeader = 1; print MAGENTA "\t\t\\_ $_\n"; } } } close($fh); } print CYAN "\t\\_No cPanel IP's found in /etc/security/access.conf file\n" if ( $showHeader == 0 ); } print "\n"; print BOLD BLUE "Checking last 10 lines from /usr/local/cpanel/logs/login_log:\n"; SSP::Util::_tail_log_file('/usr/local/cpanel/logs/login_log'); print "\n"; print BOLD BLUE "Checking last 10 lines from /usr/local/cpanel/logs/cphulkd.log:\n"; SSP::Util::_tail_log_file('/usr/local/cpanel/logs/cphulkd.log'); print "\n"; my $pass_exp = SSP::Util::timed_run( 3, 'chage', '-l', 'root' ); my @pass_exp_lines = split /\n/, $pass_exp; foreach my $passline (@pass_exp_lines) { my ( $passvar, $passval ) = ( split( /\: /, $passline ) ); next unless ( $passvar =~ m/Password expires/ ); print BOLD BLUE "Root Password Expires: "; print CYAN $passval . "\n\n"; } my $rootsshpath = '/root/.ssh'; my $authkeysfile = '/root/.ssh/authorized_keys'; print BOLD BLUE "Checking the $rootsshpath directory and $authkeysfile file...\n"; if ( open my $fh, "<", $authkeysfile ) { ## no critic (InputOutput::RequireBriefOpen) use File::stat; my $authkeysfile_issues = 0; my $attributes = SSP::Util::timed_run( 0, 'lsattr', $rootsshpath ); $is_immutable = 1 if ( $attributes =~ m/^-[-]*(?:a|i)/ ); my $authkey_obj = stat $rootsshpath; my $authkeysUID = $authkey_obj->uid; my $authkeysGID = $authkey_obj->gid; my $mode = $authkey_obj->mode; my $checkmode = "0700"; if ( sprintf( "%04o", $mode & 07777 ) ne $checkmode ) { print CYAN "\t\\_ Permissions on the $rootsshpath directory are not $checkmode [ "; printf( "%04o", $mode & 07777 ); print CYAN " ]\n"; $authkeysfile_issues = 1; } if ( $authkeysGID > 0 || $authkeysUID > 0 ) { print CYAN "\t\\_ The $rootsshpath directory has a user:group that is not root:root [ " . WHITE getpwuid($authkeysUID) . ":" . WHITE getgrgid($authkeysGID) . CYAN " ]\n"; $authkeysfile_issues = 1; } $attributes = SSP::Util::timed_run( 0, 'lsattr', $authkeysfile ); $is_immutable = 1 if ( $attributes =~ m/^-[-]*(?:a|i)/ ); $authkey_obj = stat $authkeysfile; $authkeysUID = $authkey_obj->uid; $authkeysGID = $authkey_obj->gid; $mode = $authkey_obj->mode; $checkmode = "0600"; if ( sprintf( "%04o", $mode & 07777 ) ne $checkmode ) { print CYAN "\t\\_ Permissions on the $authkeysfile file are not $checkmode [ "; printf( "%04o", $mode & 07777 ); print CYAN " ]\n"; $authkeysfile_issues = 1; } if ( $authkeysGID > 0 || $authkeysUID > 0 ) { print CYAN "\t\\_ The $authkeysfile file has a user:group that is not root:root [ " . WHITE getpwuid($authkeysUID) . ":" . WHITE getgrgid($authkeysGID) . CYAN " ]\n"; $authkeysfile_issues = 1; } if ($is_immutable) { print CYAN "\t\\_ /root/.ssh/authorized_keys file is immutable\n"; $authkeysfile_issues = 1; } if ( $authkeysfile_issues == 0 ) { print CYAN "\t\\_ No issues found\n"; } print BOLD BLUE "\ncPanel Support Tickets that are authorized via the $authkeysfile file (might be legacy ticket numbers):\n"; my $tickets_found = 0; while (<$fh>) { next unless ( $_ =~ m/cpanel.net_/ ); my ($sshname) = ( split( /\s+/, $_ ) )[2]; my ($auth_ticket_id) = ( split( /_/, $sshname ) )[0]; print CYAN "\t\\_ " . $auth_ticket_id . "\n"; $tickets_found = 1; } close($fh); if ( $tickets_found == 0 ) { print CYAN "\t\\_ None\n" unless ($tickets_found); } } else { print RED "\t\\_ Error opening the $authkeysfile file ($!)\n"; } print "\n"; my $is_sshd_listening = SSP::Util::timed_run( 6, 'lsof', "-itcp:$sshd_conf{'port'}" ); my @sshd_output = split /\n/, $is_sshd_listening; print BOLD BLUE "Checking to see if sshd is listening on port $sshd_conf{'port'} \n"; foreach my $sshd_line (@sshd_output) { chomp($sshd_line); next unless ( $sshd_line =~ m/LISTEN/ ); print CYAN "\t\\_ " . $sshd_line . "\n"; } print "\n"; my $hostname = SSP::Util::get_hostname(); my $external_ip_address = SSP::Util::get_external_ip(); my $loadavg = SSP::Util::timed_run( 2, 'cat', '/proc/loadavg' ); my ( $la1, $la5, $la15 ) = ( split( /\s+/, $loadavg ) ); my $corecount = SSP::Util::timed_run( 2, 'nproc' ); chomp($corecount); print BOLD BLUE "OS: " . CYAN $osbrand . BOLD BLUE " Hostname: " . CYAN $hostname . BOLD BLUE " [ " . CYAN $external_ip_address . BOLD BLUE " ] " . BOLD BLUE "Load: " . CYAN $la1 . " " . $la5 . " " . $la15 . BOLD BLUE " [Cores: " . CYAN $corecount . BOLD BLUE "]\n"; print "\n"; print "Please paste the above information into your ticket currently open with cPanel Support.\n"; print "\n"; } 1; SSP_MISC $fatpacked{"SSP/Security.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'SSP_SECURITY'; #!/usr/bin/perl # SSP - System Status Probe (Security module) # Find and print useful troubleshooting info on cPanel servers =head1 COPYRIGHT This software is Copyright 2024 WebPros International, LLC. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THE SOFTWARE LICENSED HEREUNDER IS PROVIDED "AS IS" AND WEBPROS INTERNATIONAL, LLC D/B/A/ CPANEL (CPANEL) HEREBY DISCLAIMS ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, RELATING TO THE SOFTWARE, ITS THIRD PARTY COMPONENTS, AND ANY DATA ACCESSED THEREFROM, OR THE ACCURACY, TIMELINESS, COMPLETENESS, OR ADEQUACY OF THE SOFTWARE, ITS THIRD PARTY COMPONENTS, AND ANY DATA ACCESSED THEREFROM, INCLUDING THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. CPANEL DOES NOT WARRANT THAT THE SOFTWARE OR ITS THIRD PARTY COMPONENTS ARE ERROR-FREE OR WILL OPERATE WITHOUT INTERRUPTION. IF THE SOFTWARE, ITS THIRD PARTY COMPONENTS, OR ANY DATA ACCESSED THEREFROM IS DEFECTIVE, YOU ASSUME THE SOLE RESPONSIBILITY FOR THE ENTIRE COST OF ALL REPAIR OR INJURY OF ANY KIND, EVEN IF CPANEL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DEFECTS OR DAMAGES. NO ORAL OR WRITTEN INFORMATION OR ADVICE GIVEN BY CPANEL, ITS AFFILIATES, LICENSEES, DEALERS, SUB-LICENSORS, AGENTS OR EMPLOYEES SHALL CREATE A WARRANTY OR IN ANY WAY INCREASE THE SCOPE OF ANY WARRANTY. =cut package SSP::Security; use 5.006; use strict; use warnings; use File::stat; use Term::ANSIColor qw(:constants); $Term::ANSIColor::AUTORESET = 1; our $RUN_STATE; our $CRIT_BUFFER_HACKED; our %CPCONF; # cpanel.config our $ORIGINAL_PATH; our %MEMOIZE_CACHE; our $CPANEL_LICENSE_FILE = '/usr/local/cpanel/cpanel.lisc'; sub run { my $self = shift; SSP::Util::init(); if ( SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ) ) { %CPCONF = SSP::Util::get_cpanel_conf(); } $self->check_for_susp_rc_modules(); $self->new_cvechecks(); $self->check_for_bash_secadv_20140924(); $self->all_malware_checks(); $self->check_for_mounted_cpanel_lisc(); } sub check_for_hacked_server_touchfile { return unless SSP::Util::i_am('cptech'); my $docdir = '/usr/share/doc'; return unless -d $docdir; opendir( my $fh, $docdir ) or return; # .cp.jeff.2014-04-09_10.5.40.209_1234567 my @touchfiles = grep { /^\.cp\.([^\d]+)\.(\d{4}-\d{2}-\d{2})_([^_]+)_(\d+)$/ } readdir $fh; closedir $fh; return if scalar @touchfiles == 0; SSP::Security::print_generic_hack_predef('HACKED SERVER'); for my $touchfile (@touchfiles) { if ( $touchfile =~ /^\.cp\.([^\d]+)\.(\d{4}-\d{2}-\d{2})_([^_]+)_(\d+)$/ ) { my ( $cptech, $date, $ipaddr, $ticket ) = ( $1, $2, $3, $4 ); $date =~ s#-#/#g; $cptech = ucfirst $cptech; SSP::Util::print_critical( "\tL3: $cptech reported this server at $ipaddr as compromised on $date local server time in ticket $ticket", \$CRIT_BUFFER_HACKED ); if ( !grep { /^$ipaddr$/ } @{ SSP::Util::get_local_ipaddrs_aref() } ) { SSP::Util::print_critical( "\t \\_ NOTE: IP address $ipaddr not found on the server!", \$CRIT_BUFFER_HACKED ); } } } SSP::Util::print_critical(); } sub new_cvechecks { my $raw = SSP::Util::timed_run( 5, 'curl', '-s', '-4', 'https://raw.githubusercontent.com/CpanelInc/tech-CSI/master/cve_data.json' ); return unless ($raw); my $json_output = SSP::Util::get_json_href($raw); my $showHeader = 0; my $lastPkg = ""; for my $cvedata ( @{$json_output} ) { next if ( SSP::Util::is_os_vulnerable( $cvedata->{OS_Vulnerable} ) == 0 ); my $installed = SSP::Util::is_installed( $cvedata->{Package_Name} ); next unless ($installed); my $pkg = SSP::Util::is_kernel( $cvedata->{Package_Name} ); chomp( my $pkgver = SSP::Util::get_pkg_version($pkg) ); if ( $pkgver eq "" ) { SSP::Util::print_crit('PACKAGE VERSION ISSUE!'); SSP::Util::print_critical(" - $pkg installed but version wasn't detected! CVE Check Failed!"); next; } my $found_in_changelog = SSP::Util::found_in_changelog( $pkg, $cvedata->{CVE_ID} ); next unless ( !$found_in_changelog ); my $op1 = '>='; chomp( $cvedata->{Patched_Version} ); next if ( SSP::Util::version_compare( $pkgver, $op1, $cvedata->{Patched_Version} ) ); my $op2 = '<'; next if ( SSP::Util::version_compare( $pkgver, $op2, $cvedata->{First_Vulnerable_Version} ) ); SSP::Util::print_crit('Possible CVE Vulnerabilities Detected: ') unless ($showHeader); SSP::Util::print_critical("\nUse the PREDEF::Security::CVE::Vulnerabilities predefined macro.") unless ($showHeader); $showHeader = 1; my $digitpkgver; my $alphapkgver; if ( $pkg =~ m{openssl} && $pkgver < 3 ) { $digitpkgver = SSP::Util::digit_to_alpha($pkgver); $alphapkgver = SSP::Util::alpha_to_digit( $cvedata->{First_Vulnerable_Version} ); SSP::Util::print_critical( CYAN " \\_ Package: " . YELLOW $pkg . CYAN " Version running: " . YELLOW $pkgver . " (Converted to: " . $digitpkgver . " )" ) unless ( $pkg eq $lastPkg ); $lastPkg = $pkg; SSP::Util::print_critical( CYAN "\t\\_ First version reported to be vulnerable: " . YELLOW $alphapkgver . " (Converted to: " . $cvedata->{First_Vulnerable_Version} . " )" ); $alphapkgver = SSP::Util::alpha_to_digit( $cvedata->{Patched_Version} ); SSP::Util::print_critical( CYAN "\t\\_ CVE ID/Case ID: " . YELLOW $cvedata->{CVE_ID} . CYAN " Patched in version: " . GREEN $alphapkgver . " (Converted from " . $cvedata->{Patched_Version} . " )" ); } else { $digitpkgver = ''; $alphapkgver = ''; SSP::Util::print_critical( CYAN " \\_ Package: " . YELLOW $pkg . CYAN " Version running: " . YELLOW $pkgver ) unless ( $pkg eq $lastPkg ); $lastPkg = $pkg; SSP::Util::print_critical( CYAN "\t\\_ First version reported to be vulnerable: " . YELLOW $cvedata->{First_Vulnerable_Version} ); SSP::Util::print_critical( CYAN "\t\\_ CVE ID/Case ID: " . YELLOW $cvedata->{CVE_ID} . CYAN " Patched in version: " . GREEN $cvedata->{Patched_Version} ); } if ($found_in_changelog) { SSP::Util::print_critical( BLUE "\t\\_ Method used: " . WHITE "zgrep -E '" . $cvedata->{CVE_ID} . "' /usr/share/doc/" . $pkg . "/changelog.Debian.gz" ) unless ( !SSP::Util::i_am('ubuntu') ); SSP::Util::print_critical( BLUE "\t\\_ Method used: " . WHITE "rpm -q --changelog " . $pkg . " | grep -E '" . $cvedata->{CVE_ID} . "'" ) unless ( SSP::Util::i_am('ubuntu') ); } else { if ( $pkg =~ m{openssl} && $pkgver < 3 ) { SSP::Util::print_critical( BLUE "\t\\_ Method used: " . WHITE "Version Check: Running: $pkgver [ $digitpkgver ] <= Patched: " . $cvedata->{Patched_Version} . " [ $alphapkgver ]" ); } else { SSP::Util::print_critical( BLUE "\t\\_ Method used: " . WHITE "Version Check: Running: $pkgver <= Patched: " . $cvedata->{Patched_Version} ); } } } if ( $showHeader > 0 ) { SSP::Util::print_critical( MAGENTA "The above CAN report false-positives if the RPM DB is corrupt." ) unless ( SSP::Util::i_am('ubuntu') ); } } sub check_for_bash_secadv_20140924 { chomp( my $bash_output = SSP::Util::timed_run( 0, 'env x=\'() { :;}; echo vulnerable\' bash -c ""' ) ); return if !( $bash_output =~ m{ vulnerable }xms ); SSP::Util::print_critical(); SSP::Util::print_crit('Installed \'bash\' shell is vulnerable to remote code injection. Verify by running the following at a shell prompt, it should return "vulnerable":'); SSP::Util::print_critical(); SSP::Util::print_critical(' env x=\'() { :;}; echo vulnerable\' bash -c ""'); SSP::Util::print_critical('Send customer this premade: "SECURITY - Bash advisory 2014-09-24 - Discovery"'); SSP::Util::print_critical(); } ### BEGIN MALWARE CHECKS ### sub all_malware_checks { look_for_suspicious_files(); check_sha256sums(); check_processes(); check_for_fritzfrog_botnet(); check_for_cdorked_A(); check_for_cdorked_B(); check_for_libkeyutils_symbols(); check_for_ebury_ssh_shmem(); check_for_dragnet(); check_for_shenanigans(); check_for_xbash(); check_passwd_hiddenwasp_dirtycow(); check_for_malicious_root_cron(); check_for_spoofed_kernel_modules(); check_binaries_for_shellcode(); check_for_bad_perms_auth_keys(); check_changepasswd_modules(); check_for_missing_ps_cmd(); } sub check_sha256sums { my @binaries = qw( /usr/bin/ssh /usr/sbin/sshd /usr/sbin/named /usr/bin/ssh-add ); my $libkeyutils_files_ref = SSP::Util::build_libkeyutils_file_list(); push @binaries, @$libkeyutils_files_ref; my $httpd_bin = SSP::Util::find_httpd_bin(); push @binaries, $httpd_bin if ( -x defined $httpd_bin ); foreach my $binary_file (@binaries) { chomp($binary_file); my ($binary_hash) = ( split( /\s+/, SSP::Util::timed_run( 0, 'sha256sum', "$binary_file" ) ) )[0]; return unless ($binary_hash); my $vulnerable = known_sha256_hashes($binary_hash); if ($vulnerable) { SSP::Security::print_generic_hack_predef('POSSIBLE ROOT-LEVEL-COMPROMISE'); my $crit_output = CYAN "\t$binary_file " . MAGENTA "has a sha256 hash [ " . CYAN $binary_hash . MAGENTA " ] known to be vulnerable"; SSP::Util::print_critical( $crit_output, \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical(); } } } sub known_sha256_hashes { my $checksum = shift; my $URL = "https://raw.githubusercontent.com/CpanelInc/tech-CSI/master/known_256hashes.txt"; my @knownhashes = SSP::Util::timed_run( 0, 'curl', '-s', '-4', $URL ); return if !@knownhashes; if ( grep { /$checksum/ } @knownhashes ) { return 1; } return 0; } sub look_for_suspicious_files { my $URL = "https://raw.githubusercontent.com/CpanelInc/tech-CSI/master/suspicious_files.txt"; my $files = SSP::Util::timed_run( 0, 'curl', '-s', '-4', $URL ); my @files = split /\n/, $files; my $fileType; for my $file (@files) { chomp($file); my $fStat = lstat($file); if ( -f _ or -d _ and not -z _ and not -l _ ) { if ( -f _ ) { $fileType = "file"; } if ( -d _ ) { $fileType = "directory"; } my ($FileU) = getpwuid( ( $fStat->uid ) ); my ($FileG) = getgrgid( ( $fStat->gid ) ); $FileU = "UNKNOWN" if ( $FileU eq "" ); $FileG = "UNKNOWN" if ( $FileG eq "" ); my $FileSize = $fStat->size; my $ctime = $fStat->ctime; my $isNOTRPMowned; if ( SSP::Util::i_am('ubuntu') ) { $isNOTRPMowned = SSP::Util::timed_run( 0, "apt-cache dump | grep '$file'" ); } else { $isNOTRPMowned = SSP::Util::timed_run( 0, 'rpm', '-qf', "$file" ); } chomp($isNOTRPMowned); my $RPMowned = "Yes"; if ( $isNOTRPMowned eq "" or $isNOTRPMowned =~ m/not owned by/ ) { $RPMowned = "No"; } my $isImmutable = isImmutable($file); if ($isImmutable) { $isImmutable = MAGENTA " [IMMUTABLE]"; } else { $isImmutable = ""; } if ( $FileU eq "root" or $FileG eq "root" ) { my $change_time = scalar localtime($ctime); SSP::Security::print_generic_hack_predef('POSSIBLE ROOT-LEVEL-COMPROMISE'); my $crit_output = "Suspicious $fileType found: " . CYAN $file . $isImmutable . YELLOW "\n\t\\_ Size: " . CYAN $FileSize . YELLOW " Date Changed: " . CYAN $change_time . YELLOW "\n\t\\_ Package Owned: " . CYAN $RPMowned . YELLOW " Owned by U/G: " . CYAN $FileU . "/" . $FileG; SSP::Util::print_critical( $crit_output, \$CRIT_BUFFER_HACKED ); my $docdir = '/usr/share/doc'; if ( -s "$docdir/.l3_note" && SSP::Util::i_am('cptech') ) { my $cnt = 0; my $crit_output; if ( !exists( $ENV{'STY'} ) ) { while ( $cnt <= 5 ) { SSP::Util::blink_text("\t\\_ *** PLEASE REVIEW THE NOTE AT $docdir/.l3_note BEFORE ESCALATING! ***"); $cnt++; } } $crit_output = BOLD RED ON_BLACK "\t\\_ *** PLEASE REVIEW THE NOTE AT $docdir/.l3_note BEFORE ESCALATING! ***"; SSP::Util::print_critical( $crit_output, \$CRIT_BUFFER_HACKED ); } } else { SSP::Util::print_crit('POSSIBLE USER-LEVEL-COMPROMISE '); SSP::Util::print_critical( "Suspicious $fileType found: " . CYAN $file . $isImmutable . YELLOW "\n\t\\_ Size: " . CYAN $FileSize . YELLOW " Date Changed: " . CYAN scalar localtime($ctime) . YELLOW "\n\t\\_ Package Owned: " . CYAN $RPMowned . YELLOW " Owned by U/G: " . CYAN $FileU . "/" . $FileG ); SSP::Util::print_critical( CYAN "\t\\_ Consider running CSI https://go.cpanel.net/CSI" ); } } } } sub isImmutable { my $FileToCheck = shift; return if !-e $FileToCheck; my $attr = SSP::Util::timed_run( 0, '/usr/bin/lsattr', "$FileToCheck 2> /dev/null" ); if ( $attr =~ m/^\s*\S*[ai]/ ) { return 1; } return 0; } sub check_processes { my $URL = "https://raw.githubusercontent.com/CpanelInc/tech-CSI/master/suspicious_procs.txt"; my $susp_procs = SSP::Util::timed_run( 0, 'curl', '-s', '-4', $URL ); my @susp_procs = split /\n/, $susp_procs; my $headerPrint = 0; foreach my $suspicious_process (@susp_procs) { chomp($suspicious_process); my %procs = SSP::Util::grep_process_cmd( $suspicious_process, 'root' ); next unless ( SSP::Util::exists_process_cmd( $suspicious_process, 'root' ) ); next unless ( _ignore_susp_proc($suspicious_process) ); SSP::Security::print_generic_hack_predef('SUSPICIOUS PROCESS') unless ( $headerPrint == 1 ); $headerPrint = 1; my $crit_output = CYAN "\t\\_ " . join( "\t\\_ ", map { my ( $u, $c, $a ) = @{ $procs{$_} }{ 'USER', 'COMM', 'ARGS' }; "[pid: $_] [user: $u] [cmd: $c] [args: $a]" } keys %procs ); SSP::Util::print_critical( $crit_output, \$CRIT_BUFFER_HACKED ); my $docdir = '/usr/share/doc'; if ( -s "$docdir/.l3_note" && SSP::Util::i_am('cptech') ) { my $crit_output = BOLD RED "\t\\_Note From L3: PLEASE REVIEW THE NOTE FROM L3 AT $docdir/.l3_note BEFORE ESCALATING!"; SSP::Util::print_critical( $crit_output, \$CRIT_BUFFER_HACKED ); } } } sub _ignore_susp_proc { my $tcProc = shift; return 1 if ( $tcProc =~ m{log4j} && -e '/usr/bin/log4j-cve-2021-44228-hotpatch' ); return 0; } sub check_for_fritzfrog_botnet { my $maliciousPortListening = 0; my $maliciousProcessFound = 0; my $ports = SSP::Util::get_lsof_port_href(); if ( defined( $ports->{'1234'} ) ) { $maliciousPortListening = 1; } my $info; my @MaliciousProcs = qw( nginx ifconfig libexec php-fpm ); foreach my $proc (@MaliciousProcs) { chomp( my $PidOf = SSP::Util::timed_run( 0, "pidof $proc" ) ); next unless ($PidOf); my $isDeleted = SSP::Util::timed_run( 0, "ls -l /proc/$PidOf/exe" ); next unless ( $isDeleted =~ m/deleted/ ); $info .= ' [ ' . $proc . '/' . $PidOf . ' ]'; $maliciousProcessFound++; } if ( $maliciousPortListening and $maliciousProcessFound > 0 ) { SSP::Security::print_generic_hack_predef('FritzFrog Botnet'); my $crit_output = "Suspicious process(es) running " . CYAN $info . MAGENTA " and listening on port 1234."; SSP::Util::print_critical( $crit_output, \$CRIT_BUFFER_HACKED ); } } sub check_for_dragnet { if ( open my $fh, '<', '/proc/self/maps' ) { while (<$fh>) { if (m{ (\s|\/) libc\.so\.0 (\s|$) }x) { SSP::Security::print_generic_hack_predef('DRAGNET ROOTKIT'); SSP::Util::print_critical( "\t\\_ 'libc.so.0' found in process maps", \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical(); last; } } close($fh); } } sub print_footer_hacked { return unless $CRIT_BUFFER_HACKED; SSP::Util::print_magenta(qq{\n\nThis server might be compromised. Please use the predef "Escalate::Escalate to TS Level 3 (Hacked Server)" and copy/paste the below template:}); print BOLD BLUE "\n===== *** BEGIN SSP COPY FOR HACKED COMPROMISE TICKETS *** ======\n\n"; SSP::Info::print_hostname(); SSP::Info::print_os(); SSP::Info::print_kernel_and_cpu(); SSP::Info::print_kernelcare_info(); SSP::Info::print_cpanel_info(); SSP::Info::check_for_license_info(); printf( "%s", $CRIT_BUFFER_HACKED ); print BOLD BLUE "\n===== *** END SSP COPY FOR HACKED COMPROMISE TICKETS *** ======\n"; } sub print_generic_hack_predef { my $name = shift; SSP::Util::print_critical( '', \$CRIT_BUFFER_HACKED ); SSP::Util::print_crit( '!! [ ' . $name . ' ] !! ', \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( 'All analysts, escalate this ticket to L3 using "Escalate::Escalate to TS Level 3 (Hacked Server)"', \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( "\t-- If this appears to be the direct cause of services being down, please escalate the ticket to Emergency status.", \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( "\tL1/L2: LOG OUT NOW. Do not execute any other commands unless given explicit directions by an L3 analyst or Supervisor.", \$CRIT_BUFFER_HACKED ); } sub print_ebury_cdorked_predef { my $name = shift; SSP::Security::print_generic_hack_predef($name); SSP::Util::print_critical( "\tL3: use \"L3 - eBury / CDorked [L3 only]\"", \$CRIT_BUFFER_HACKED ); } sub check_for_cdorked_A { return unless my $httpd_bin = SSP::Util::find_httpd_bin(); return unless -f $httpd_bin; my $max_bin_size = 10_485_760; # avoid slurping too much mem my $fStat = lstat($httpd_bin); return if ( $fStat->size > $max_bin_size ); my @apache_bins = (); push @apache_bins, $httpd_bin; my %procs = SSP::Util::grep_process_cmd( qr{ $httpd_bin }xms, 'root' ); for my $pid ( keys %procs ) { my $proc_pid_exe = "/proc/" . $pid . "/exe"; my $fStat = lstat($proc_pid_exe); if ( -l $proc_pid_exe && readlink($proc_pid_exe) =~ m{ \(deleted\) }xms ) { next if ( $fStat->size > $max_bin_size ); push @apache_bins, $proc_pid_exe; } } for my $check_bin (@apache_bins) { my $httpd; if ( open my $fh, '<', $check_bin ) { local $/; $httpd = <$fh>; close $fh; } next if !$httpd; if ( $httpd =~ /(open_tty|hangout|ptsname|Qkkbal)/ ) { my $signature = $check_bin . ": \"" . $1 . "\""; print_ebury_cdorked_predef('CDORKED'); SSP::Util::print_critical( "\tString found in $signature", \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical(); last; } } } sub check_for_cdorked_B { my @files = ( '/usr/sbin/arpd ', '/usr/sbin/tunelp ', '/usr/bin/s2p ' ); my $cdorked_files; for my $file (@files) { if ( -e $file ) { $cdorked_files .= "[$file] "; } } if ($cdorked_files) { print_ebury_cdorked_predef('CDORKED'); SSP::Util::print_critical( "\tThe following files were found (note the spaces at the end of the files):", \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( "\t$cdorked_files", \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical(); } } sub check_for_libkeyutils_symbols { local $ENV{'LD_DEBUG'} = 'symbols'; my $output = SSP::Util::timed_run_trap_stderr( 0, '/bin/true' ); return unless $output; if ( $output =~ m{ /lib(keyutils|ns[25]|pw[35]|s[bl]r)\. }xms ) { print_ebury_cdorked_predef('EBURY'); SSP::Util::print_critical( 'Ebury libs were found in symbol table.', \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( 'To confirm: LD_DEBUG=symbols /bin/true 2>&1 | egrep \'/lib(keyutils|ns[25]|pw[35]|s[bl]r)\\.\'', \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical(); } } sub check_for_ebury_ssh_shmem { my $ipcs_ref = SSP::Util::get_ipcs_href(); # As far as we know, sshd sholudn't be using shared memory at all, so any usage is a strong sign of ebury. return if !defined( $ipcs_ref->{root}{mp} ); for my $href ( @{ $ipcs_ref->{root}{mp} } ) { my $shmid = $href->{shmid}; my $cpid = $href->{cpid}; my $procs = SSP::Util::get_process_pid_href(); if ( defined $procs->{$cpid} && $procs->{$cpid}->{ARGS} =~ m{ \A /usr/sbin/sshd \b }xms ) { print_ebury_cdorked_predef('EBURY'); SSP::Util::print_critical( "\tShared memory segment created by sshd process exists:", \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( "\t\tsshd PID: " . $cpid, \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( "\t\tshmid: " . $shmid, \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( timed_run( 0, "echo --- ps -p ${cpid} uww ---;ps -p ${cpid} uww; echo --- ipcs -m -i ${shmid} ---; ipcs -m -i ${shmid}; echo ---", \$CRIT_BUFFER_HACKED ) ); last; } } } sub check_passwd_hiddenwasp_dirtycow { my @uid_0_users; my ( $firefart, $hiddenwasp ); open my $fh, '<', '/etc/passwd' or die $!; while (<$fh>) { if (/\A([^:]+):[^:]+:0:0:([^:]+):/) { push @uid_0_users, $1 if $1 ne 'root'; ++$firefart if ( $1 eq 'firefart' || $2 eq 'pwned' ); ++$hiddenwasp if $1 eq 'sftp'; last if scalar @uid_0_users >= 5; } } close $fh; if ($firefart) { SSP::Security::print_generic_hack_predef('FireFart / Dirty COW'); SSP::Util::print_critical( 'The root user GECOS field in the passwd database is "pwned", which is a typical indication of the FireFart Dirty COW exploit tool.', \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical("\tL3: use \"L3 - FireFart / Dirty COW Exploit [L3 Only]\""); SSP::Util::print_critical(); } if ($hiddenwasp) { SSP::Security::print_generic_hack_predef('HiddenWasp'); SSP::Util::print_critical( 'Root SFTP user detected - possible HiddenWasp', \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( "\tL3: see TECH-755", \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical(); } } sub check_for_malicious_root_cron { my %warning = (); return unless my $crons_aref = SSP::Util::get_cron_files(); for my $cron (@$crons_aref) { if ( open my $cron_fh, '<', $cron ) { while (<$cron_fh>) { if (m{ \A [^#]* (tor2web|\.onion\.) }x) { $warning{$cron}{$1} = 1; } if (m{ \A [^#]* (pastebin) }x) { $warning{$cron}{$1} = 1 unless ( $cron =~ m{/var/spool/cron/crontabs/root|/var/spool/cron/root} && -x '/opt/zabbix_scripts/malicious.pastebinCronJobUsers.sh' ); } } close $cron_fh; } } if (%warning) { SSP::Util::print_crit( "Potentially malicious root cronjob: ", \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( 'Escalate to L3 before proceeding', \$CRIT_BUFFER_HACKED ); for my $cron ( keys(%warning) ) { SSP::Util::print_critical( "\t \\_ " . $cron . " contains [ " . join( ' ', sort( keys( %{ $warning{$cron} } ) ) ) . " ]", \$CRIT_BUFFER_HACKED ); } } } sub check_for_spoofed_kernel_modules { my @dirs = qw( /usr/local/sbin /usr/local/bin /sbin /bin /usr/sbin /usr/bin ); my @files; foreach my $dir (@dirs) { ( -d $dir && !-l $dir ) ? push @files, glob("$dir/[*") : next; } my (@spoofs) = grep { !/(\/usr\/)?\/bin\/\[$/m } @files; if (@spoofs) { SSP::Util::print_crit( 'Suspicious file found: ', \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( 'Escalate to L3 confirm their validity before proceeding(see TECH-762)', \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( join "\n", map { "\t \\_ $_" } @spoofs, \$CRIT_BUFFER_HACKED ); } } sub check_binaries_for_shellcode { my @binaries = qw( /bin/ping /usr/bin/crontab /usr/bin/newgrp /usr/bin/pkexec /bin/su /usr/bin/quota ); foreach my $binary (@binaries) { my $isELF = SSP::Util::timed_run( 0, 'file', "$binary" ); next unless ( $isELF =~ m/ ELF / ); my $contains_bash = SSP::Util::timed_run( 0, 'hexdump', '-C', "$binary" ); if ( $contains_bash =~ m/bin.*bash/ ) { SSP::Util::print_crit( $binary . " header contains shellcode\n", \$CRIT_BUFFER_HACKED ); my $crit_output = "\t\\_ Escalate to L3 to have them confirm [ " . YELLOW "hexdump -C " . $binary . " | grep 'bin.*bash'" . MAGENTA " ]"; SSP::Util::print_critical( $crit_output, \$CRIT_BUFFER_HACKED ); } } } sub check_for_bad_perms_auth_keys { my $checkmode = '0600'; my $path = '/root/.ssh/authorized_keys'; return unless ( -e $path ); my $fStat = lstat($path); return unless my ( $uid, $gid ) = ( $fStat->uid, $fStat->gid ); my $user = getpwuid($uid); $user = $user || $uid; my $group = getgrgid($gid); $group = $group || $gid; if ( !( $user eq 'root' ) || !( $group eq 'root' ) ) { SSP::Security::print_generic_hack_predef('Potential Root Compromise'); SSP::Util::print_critical( 'Non-default Perms: ' . $path . ' [owner ' . $user . ':' . $group . '] (default root:root)', \$CRIT_BUFFER_HACKED ); } } sub check_for_shenanigans { return unless SSP::Util::i_am('cptech'); use List::Util qw[shuffle]; my $URL = eval unpack u => q{_(FAT='!S.B\O'0B.P}; ## no critic (ProhibitStringyEval) my $files = SSP::Util::timed_run( 6, 'curl', '-s', "$URL" ); my @files = split /\n/, $files; my @shenanigans; foreach my $file (@files) { chomp($file); my $fStat = lstat($file); if ( -f $file or -d $file and not -z $file and not -l $file ) { my $filetype = SSP::Util::timed_run( 5, 'file', "$file" ); next if ( $filetype =~ m/data/ && $file eq "/home/installer" ); next if ( $file eq '/root/installer.1' && -d '/var/cpanel/addons/SMTP2GO' ); next if ( $file eq '/root/installr.sh' && -d '/usr/local/cpanel/whostmgr/docroot/whmsonic/' && grep { "WHMSonic Setup" } '/root/installr.sh' ); push @shenanigans, "[" . $file . "] "; } } my @shenaniganlist; if ( scalar(@shenanigans) > 1 ) { @shenaniganlist = ( shuffle @shenanigans )[ 0 .. ( 2 - 1 ) ]; } else { @shenaniganlist = @shenanigans; } # Suspicious files/directories if ( scalar(@shenaniganlist) > 0 ) { SSP::Security::print_generic_hack_predef('LICENSE ISSUE: (suspicious file/directory)'); SSP::Util::print_critical( 'The following files were found:', \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( "\t" . join( "\n\t", @shenaniganlist ), \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical(); } # Suspicious process if ( my %procs = SSP::Util::grep_process_cmd( '^(cspdaemon|checkstatus)$', 'root' ) ) { SSP::Security::print_generic_hack_predef('LICENSE ISSUE (suspicious process)'); SSP::Util::print_critical( 'The following suspicious running processes were found (please verify):', \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( "\t\\_ " . join( "\n\t\\_ ", map { my ( $u, $c, $a ) = @{ $procs{$_} }{ 'USER', 'COMM', 'ARGS' }; "[pid: $_] [user: $u] [cmd: $c] [args: $a]" } keys %procs ), \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical(); } # YONCU my $bad_cpkeyclt_found = 0; if ( -s '/usr/local/cpanel/cpkeyclt' && -T _ ) { SSP::Security::print_generic_hack_predef('LICENSE ISSUE (YONCU)'); SSP::Util::print_critical( "\t/usr/local/cpanel/cpkeyclt was found to be a text file", \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical(); } if ( -d '/etc/csf/' ) { opendir( my $dir_fh, "/etc/csf" ); my @files = grep { !/^\.\.?$/ } readdir $dir_fh; closedir $dir_fh; foreach my $file (@files) { next unless ( -f "/etc/csf/$file" ); open( my $fh, '<', "/etc/csf/$file" ); while (<$fh>) { chomp($_); if ( $_ =~ m{CSP Licensing Servers|/usr/bin/cspfwd|/usr/bin/CSPUpdate} ) { SSP::Security::print_generic_hack_predef('LICENSE ISSUE: (suspicious line found in /etc/csf directory)'); SSP::Util::print_critical( 'The following file within /etc/csf/ contains license circumvention data:', \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( "\t" . join( "\n\t", $file ), \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical(); last; } } close($fh); } } # CX-771 return unless SSP::Util::i_am('cptech'); my $thegameisafoot = 0; if ( -e "/root/.bash_history" ) { open( my $fh, '<', "/root/.bash_history" ); my @HISTORY; my $linecnt = 0; while (<$fh>) { chomp; push @HISTORY, $_ unless ( $linecnt > 20 ); $linecnt++; } close($fh); $thegameisafoot++ if ( grep { /Still logged into remote/ } @HISTORY ); } # check if CSPUpdate is found in any /var/log/cron* or /var/log/syslog* files. my @cronlog = glob(q{ /var/log/syslog* /var/log/cron* }); my $csp_count = SSP::Util::timed_run( 0, 'grep', 'CSPUpdate', @cronlog ); $thegameisafoot = 3 if ($csp_count); # check if queueprocd.log is empty $thegameisafoot++ if ( !-s '/usr/local/cpanel/logs/queueprocd.log' ); # check size of /usr/local/cpanel/error_log (if less than 2KB) my $logfile = '/usr/local/cpanel/logs/error_log'; my $fStat = lstat($logfile); my $FileSize = $fStat->size; $thegameisafoot++ if ( $FileSize < 2048 ); # check if current /var/log/cron or /var/log/syslog is empty $logfile = '/var/log/cron'; if ( SSP::Util::i_am('ubuntu') ) { $logfile = '/var/log/syslog'; } $thegameisafoot++ if ( !-s $logfile ); if ( $thegameisafoot > 2 ) { SSP::Security::print_generic_hack_predef('LICENSE ISSUE (CX-771)'); SSP::Util::print_critical( "Suspicious license activity found (L3's please verify)", \$CRIT_BUFFER_HACKED ); } } sub check_for_xbash { return unless my $mysql_datadir = SSP::Util::get_mysql_datadir(); return unless -d $mysql_datadir; opendir( my $dh, $mysql_datadir ); my $showHeader = 0; while ( my $database = readdir($dh) ) { next unless ( $database =~ m/PLEASE_READ|README_TO_RECOVER/i ); SSP::Security::print_generic_hack_predef('Possible Xbash Variant Ransomware: ') unless ($showHeader); SSP::Util::print_critical( "\tThe following files or directories were found:", \$CRIT_BUFFER_HACKED ) unless ($showHeader); $showHeader = 1; SSP::Util::print_critical( "\t\t" . $mysql_datadir . $database, \$CRIT_BUFFER_HACKED ); } closedir $dh; } sub check_for_missing_ps_cmd { return if ( -s '/bin/ps' or -s '/usr/bin/ps' ); SSP::Security::print_generic_hack_predef('Potential Root Compromise'); SSP::Util::print_critical( "\t\\_ The ps command is missing!", \$CRIT_BUFFER_HACKED ); } sub check_changepasswd_modules { my $dir = '/usr/local/cpanel/Cpanel/ChangePasswd/'; return unless ( -d $dir ); return unless opendir( my $dh, $dir ); my @dir_contents = grep { /\.pm\Z/ } readdir $dh; close $dh; return unless @dir_contents; my @suspicious; foreach my $module (@dir_contents) { next if ( $module eq 'DigestAuth.pm' ); push @suspicious, $module if ( -s $dir . $module ); } if (@suspicious) { SSP::Security::print_generic_hack_predef('Cpanel::ChangePasswd'); SSP::Util::print_critical( "\tFound custom ChangePasswd module(s) [ " . join( ' ', @suspicious ) . ' ]', \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( "\tThese modules can intercept all system password changes, see TECH-893", \$CRIT_BUFFER_HACKED ); } } sub check_cpanellogin_in_sudoers { # If this routine gets called, it has already been determined that the cpanellogin user is found in the sudoers file(s) with ALL and/or NOPASSWORD!!! SSP::Security::print_generic_hack_predef('Root Authentication Vector Detected:'); SSP::Util::print_critical( "\t\\_ cpanellogin user found in sudoers file which is known to be associated with root compromises!", \$CRIT_BUFFER_HACKED ); } ## END malware checks sub csi_checks_only { SSP::Warn::check_port_hash(); SSP::Security::check_for_bash_secadv_20140924(); # advisory all_malware_checks(); SSP::Crit::check_mysql_skip_grants(); SSP::Util::print_info2('SSP Security checks done.'); } sub check_for_susp_rc_modules { return unless ( -s '/etc/rc.modules' ); my @ignore = qw( acpiphp ip_conntrack_ftp ); my $line; open( my $fh, '<', '/etc/rc.modules' ); my $showHeader = 0; while (<$fh>) { $line = $_; chomp($line); if ( grep { $line =~ $_ } @ignore ) { next; } else { SSP::Security::print_generic_hack_predef('Suspicious line found within the /etc/rc.modules file.') unless ($showHeader); $showHeader = 1; SSP::Util::print_critical( "\t\\_ $line", \$CRIT_BUFFER_HACKED ); my $docdir = '/usr/share/doc'; if ( -s "$docdir/.l3_note" && SSP::Util::i_am('cptech') ) { my $cnt = 0; my $crit_output; if ( !exists( $ENV{'STY'} ) ) { while ( $cnt <= 5 ) { SSP::Util::blink_text("\t\\_ *** PLEASE REVIEW THE NOTE AT $docdir/.l3_note BEFORE ESCALATING! ***"); $cnt++; } } $crit_output = BOLD RED ON_BLACK "\t\\_ *** PLEASE REVIEW THE NOTE AT $docdir/.l3_note BEFORE ESCALATING! ***"; SSP::Util::print_critical( $crit_output, \$CRIT_BUFFER_HACKED ); } } } close($fh); } sub check_for_mounted_cpanel_lisc { my @mounts = split /\n/, SSP::Util::timed_run( 0, 'mount' ); for my $mount (@mounts) { if ( SSP::Util::i_am('cptech') and $mount =~ m{ \s \Q$CPANEL_LICENSE_FILE\E \s }xms ) { SSP::Security::print_generic_hack_predef('LICENSE'); SSP::Util::print_critical( 'The following mount entry was found:', \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( "\t" . $mount, \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical( "\tEscalate to L3!.", \$CRIT_BUFFER_HACKED ); SSP::Util::print_critical(); } } } 1; SSP_SECURITY $fatpacked{"SSP/Util.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'SSP_UTIL'; #!/usr/bin/perl # SSP - System Status Probe (Util Module) # Find and print useful troubleshooting info on cPanel servers =head1 COPYRIGHT This software is Copyright 2024 WebPros International, LLC. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THE SOFTWARE LICENSED HEREUNDER IS PROVIDED "AS IS" AND WEBPROS INTERNATIONAL, LLC D/B/A/ CPANEL (CPANEL) HEREBY DISCLAIMS ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, RELATING TO THE SOFTWARE, ITS THIRD PARTY COMPONENTS, AND ANY DATA ACCESSED THEREFROM, OR THE ACCURACY, TIMELINESS, COMPLETENESS, OR ADEQUACY OF THE SOFTWARE, ITS THIRD PARTY COMPONENTS, AND ANY DATA ACCESSED THEREFROM, INCLUDING THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. CPANEL DOES NOT WARRANT THAT THE SOFTWARE OR ITS THIRD PARTY COMPONENTS ARE ERROR-FREE OR WILL OPERATE WITHOUT INTERRUPTION. IF THE SOFTWARE, ITS THIRD PARTY COMPONENTS, OR ANY DATA ACCESSED THEREFROM IS DEFECTIVE, YOU ASSUME THE SOLE RESPONSIBILITY FOR THE ENTIRE COST OF ALL REPAIR OR INJURY OF ANY KIND, EVEN IF CPANEL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DEFECTS OR DAMAGES. NO ORAL OR WRITTEN INFORMATION OR ADVICE GIVEN BY CPANEL, ITS AFFILIATES, LICENSEES, DEALERS, SUB-LICENSORS, AGENTS OR EMPLOYEES SHALL CREATE A WARRANTY OR IN ANY WAY INCREASE THE SCOPE OF ANY WARRANTY. =cut package SSP::Util; use 5.006; use strict; use warnings; use File::Find; use Socket; use IO::Socket::INET; use Sys::Hostname; use Term::ANSIColor qw(:constants); use Time::Local qw{timelocal timegm}; use IPC::Open3; use Cwd qw(abs_path); # Application version (The project maintainer will bump this, don't modify it.) our $VERSION = '5.00.054'; # Global variables that alter application runtime our $OPT_SKIP_NETWORKING; # Disable network calls our $OPT_TIMEOUT; # How long to wait for system commands to finish executing # Global variables updated throughout application our $CRIT_BUFFER; # Critical output to be printed at the end # Things that are the same but used many places our $CPANEL_LICENSE_FILE = '/usr/local/cpanel/cpanel.lisc'; our $CPANEL_VERSION_FILE = '/usr/local/cpanel/version'; our $CPANEL_CONFIG_FILE = '/var/cpanel/cpanel.config'; our $MYSQL_CONF_FILE = '/etc/my.cnf'; our $PURE_FTPD_CONF_FILE = '/etc/pure-ftpd.conf'; # Global variables initialized at application initialization our %CPCONF; # cpanel.config our $ORIGINAL_PATH; our %SOCKET; # Dispatcher for optional Socket module usage our $RUN_STATE; our $HTTP_GET_HOST_CACHE; our %MEMOIZE_CACHE; our $YUM_OR_DNF; our $YUM_CONF; our $gl_is_kernel = 0; init(); sub init { $ORIGINAL_PATH = $ENV{'PATH'}; ## no critic (LocalizedPunctuationVars) $ENV{'PATH'} = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin'; $| = 1; ## use critic $Term::ANSIColor::AUTORESET = 1; _memoize(); _populate_run_state(); if ( i_am_one_of( 'cpanel', 'dnsonly' ) ) { %CPCONF = get_cpanel_conf(); } ## no critic (StringyEval) # This avoids compile-time errors on old Perl where Socket::get(addr|name)info and related constants don't exist. eval q( # Perl 5.14+ # The import is redundant but guarantees the desirable failure of this eval at run-time if anything is missing. Socket->import(qw(getaddrinfo getnameinfo NI_NAMEREQD NI_NUMERICHOST NIx_NOSERV SOCK_RAW)); %SOCKET = ( 'getaddrinfo' => \&Socket::getaddrinfo, 'getnameinfo' => \&Socket::getnameinfo, 'NI_NAMEREQD' => Socket::NI_NAMEREQD, 'NI_NUMERICHOST' => Socket::NI_NUMERICHOST, 'NIx_NOSERV' => Socket::NIx_NOSERV, 'SOCK_RAW' => Socket::SOCK_RAW, ); return 1; ); ## use critic $SIG{'INT'} = sub { ## no critic (LocalizedPunctuationVars) print "\n\nJust being impatient, or did SSP actually hang? What if it didn't get a chance to check something really important?\n"; print "\nIf you really want out of here and it didn't work the first time then you interrupted a child and not the parent process. Keep hitting CTRL+C.\n"; SSP::Security::print_footer(); die; }; return 1; } sub _populate_run_state { ## no critic (RequireArgUnpacking) return unless _init_run_state(); _set_run_type('kernelcare') if -x '/usr/bin/kcarectl'; _set_run_type('wptk') if -x '/usr/local/bin/wp-toolkit'; _set_run_type('jetbackup') if ( -x '/usr/bin/jetbackup' or -x '/usr/bin/jetbackup5' ); if ( -d '/usr/local/cpanel' ) { my $get_current_profile_href = SSP::Util::get_node_info_href(); my $profile_id = exists $get_current_profile_href->{'data'}->{'code'} ? lc( $get_current_profile_href->{'data'}->{'code'} ) : 'UNKNOWN'; _set_run_type($profile_id) unless ( $profile_id eq 'UNKNOWN' || $profile_id eq 'standard' ); if ( -e '/var/cpanel/dnsonly' or license_file_is_dnsonly() ) { _set_run_type('dnsonly'); } else { _set_run_type('cpanel'); _set_run_type('solo') if license_file_is_solo(); if ( -f '/etc/cpanel/ea4/is_ea4' ) { _set_run_type('ea4'); } elsif ( -d '/usr/local/apache' ) { _set_run_type('ea3'); } } my ( $cp_numeric_version, $cp_original_version ) = get_cpanel_version(); _set_run_var( 'cpanel_numeric_version', $cp_numeric_version ); _set_run_var( 'cpanel_original_version', $cp_original_version ); } if ( exists $ENV{'PACHA_AUTOFIXER'} ) { _set_run_type('cptech'); } elsif ( defined $ENV{'HISTFILE'} and index( $ENV{'HISTFILE'}, 'cpanel_ticket' ) != -1 ) { _set_run_type('cptech'); } else { foreach ( @ENV{ 'SSH_CLIENT', 'SSH_CONNECTION' } ) { next unless defined $_; next unless m{\A (184\.94\.197\.[2-6]|208\.74\.123\.98)}xms; _set_run_type('cptech'); last; } } ## Add additional supported OS names to this @os_array. ## lcname is the ID in /etc/os-release, ucname is the proper OS name for readability. my @os_array = ( { lcname => 'centos', ucname => 'CentOS', }, { lcname => 'almalinux', ucname => 'AlmaLinux', }, { lcname => 'rocky', ucname => 'Rocky Linux', }, { lcname => 'cloudlinux', ucname => 'CloudLinux', }, { lcname => 'ubuntu', ucname => 'Ubuntu' }, { lcname => 'amazon', ucname => 'Amazon' }, { lcname => 'rhel', ucname => 'RedHat' }, ); my %os_hash = map { $_->{lcname} => { ucname => $_->{ucname} } } @os_array; my ( $os, $dist, $maj, $min, $bld ) = get_os_info(); $bld = ($bld) ? $bld : ""; my $os_version = $maj . "." . $min . "." . $bld; my $ProperOS = ( $os_hash{$dist}->{ucname} ) ? $os_hash{$dist}->{ucname} : RED "Unsupported OS " . CYAN ucfirst($dist); #print "DEBUG: os=$os / dist=$dist / os_version=$os_version\n"; _set_run_type($dist); _set_run_var( 'os_ises', 2 ) unless ( $dist =~ m/(?:Amazon)/i ); _set_run_var( 'os_ises', 1 ) if ( $dist =~ m/(?:Amazon)/i ); my $release = $ProperOS . " " . BOLD CYAN ucfirst($os) . " release " . $os_version; $release =~ s/^\s+|\s+$//; if ( length $release >= 4 ) { _set_run_var( 'os_release', $release ); } if ( $release =~ /(\d+\.\d+)/ ) { _set_run_var( 'os_version', $1 ); } elsif ( $release =~ /(\d+)/ ) { _set_run_var( 'os_version', $1 ); } } sub print_help { print BOLD YELLOW ON_BLACK "System Status Probe $VERSION\n" . RESET; ( my $message = q{ This software is Copyright 2023 by cPanel, L.L.C. Find and print useful troubleshooting info on cPanel servers. Usage: ssp [option] --bugreport Provide bug report template along with environment details (intended for cPanel tech use only) --csi cPanel Security Inspection, run's several security checks --docreport Provide document bug report template (intended for cPanel tech use only) --no-network Skip all network related checks --timeout This overides timeouts of several longer running functions (min: 5s) --sshcheck Report servers sshd settings to assist cPanel Support analysts with connecting to your server } ) =~ s/^ {8}//mg; print $message; } sub get_phpini_aref { my $phpini = '/usr/local/lib/php.ini'; my @phpini; return () if !-f $phpini; if ( open my $fh, '<', $phpini ) { while (<$fh>) { next if (/^(?:;|$|\[)/); chomp; push @phpini, $_; } close $fh; } return \@phpini; } sub find_httpd_bin { if ( SSP::Util::i_am('almalinux') ) { return 'httpd' if -x '/usr/sbin/httpd'; } if ( SSP::Util::i_am('ea4') ) { return '/usr/sbin/httpd' if -x '/usr/sbin/httpd'; } return; } sub get_apache_version_href { return unless my $httpd_bin = find_httpd_bin(); return unless my @output = split /\n/, timed_run( 0, $httpd_bin, '-v' ); my %info; foreach (@output) { if (m{ \A Server \s+ version: \s+ Apache/([^\s]+) \s }xms) { $info{'version'} = $1; } if (m{ \A Server \s+ built: \s+ (.*) \z }xms) { $info{'built'} = $1; $info{'built'} =~ s/^\s+//g; } # Below is for EA3 only if (m{ \A Cpanel::Easy::Apache \s+ (.*) \z }xms) { $info{'ea_version'} = $1; } } if ( i_am('ea4') ) { if ( SSP::Util::i_am('ubuntu') ) { my $ea_apache_info = timed_run( 0, 'dpkg', '-s', 'ea-apache24' ); my @ea_apache_info = split /\n/, $ea_apache_info; foreach my $ea_info (@ea_apache_info) { chomp($ea_info); next unless ( $ea_info =~ m/^Version:/ ); $info{'ea_version'} = $ea_info; last; } $info{'ea_version'} =~ s/Version: /ea-apache24-/; } else { chomp( $info{'ea_version'} = timed_run( 0, 'rpm', '-qf', $httpd_bin ) ); $info{'ea_version'} =~ s/\.\w\d{1,3}\D+\d+\n//; } } return \%info; } sub get_apache_version { return unless my $href = get_apache_version_href(); return unless defined $href->{'version'}; return $href->{'version'}; } sub get_apache_modules_href { return unless my $httpd_bin = find_httpd_bin(); my %modules = map { ( split( /\s+/, $_, 3 ) )[1] => 1 } split /\n/, timed_run( 0, $httpd_bin, '-M' ); return \%modules; } sub get_cpanel_license_file_info_href { my %license; if ( open my $license_fh, '<', $CPANEL_LICENSE_FILE ) { my @license_text; while (<$license_fh>) { last if m{ \A -----BEGIN }xms; next unless m{ \A \p{IsPrint}+ \Z }xms; chomp; push @license_text, $_; } close $license_fh; %license = map { ( split( /:\s+/, $_, 2 ) )[ 0, 1 ] } @license_text; } return \%license; } sub license_file_is_cloudlinux { my $href = get_cpanel_license_file_info_href(); return if not exists $href->{products}; return 1 if grep { /cloudlinux/ } $href->{products}; return 0; } sub license_file_is_cpanel { my $href = get_cpanel_license_file_info_href(); return if not exists $href->{products}; return 1 if grep { /cpanel/ } $href->{products}; return 0; } sub license_file_is_dnsonly { my $href = get_cpanel_license_file_info_href(); return if not exists $href->{products}; return 1 if grep { /dnsonly/ } $href->{products}; return 0; } sub license_file_is_solo { # products =~ cpanel and maxusers = 1 indicates Solo. my $href = get_cpanel_license_file_info_href(); return if not exists $href->{products} or not exists $href->{maxusers}; return 1 if ( grep { /cpanel/ } $href->{products} and $href->{maxusers} == 1 ); return 0; } sub get_cpanel_conf { my %cpconf; if ( open( my $cpconf_fh, '<', $CPANEL_CONFIG_FILE ) ) { local $/ = undef; %cpconf = map { ( split( /=/, $_, 2 ) )[ 0, 1 ] } split( /\n/, readline($cpconf_fh) ); close $cpconf_fh; return %cpconf; } else { print_crit('cpanel.config: '); print_critical("$CPANEL_CONFIG_FILE could not be opened.\n"); } } sub get_cpanel_version { my $numeric_version; my $original_version; if ( open my $file_fh, '<', $CPANEL_VERSION_FILE ) { $original_version = readline($file_fh); close $file_fh; } return ( 'UNKNOWN', 'UNKNOWN' ) unless defined $original_version; chomp $original_version; # Parse either 1.2.3.4 or 1.2.3-THING_4 to 1.2.3.4 $numeric_version = join( '.', split( /\.|-[a-zA-Z]+_/, $original_version ) ); $numeric_version = 'UNKNOWN' unless $numeric_version =~ /^\d+\.\d+\.\d+\.\d+$/; return ( $numeric_version, $original_version ); } sub _version_cmp { my ( $first, $second ) = @_; my ( $a1, $b1, $c1, $d1, $e1, $f1 ) = split /[\._]/, $first; my ( $a2, $b2, $c2, $d2, $e2, $f2 ) = split /[\._]/, $second; for my $ref ( \$a1, \$b1, \$c1, \$d1, \$e1, \$f1, \$a2, \$b2, \$c2, \$d2, \$e2, \$f2, ) { # Fill empties with 0 $$ref = 0 unless defined $$ref; } return $a1 <=> $a2 || $b1 <=> $b2 || $c1 <=> $c2 || $d1 <=> $d2 || $e1 <=> $e2 || $f1 <=> $f2; } sub version_compare { # example: return if version_compare($ver_string, qw( >= 1.2.3.3 )); # Must be no more than four version numbers separated by periods and/or underscores. my ( $ver1, $mode, $ver2 ) = @_; return if ( !defined($ver1) || ( $ver1 =~ /[^\._0-9]/ ) ); return if ( !defined($ver2) || ( $ver2 =~ /[^\._0-9]/ ) ); # Shamelessly copied the comparison logic out of Cpanel::Version::Compare my %modes = ( '>' => sub { return if $_[0] eq $_[1]; return _version_cmp(@_) > 0; }, '<' => sub { return if $_[0] eq $_[1]; return _version_cmp(@_) < 0; }, '==' => sub { return $_[0] eq $_[1] || _version_cmp(@_) == 0; }, '!=' => sub { return $_[0] ne $_[1] && _version_cmp(@_) != 0; }, '>=' => sub { return 1 if $_[0] eq $_[1]; return _version_cmp(@_) >= 0; }, '<=' => sub { return 1 if $_[0] eq $_[1]; return _version_cmp(@_) <= 0; } ); return if ( !exists $modes{$mode} ); return $modes{$mode}->( $ver1, $ver2 ); } sub _timedsaferun { # Borrowed from WHM 66 Cpanel::SafeRun::Timed and modified # We need to be sure to never return undef, return an empty string instead. my ( $timer, $stderr_to_stdout, @PROGA ) = @_; return '' if ( substr( $PROGA[0], 0, 1 ) eq '/' && !-x $PROGA[0] ); $timer = $timer ? $timer : 25; # A timer value of 0 means use the default, currently 25. $timer = $OPT_TIMEOUT ? $OPT_TIMEOUT : $timer; my $output; my $complete = 0; my $pid; my $fh; # FB-63723: must declare $fh before eval block in order to avoid unwanted implicit waitpid on die eval { local $SIG{'__DIE__'} = 'DEFAULT'; local $SIG{'ALRM'} = sub { $output = ''; print RED ON_BLACK 'Timeout while executing: ' . join( ' ', @PROGA ) . "\n"; die; }; alarm($timer); if ( $pid = open( $fh, '-|' ) ) { ## no critic (BriefOpen) local $/; $output = readline($fh); close($fh); } elsif ( defined $pid ) { open( STDIN, '<', '/dev/null' ); ## no critic (BriefOpen) if ($stderr_to_stdout) { open( STDERR, '>&', 'STDOUT' ); ## no critic (BriefOpen) } else { open( STDERR, '>', '/dev/null' ); ## no critic (BriefOpen) } exec(@PROGA) or exit 1; ## no critic (NoExitsFromSubroutines) } else { print RED ON_BLACK 'Error while executing: [ ' . join( ' ', @PROGA ) . ' ]: ' . $! . "\n"; alarm 0; die; } $complete = 1; alarm 0; }; alarm 0; if ( !$complete && $pid && $pid > 0 ) { kill( 15, $pid ); #TERM sleep(2); # Give the process a chance to die 'nicely' kill( 9, $pid ); #KILL } return defined $output ? $output : ''; } sub timed_run { my ( $timer, @PROGA ) = @_; return _timedsaferun( $timer, 0, @PROGA ); } sub timed_run_trap_stderr { my ( $timer, @PROGA ) = @_; return _timedsaferun( $timer, 1, @PROGA ); } sub get_local_ipaddrs_aref { my @local_ipaddrs_list; my @output; unless ( @output = split /\n/, timed_run( 0, 'ip', 'addr' ) ) { @output = split /\n/, timed_run( 0, 'ifconfig', '-a' ); } for my $line (@output) { if ( $line =~ m{ (\d+\.\d+\.\d+\.\d+) }xms ) { push @local_ipaddrs_list, $1; } } return \@local_ipaddrs_list; } sub print_version { print BOLD YELLOW ON_BLACK "\tSSP $VERSION\n\n"; } sub get_tiers_file { #TODO: Get rid of this in favor of get_tiers_json_href if it works out. return _http_get( Host => 'httpupdate.cpanel.net', Path => '/cpanelsync/wp2-TIERS' ) if ( SSP::Util::i_am('wp2') ); return _http_get( Host => 'httpupdate.cpanel.net', Path => '/cpanelsync/TIERS' ); } sub get_process_pid_href { # Tested on CentOS 5 through 7. # 'ps' is horrible at providing reliably-parseable output. This is probably as close as we can get. # etimes field doesn't exist until CentOS 7 but can be derived from etime. my $field_separator = '#^#'; # Any sequence unlikely to occur in normal ps output. ps will also pad everything with spaces. my $ps_format_opt = join( $field_separator, qw( %p %P %U %t %n %c %a %C ) ); # like 'pid#^#ppid#^#user#^#etime#^#nice#^#comm#^#args' my %hash = map { my ( $pid, $ppid, $user, $etime, $nice, $comm, $args, $cpu ) = split /\s*\Q$field_separator\E\s*/, $_; $pid =~ s/^\s+//; $args =~ s/\s+$//; my ( $sec, $min, $hou, $day ) = reverse split( /[:-]/, $etime ); $day += 0; $hou += 0; $min += 0; $sec += $day * 86400 + $hou * 3600 + $min * 60; $pid => { 'PPID' => defined $ppid ? $ppid : '', 'USER' => defined $user ? $user : '', 'ETIME' => defined $etime ? $etime : '', 'NICE' => defined $nice ? $nice : '0', 'COMM' => defined $comm ? $comm : '', 'ARGS' => defined $args ? $args : '', 'CPU' => defined $cpu ? $cpu : '', 'ETIMES' => $sec, } } split /\n/, timed_run( 0, 'ps', '--no-headers', '--width=1000', '-eo', $ps_format_opt ); return \%hash; } sub grep_process_cmd { # Matches short (COMM) or long (ARGS) command columns my ( $pattern, $user ) = @_; my $procs = get_process_pid_href(); my %result; for my $pid ( keys %{$procs} ) { next if defined $user ? $procs->{$pid}->{'USER'} ne $user : 0; $result{$pid} = $procs->{$pid} if grep { /^$pattern/ } @{ $procs->{$pid} }{ 'COMM', 'ARGS' }; } return %result; } sub exists_process_cmd { my ( $pattern, $user ) = @_; my %procs = grep_process_cmd( $pattern, $user ); return scalar keys %procs ? 1 : 0; } sub get_lsof_port_href { my %hash; for ( split /\n/, timed_run( 0, 'lsof', '+c15', '-n', '-P', '-i' ) ) { # cmd will be max 15 characaters due to lsof limitation # Example from CentOS 6: # spamd 1781 root 5u IPv4 10887 0t0 TCP 127.0.0.1:783 (LISTEN) # nc 9468 root 3u IPv6 84415 0t0 TCP [::1]:25 (LISTEN) # Example from an older CentOS 5 system (note empty SIZE column): # exim 3066 mailnull 3u IPv6 2566011 TCP *:smtp (LISTEN) my @lsof = split( /\s+/, $_, 10 ); if ( defined( $lsof[9] ) && $lsof[9] =~ /LISTEN/ ) { splice( @lsof, 6, 1 ); # Drop the SIZE/OFF column which can sometimes be blank and throw everything off } if ( defined( $lsof[8] ) && $lsof[8] =~ /LISTEN/ ) { # SIZE/OFF column is blank, or has been dropped if ( $lsof[7] =~ /^(.*):(\d+)$/ ) { my ( $ip, $port ) = ( $1, $2 ); push @{ $hash{$port} }, { 'CMD' => $lsof[0], 'PID' => $lsof[1], 'USER' => $lsof[2], 'IPV' => $lsof[4], 'PROTO' => $lsof[6], 'IP' => $ip }; } } } return \%hash; } sub get_ipcs_href { my %hash; my $header = 0; # For now, all we need is shared memory segment owner and creator-pid, but the data structure is extensible. # ipcs -m -p # #------ Shared Memory Creator/Last-op -------- #shmid owner cpid lpid #2228224 root 992 992 #2588673 root 1309 1315 #2195458 root 985 985 #2621443 root 1309 1315 for ( split /\n/, timed_run( 0, 'ipcs', '-m', '-p' ) ) { if ( $header == 0 ) { $header = 1 if m/^ shmid \s+ owner \s+ cpid \s+ lpid \s* $/ix; next; } my @ipcs = split( /\s+/, $_, 5 ); push @{ $hash{ $ipcs[1] }{mp} }, { # Key by owner, type 'mp' (-m -p output) 'shmid' => $ipcs[0], 'cpid' => $ipcs[2], 'lpid' => $ipcs[3] }; } return \%hash; } sub get_mysql_conf_href { return unless open( my $mycnf_fh, '<', $MYSQL_CONF_FILE ); my %conf; my $section = 'unknown'; while (<$mycnf_fh>) { chomp; next if /^(#|$)/; if (m{ \A \s* \[([^\]]+)] }x) { $section = lc($1); $section =~ s/^\s*//g; $section =~ s/\s*$//g; next; } if (m{ \A \s* ([^=]+?) \s* = \s* (?:["']?) ([^"']*?) (?:["']?) \s* \Z }x) { my $key = lc($1); $key =~ tr/_-//d; $conf{$section}{$key} = [ $1, $2 ]; next; } if (m{ \A \s* ([^\s]+) \s* \Z }x) { my $key = lc($1); $key =~ tr/_-//d; $conf{$section}{$key} = [ $1, 'enabled' ]; } } close $mycnf_fh; return unless scalar keys(%conf); return \%conf; } sub get_pureftpd_conf_href { my %conf; if ( open( my $pureftpdconf_fh, '<', $PURE_FTPD_CONF_FILE ) ) { while (<$pureftpdconf_fh>) { next if /^(#|$)/; if (m{ \A \s* ([^\s]+?) \s+ (.*) \Z }x) { my $key = lc($1); $conf{$key} = { name => $1, value => $2 }; } } close $pureftpdconf_fh; } return \%conf; } sub get_proftpd_conf_href { my %conf; if ( open( my $proftpdconf_fh, '<', '/etc/proftpd.conf' ) ) { while (<$proftpdconf_fh>) { next if /^(#|$)/; if (m{ \A \s* ([^\s]+?) \s+ (.*) \Z }x) { my $key = lc($1); $conf{$key} = { name => $1, value => $2 }; } } close $proftpdconf_fh; } return \%conf; } sub get_exim_localopts_href { my %conf; if ( open( my $conf_fh, '<', '/etc/exim.conf.localopts' ) ) { local $/ = undef; %conf = map { ( split( /=/, $_, 2 ) )[ 0, 1 ] } split( /\n/, readline($conf_fh) ); close $conf_fh; } return \%conf; } sub get_hostinfo_href { my $info = { 'environment' => get_environment(), 'hardware' => timed_run( 0, 'uname', '-i' ), 'kernel' => timed_run( 0, 'uname', '-r' ), 'installtime' => undef, 'installtime_epoch' => undef, 'isLVE' => 0, }; chomp @$info{qw(hardware kernel)}; my $kernmods = get_kernel_modules_aref(); $info->{isLVE} = $kernmods->{kmodlve}; if ( i_am('ubuntu') ) { my $get_root_df = SSP::Util::timed_run( 4, 'df', '/' ); my ($get_root_name) = ( split( /\s+/, $get_root_df ) )[7]; my $tuneFS = SSP::Util::timed_run( 0, 'dumpe2fs', $get_root_name ); my @tuneFS = split /\n/, $tuneFS; my $line; my $installtime; foreach $line (@tuneFS) { next unless ( $line =~ m/Filesystem created:/ ); my ($cd) = ( split( /\s+/, $line ) )[2]; my ($m) = ( split( /\s+/, $line ) )[3]; my ($d) = ( split( /\s+/, $line ) )[4]; my ($hms) = ( split( /\s+/, $line ) )[5]; my ($y) = ( split( /\s+/, $line ) )[6]; $installtime = $cd . " " . $m . " " . $d . " " . $hms . " " . $y; $info->{'installtime'} = $installtime; } } else { my $rpm_name = 'basesystem'; my $rpms; if ( $rpms = get_rpm_href() and exists $rpms->{$rpm_name}->[0]->{'installtime'} ) { $info->{'installtime_epoch'} = $rpms->{$rpm_name}->[0]->{'installtime'}; $info->{'installtime'} = scalar localtime( $info->{'installtime_epoch'} ); } } return $info; } sub get_environment { my $envtype; if ( open my $envtype_fh, '<', '/var/cpanel/envtype' ) { $envtype = readline($envtype_fh); close $envtype_fh; } else { $envtype = timed_run( 0, '/usr/local/cpanel/bin/envtype' ); } chomp $envtype if $envtype; if ( !$envtype ) { return 'unknown-envtype'; } return $envtype; } sub get_cpuinfo_href { my %cpuinfos; open my $cpuinfo_fh, '<', '/proc/cpuinfo'; for my $line ( readline $cpuinfo_fh ) { if ( $line =~ /^model name/m ) { $line =~ s/^model name\s+:\s+//; $line =~ s/\(R\)//g; $line =~ s/\(tm\)//g; $line =~ s/\s{2,}/ /; $line =~ s/\s*\@/ \@/; $cpuinfos{'model'} = $line; $cpuinfos{'numcores'}++; } if ( $line =~ /^cpu MHz/m ) { $line =~ s/^cpu MHz\s+:\s+//; $cpuinfos{'mhz'} = $line; } } close $cpuinfo_fh; chomp %cpuinfos; return \%cpuinfos; } sub get_meminfo { # General logic from WHM 56 Cpanel::Sys::Hardware::Memory my $proc_meminfo = '/proc/meminfo'; my $proc_beancounters = '/proc/user_beancounters'; my %meminfo; my $hostinfo = get_hostinfo_href(); if ( defined( $hostinfo->{'environment'} ) && $hostinfo->{'environment'} eq 'virtuozzo' || $hostinfo->{'environment'} eq "vzcontainer" ) { # https://wiki.openvz.org/UBC_primary_parameters#vmguarpages # https://wiki.openvz.org/UBC_secondary_parameters#privvmpages if ( open( my $proc_beancounters_fh, '<', $proc_beancounters ) ) { while (<$proc_beancounters_fh>) { if (m/^\s*(\S+)\s+(.*)/) { my $type = $1; my $parm = $2; chomp($parm); my ( $held, $maxheld, $barrier, $limit, $failcnt ) = split( /\s+/, $parm ); next if $held eq '-'; # NOTE: VZ uses the # of 4-KiB pages, convert to KiB. # installed value is the lowest of privvmpages, physpages, or vmguarpages barrier (ignoring 0) if ( $type =~ /^(privvmpages|physpages|vmguarpages)$/ ) { unless ( $barrier eq "0" || ( defined( $meminfo{'installed'} ) && $meminfo{'installed'} <= ( $barrier * 4 ) ) ) { $meminfo{'installed'} = $barrier * 4; } } elsif ( $type eq 'oomguarpages' ) { $meminfo{'used'} = $held * 4; } elsif ( $type eq 'swappages' ) { $meminfo{'swapinstalled'} = $limit * 4; } } } close($proc_beancounters_fh); $meminfo{'available'} = $meminfo{'installed'} - $meminfo{'used'}; } } elsif ( open my $proc_meminfo_fh, '<', $proc_meminfo ) { while (<$proc_meminfo_fh>) { if (/^\s*([^\:]+):\s+(\d+)/) { $meminfo{ lc($1) } = $2; } } close $proc_meminfo_fh; $meminfo{'available'} = $meminfo{'memfree'} + $meminfo{'buffers'} + $meminfo{'cached'}; $meminfo{'installed'} = $meminfo{'memtotal'}; $meminfo{'used'} = sprintf( '%u', $meminfo{'memtotal'} - $meminfo{'memfree'} ); $meminfo{'swapinstalled'} = $meminfo{'swaptotal'}; } chomp %meminfo; return \%meminfo; } sub get_cpupdate_conf { my ($conf) = @_; $conf = defined($conf) ? $conf : '/etc/cpupdate.conf'; my %conf; if ( open( my $conf_fh, '<', $conf ) ) { local $/ = undef; %conf = map { ( split( /=/, $_, 2 ) )[ 0, 1 ] } split( /\n/, readline($conf_fh) ); close $conf_fh; } return \%conf; } sub format_meminfo { my ($num) = @_; return 'none or unknown' if ( !defined($num) ); my $hostinfo = get_hostinfo_href(); # The original values are 9223372036854775807 and 2147483647 4-KiB pages if ( defined( $hostinfo->{'environment'} ) && $hostinfo->{'environment'} eq 'virtuozzo' || $hostinfo->{'environment'} eq "vzcontainer" ) { return $num = 'unlimited' if $num == '36893488147419103228' + 0; # KiB if ( defined( $hostinfo->{'hardware'} ) && $hostinfo->{'hardware'} eq 'i386' ) { return $num = 'unlimited' if $num == '8589934588' + 0; # KiB } } return int( $num / 1024 ) . "MB"; } sub get_whm_install_info { my $info = { 'installtime' => undef, 'installtime_epoch' => undef, 'installversion' => undef, 'lastupdatetime' => undef, 'lastupdatetime_epoch' => undef, }; my $install_log = '/var/log/cpanel-install.log'; if ( -f $install_log ) { my $birthday_mtime = ( stat($install_log) )[9]; $info->{'installtime'} = scalar localtime($birthday_mtime); $info->{'installtime_epoch'} = $birthday_mtime; if ( open( my $conf_fh, '<', $install_log ) ) { while ( readline($conf_fh) ) { if (m{Target \s version \s set \s to \s '(\d+\.\d+[^']+)'}xms) { $info->{'installversion'} = $1; if (m{(\d+-\d+-\d+ \s+ \d+:\d+:\d+ (?:\s \-\d+)?)}xms) { $info->{'installtime'} = $1; } last; } last if $. >= 20_000; } close $conf_fh; } } if ( my $update_mtime = ( stat($CPANEL_VERSION_FILE) )[9] ) { $info->{'lastupdatetime'} = scalar localtime($update_mtime); $info->{'lastupdatetime_epoch'} = $update_mtime; } return $info; } sub print_info { my $text = shift; print BOLD YELLOW ON_BLACK "[INFO] * $text"; } sub print_warn { my $text = shift; print BOLD RED ON_BLACK "[WARN] * $text"; } sub print_crit { my $text = shift || ''; my $buffer = shift || \$CRIT_BUFFER; $$buffer .= BOLD MAGENTA ON_BLACK '[CRIT] * ' . $text; print BOLD MAGENTA ON_BLACK '[CRIT] * ' . $text; } sub print_critical { my $text = shift || ''; my $buffer = shift || \$CRIT_BUFFER; $$buffer .= BOLD MAGENTA ON_BLACK $text . "\n"; print BOLD MAGENTA ON_BLACK $text . "\n"; } sub print_footer { SSP::Security::print_footer_hacked(); return unless $CRIT_BUFFER; if ($CRIT_BUFFER) { my $cnt = 0; my $crit_border = "*" x 42; if ( !exists( $ENV{'STY'} ) ) { while ( $cnt <= 8 ) { SSP::Util::blink_text("* Please review CRITICAL output above! *"); ## Should be 80 characters or less. Too long and it may not work. $cnt++; } } print RED $crit_border; print RED "\n* Please review CRITICAL output above! *\n"; print RED $crit_border; print "\n"; } } sub blink_text { my $text = shift; print BLACK $text; print "\b" x length($text); select( undef, undef, undef, 0.1 ); ## no critic (BuiltinFunctions::ProhibitSleepViaSelect) print RED $text; print "\b" x length($text); select( undef, undef, undef, 0.3 ); ## no critic (BuiltinFunctions::ProhibitSleepViaSelect) print "\b" x length($text); } sub print_3rdp { my $text = shift; print BOLD GREEN ON_BLACK "[3RDP] * $text"; } sub print_3rdp2 { my $text = shift; print BOLD GREEN ON_BLACK "$text\n"; } ## precedes informational items (e.g., "Hostname:") sub print_start { my $text = shift; print BOLD YELLOW ON_BLACK $text; } ## for informational items (e.g., the server's hostname) sub print_normal { my $text = shift; print BOLD CYAN ON_BLACK "$text\n"; } ## for important things (e.g., "Hostname is not a FQDN") sub print_warning { my $text = shift; print BOLD RED ON_BLACK "$text\n"; } ## for other imporant things (e.g., "You are in an LVE, do not restart services") sub print_warning_underline { my $text = shift; print BOLD UNDERLINE "$text\n"; } sub print_info2 { my $text = shift; print BOLD GREEN ON_BLACK "$text\n"; } sub print_magenta { my $text = shift; print BOLD MAGENTA ON_BLACK "$text\n"; } sub print_red { my $text = shift; print BOLD RED ON_BLACK "$text\n"; } sub get_lsws_version_aref { my $lshttpd = '/usr/local/lsws/bin/lshttpd'; return [] unless my @lshttpd_version_output = split /\n/, timed_run( 0, $lshttpd, '-v' ); my ( $lsws_full_version, $lsws_numeric_version ) = (); for (@lshttpd_version_output) { if (m{ \A (LiteSpeed/(\d+(?:\.\d+){1,2}).*) }xms) { $lsws_full_version = $1; $lsws_numeric_version = $2; } } $lsws_full_version = "unknown" if !$lsws_full_version; $lsws_numeric_version = "unknown" if !$lsws_numeric_version; return [ $lsws_full_version, $lsws_numeric_version ]; } sub get_node_info_href { return unless ( -x '/usr/local/cpanel/bin/whmapi1' ); return unless my $raw = timed_run( 0, '/usr/local/cpanel/bin/whmapi1', 'get_current_profile', '--output=json' ); my $json_output = get_json_href($raw); return $json_output; } sub get_linked_server_nodes { return unless ( -e '/usr/local/cpanel/bin/whmapi1' ); return unless my $raw = timed_run( 0, '/usr/local/cpanel/bin/whmapi1', 'list_linked_server_nodes', '--output=json' ); my $json_output = get_json_href($raw); return $json_output; } sub get_json_href { my ( $raw, $fail_warning ) = @_; return unless defined $raw; my $json = load_module_with_fallbacks( 'needed_subs' => [qw{new utf8 decode}], 'modules' => [qw{Cpanel::JSON::XS JSON::XS JSON::PP}], 'fail_warning' => $fail_warning, ); return {} unless $json; # since this expects a href back, we should return an empty set in the event that the json module cannot be loaded my $href; local $@; eval { $href = $json->new->utf8->decode($raw); }; # All or nothing, just be quiet. return {} if !$href; # since this expects a href back, we should return an empty set if the raw data passed in was not valid json return $href; } sub get_installed_ea4_php_href { # Only supports WHM 54+ return unless i_am('ea4'); my $php = {}; my @available_php; my @current_php; my ( $available_php, $current_php, $phpverfull, $phpver, $relver ); (@current_php) = split( /\n/, timed_run( 0, '/usr/local/cpanel/bin/rebuild_phpconf', '--current' ) ); foreach my $line (@current_php) { my $pkg; if ( $line =~ m{ DEFAULT \s PHP: \s (\S+) }xms ) { $pkg = $1; $php->{$pkg}->{default_php} = 1; $php->{default} = $pkg; next; } if ( $line =~ m{ (\S+) \s SAPI: \s (\S+) }xms ) { $pkg = $1; $php->{$pkg}->{handler} = $2; if ( SSP::Util::i_am('ubuntu') ) { $phpverfull = timed_run( 0, 'dpkg-query', '-W', '-f', '${Version}', "$pkg" ); $phpverfull =~ s/\+/\./; ( $phpver, $relver ) = ( split( /\-/, $phpverfull ) ); } else { $phpver = timed_run( 0, 'rpm', '-q', "$pkg", '--queryformat', '%{VERSION}' ); $relver = timed_run( 0, 'rpm', '-q', "$pkg", '--queryformat', '%{RELEASE}' ); } $php->{$pkg}->{version} = $phpver; $php->{$pkg}->{release_version} = $phpver; } } return $php; } sub get_new_backup_conf_href { my $new_backup_config = '/var/cpanel/backups/config'; return unless -f $new_backup_config; return unless open( my $backupconf_fh, '<', $new_backup_config ); local $/ = undef; my $new = { map { ( split( /:\s/, $_, 2 ) )[ 0, 1 ] } split( /\n/, readline($backupconf_fh) ) }; close $backupconf_fh; return $new; } sub get_last_lines_from_file { # Returns array-ref containing the LAST $return_limit (number) lines that match $pattern which exist within $bytes_limit bytes of the end of $file. # Everything but $file is optional. # TODO: Read file in reverse similar to File::ReadBackwards (not using it right now because not a core module) my ( $file, $pattern, $return_limit, $bytes_limit ) = @_; return [] unless -e $file; return [] unless my $size = ( stat($file) )[7]; $bytes_limit = defined $bytes_limit ? $bytes_limit + 0 : 5_000_000; # Default is last 5MB of file $return_limit = defined $return_limit ? $return_limit + 0 : 1; # Default is 1 line returned $pattern = defined $pattern ? $pattern : qr{}; # Default is any match my @lines; if ( open my $fh, '<', $file ) { seek $fh, -$bytes_limit, 2; while (<$fh>) { if (m{$pattern}xms) { chomp; shift @lines if scalar @lines >= $return_limit; push @lines, $_; } } close $fh; } return \@lines; } sub tail_array_after_match { # Modifies $aref in place by searching backwards for first $pattern match and discarding the match and everything before it. my ( $aref, $pattern ) = @_; for ( my $i = $#{$aref}; $i >= 0; $i-- ) { next unless $aref->[$i] =~ $pattern; splice( @{$aref}, 0, $i + 1 ); last; } } sub check_loopback_connection { return if $OPT_SKIP_NETWORKING; return unless i_am_one_of( 'cpanel', 'dnsonly' ); my @ports = qw( 25 80 2086 ); my $connected = 0; for my $port (@ports) { my $sock = IO::Socket::INET->new( PeerAddr => '127.0.0.1', PeerPort => $port, Proto => 'tcp', Timeout => '1', ); if ($sock) { $connected = 1; close $sock; last; } } if ( !$connected ) { print_warn('Loopback connectivity: '); print_warning('could not connect to 127.0.0.1 on port 25, 80, or 2086'); } } sub get_attributes { # @want is optional, not specifying anything returns all checked attributes my ( $path, @want ) = @_; open( my $fh, '<', $path ) or return; my %attributes = ( 'APPEND-ONLY' => 0x00000020, # FS_APPEND_FL in linux/fs.h 'IMMUTABLE' => 0x00000010, # FS_IMMUTABLE_FL in linux/fs.h 'UNDELETABLE' => 0x00000002, # FS_UNRM_FL in linux/fs.h ); my $FS_IOC_GETFLAGS = 0x80086601; # Tested on CentOS 6.7 and 7.2 using: strace -e trace=ioctl -e raw=ioctl lsattr -d / my $flags = pack 'i', 0; return unless defined ioctl( $fh, $FS_IOC_GETFLAGS, $flags ); close $fh; $flags = unpack 'i', $flags; @want = keys(%attributes) if !scalar @want; my %result; foreach my $attr (@want) { next unless exists $attributes{$attr}; $result{$attr} = 1 if $flags & $attributes{$attr}; } return %result; } sub get_hostname_ssl_info { my $ssl_path = '/var/cpanel/ssl/cpanel'; return unless ( -d $ssl_path ); if ( -s "$ssl_path/mycpanel.pem" ) { my $enddate = timed_run( 2, 'openssl', 'x509', '-in', "$ssl_path/mycpanel.pem", '-noout', '-enddate' ); my ($expdate) = ( split( /\=/, $enddate ) )[1]; chomp($expdate); return GREEN " - SSL Certificate is signed and expires on $expdate"; } if ( -s "$ssl_path/cpanel.pem" ) { return YELLOW " - SSL Certificate is self-signed"; } if ( !-e "$ssl_path/mycpanel.pem" && !-e "$ssl_path/cpanel.pem" ) { return RED " - cpanel.pem and mycpanel.pem files are both missing. Causes many issues!"; } } sub get_fcap { my ($path) = @_; my $getcap = '/usr/sbin/getcap'; return unless -x $getcap; return unless -e $path; my $output = timed_run( 0, $getcap, $path ); chomp($output); my @result; if ( SSP::Util::i_am_one_of( 'rocky', 'almalinux' ) ) { $output =~ s/^$path //g; ## Rocky/AlmalLinux } if ( SSP::Util::i_am('ubuntu') ) { if ( os_version_is(qw ( < 22.04 )) ) { $output =~ s/^$path\s\=\s//g; ## Ubuntu 20.04 } else { $output =~ s/^$path //g; ## Rocky/AlmalLinux } } if ( SSP::Util::i_am('centos') ) { $output =~ s/^$path\s\=\s//g; ## CentOS } $output =~ s/\=/\+/; @result = split( /,|\+/, $output ); return @result; } sub get_mysql_full_version { return unless ( my $mysql_version = timed_run( 0, 'mysql', '-BNe', "SELECT VERSION();" ) ); my $mysql_version_string = ""; my $mysql_version_vars = timed_run( 0, 'mysql', '-BNe', "SHOW VARIABLES LIKE '%version%';" ); my @mysql_version_vars = split /\n/, $mysql_version_vars; foreach my $line (@mysql_version_vars) { $mysql_version_string .= $line if ( $line =~ m{version_comment|version_machine} ); } chomp($mysql_version); $mysql_version_string =~ s/^\s+|version_comment|version_machine|\s+$//g; my $mysql_output = $mysql_version . $mysql_version_string; chomp $mysql_output; return $mysql_output; } sub get_mysql_numeric_version { return unless my $version = get_mysql_full_version(); my $numeric; if ( $version =~ m{Distrib \s* ([0-9A-Za-z.]+)}xms ) { $numeric = $1; } return $numeric; } sub get_mysql_datadir { my $datadir = '/var/lib/mysql/'; my $mysql_conf = get_mysql_conf_href(); if ( defined $mysql_conf and defined $mysql_conf->{'mysqld'} and defined $mysql_conf->{'mysqld'}{'datadir'} ) { $datadir = $mysql_conf->{'mysqld'}{'datadir'}[1]; if ( $datadir !~ m{ / \z }xms ) { $datadir .= '/'; } } return $datadir; } sub get_cron_files { my @crons; my @cronlist = glob( q{ /etc/cron.d/{.,}* /etc/cron.hourly/{.,}* /etc/cron.daily/{.,}* /etc/cron.weekly/{.,}* /etc/cron.monthly/{.,}* /etc/crontab /var/spool/cron/root /var/spool/cron/crontabs/root } ); my %cron_ignore = ( # These contain 'interesting' commands that should be ignored if they aren't going to cause problems '7440999604d3517ca235c7949f803ece' => '/etc/cron.daily/maldet', # Default 'aede9174c0a1b0cf225165e204aa6fd8' => '/etc/cron.hourly/modsecparse.pl', # Default '195ddc2ac97502c2a96cc758cb7c9097' => '/etc/cron.daily/freshclam', # Default 'c7a32553c9f6d3d16c07281cae8572e9' => '/etc/cron.daily/tmpwatch', # Default '3ffb5926bb7533bb077e2ec37f767851' => '/etc/cron.daily/tmpwatch', # modified to not remove symlinks 'ff511360a325783a06f494a05b514689' => '/etc/cron.d/kill_orphaned_php-cron', # standard CloudLinux script 'f392372e36f56fb3e70e850a6bd1a550' => '/etc/cron.d/lvedbgovernor-utils-cron', # standard CloudLinux script 'f4498bc07101fd668dc63e88ce0bb840' => '/etc/cron.daily/csget', # configserver.com ); for my $cron (@cronlist) { next if !( -f $cron || -l $cron ); chomp( my $checksum = timed_run( 0, 'md5sum', $cron ) ); $checksum =~ s/\s.*//g; next if ( $checksum && $cron_ignore{$checksum} ); push @crons, $cron; } return \@crons; } sub _resolve { my ( $addr, $report_errors, $timeout ) = @_; return if $OPT_SKIP_NETWORKING; $report_errors = ( defined $report_errors && $report_errors ne "0" ) ? 1 : 0; $timeout = defined $timeout ? $timeout : 3; my @results; local $SIG{'ALRM'} = sub { if ($report_errors) { print_warn('Resolver: '); print_warning( 'Timed out (' . $timeout . ' seconds) resolving "' . $addr . '"' ); } return; }; alarm $timeout; if (%SOCKET) { my $getnameinfo_flags = ( $addr =~ /^\d+\.\d+\.\d+\.\d+$/ ) ? $SOCKET{'NI_NAMEREQD'} : $SOCKET{'NI_NUMERICHOST'}; # If looking up IP address we require name resolution, otherwise we want the IP address returned. my ( $err, @socks ) = $SOCKET{'getaddrinfo'}->( $addr, "", { socktype => $SOCKET{'SOCK_RAW'} } ); if ( $report_errors && $err ) { print_warn('Resolver: '); print_warning(qq{getaddrinfo() failed to resolve "$addr": $err}); } foreach my $sock (@socks) { my ( $err, $result ) = $SOCKET{'getnameinfo'}->( $sock->{addr}, $getnameinfo_flags, $SOCKET{'NIx_NOSERV'} ); if ( $report_errors && $err ) { print_warn('Resolver: '); print_warning(qq{getnameinfo() failed to resolve "$addr": $err}); next; } push @results, $result; } } else { # Fall back to older (deprecated) Socket functions my %h_errno = ( '1' => 'HOST_NOT_FOUND', '2' => 'TRY_AGAIN', '3' => 'NO_RECOVERY', '4' => 'NO_DATA' ); local $?; if ( $addr =~ /^\d+\.\d+\.\d+\.\d+$/ ) { my $packed = inet_aton($addr); return unless defined $packed; my $result = gethostbyaddr( $packed, AF_INET ); my $error = $? ? "h_errno " . ( exists $h_errno{$?} ? $h_errno{$?} : $? ) : 0; if ( $report_errors && $error ) { print_warn('Resolver: '); print_warning( 'gethostbyaddr() failed to resolve "' . $addr . '": ' . $error ); return; } push @results, $result; } else { my $packed_result = gethostbyname($addr); my $error = $? ? "h_errno " . ( exists $h_errno{$?} ? $h_errno{$?} : $? ) : 0; if ( $report_errors && $error ) { print_warn('Resolver: '); print_warning( 'gethostbyname() failed to resolve "' . $addr . '": ' . $error ); } if ( defined $packed_result ) { my $result = inet_ntoa($packed_result); push @results, $result; } } } alarm 0; return @results; } sub get_clock_skew { return if $OPT_SKIP_NETWORKING; my $rdatecmd = os_version_is(qw ( >= 8 )) ? "/usr/local/cpanel/bin/rdate" : "rdate"; my $rdateTimeout = os_version_is(qw ( >= 8 )) ? "" : qq(-t 3); ## last updated 2018-03-24 ## we do this to avoid having to do the DNS lookup my @rdate_servers = qw( 208.74.121.36 208.74.121.43 208.74.123.15 208.74.123.23 ); my $localtime = time(); my $rdate_time; my $clock_skew; my %months = qw( Jan 0 Feb 1 Mar 2 Apr 3 May 4 Jun 5 Jul 6 Aug 7 Sep 8 Oct 9 Nov 10 Dec 11); for ( 1 .. 2 ) { my $num = int rand scalar @rdate_servers; $rdate_time = timed_run( 10, $rdatecmd, '-p', $rdateTimeout, $rdate_servers[$num] ); next if $rdate_time =~ /timeout/; last if $rdate_time; } return if !$rdate_time; $rdate_time =~ s/\A rdate: \s \[[^\]]+\] \s+//gxms; if ( $rdate_time =~ m{ \A \S+ \s+ (\S+) \s+ (\d+) \s+ (\d+):(\d+):(\d+) \s+ (\d+) }xms ) { my ( $mon, $mday, $hour, $min, $sec, $year ) = ( $1, $2, $3, $4, $5, $6 ); $mon = $months{$mon}; $rdate_time = timelocal( $sec, $min, $hour, $mday, $mon, $year ); } return if ( $rdate_time !~ /\d{10,}/ ); $clock_skew = ( $rdate_time - $localtime ); $clock_skew = abs $clock_skew; # convert negative numbers to positive return $clock_skew; } sub get_license_info { return unless my $external_ip_address = get_external_ip(); my $path = '/api/ipaddrs?ip=' . $external_ip_address; my $host = 'verify.cpanel.net'; return _http_get( Host => $host, Path => $path, ReportTimeout => 1, SSL => 1, SSLLoadFailWarning => 'can\'t make HTTPS request to verify.cpanel.net' ); } sub get_cl_license_info { return unless my $external_ip_address = get_external_ip(); return timed_run( 4, 'curl', '-s', "https://cln.cloudlinux.com/wapi/check/ip/$external_ip_address" ); } sub get_exim_version_build_date { my $eximbin = '/usr/sbin/exim'; return unless -x $eximbin; return unless my $exim_out = timed_run( 0, $eximbin, '--version' ); my $ver; my $build_date; if ( $exim_out =~ m{ version \s ([^\s]+) (?:\s \#\d+)? \s built \s ([^\s]+) }xms ) { $ver = $1; $build_date = $2; } return unless $ver; return ( $ver, $build_date ); } sub get_kernel_modules_aref { my %kernel_modules = map { ( split( /\s+/, $_ ) )[0] => $_ } split /\s+/, timed_run( 0, 'lsmod' ); return \%kernel_modules; # my $lsmod = timed_run( 0, 'lsmod' ); # my @kernel_modules; # for my $line ( split( /\n/, $lsmod ) ) { # next if ( $line =~ m/\A Module \s+/x ); # push @kernel_modules, $1 if ( $line =~ m/\A ([^\s]+) \s/x ); # } # return \@kernel_modules; } sub missing_open_opath_flag { return if os_version_is(qw( < 7 )); my $eloop = 'Too many levels of symbolic links'; my $SYS_open = 2; # See syscall.ph or h2ph my $SYS_close = 3; my $sym = '/proc/self'; my $flags = 0x2e0000; # O_RDONLY|O_NOFOLLOW|O_NOATIME|O_CLOEXEC|O_PATH my $buf = pack q/i/, 0; local $! = 0; my $fd; eval { $fd = syscall( $SYS_open, $sym, $flags, $buf ); }; unless ($!) { syscall( $SYS_close, $fd ) if ($fd); return; } ( $! =~ m/\Q$eloop\E/ ) ? return 1 : return; } sub build_libkeyutils_file_list { my @dirs = qw( /lib /lib/tls /lib64 /lib64/tls ); my @libkeyutils_files; for my $dir (@dirs) { next unless -e $dir; opendir( my $dir_fh, $dir ); while ( my $file = readdir($dir_fh) ) { if ( $file =~ /^libkeyutils\.so\.(?:[\.\d]+)?$/ ) { push @libkeyutils_files, "$dir/$file\n"; } } closedir $dir_fh; } chomp @libkeyutils_files; return \@libkeyutils_files; } sub eximbin_stat { my ($aref) = @_; my @exists; my $eximbin = qw( /usr/sbin/exim ); my $statbin = qw( /usr/bin/stat ); foreach my $file (@$aref) { push @exists, $file if ( grep { /File: \'$file\'/ } timed_run( 0, $eximbin, '-be', '\'${run {' . $statbin . ' ' . $file . '}}\'' ) ); } return \@exists; } sub get_rpm_href { return get_apt_href() if ( i_am('ubuntu') ); my $timeout = $OPT_TIMEOUT ? $OPT_TIMEOUT : 25; return unless my $list = timed_run( $timeout, 'rpm', '-qa', '--queryformat', q{%{NAME}\t%{VERSION}\t%{RELEASE}\t%{ARCH}\t%{INSTALLTIME}\n} ); my %rpms; for my $line ( split( /\n/, $list ) ) { my ( $name, $version, $release, $arch, $installtime ) = split( /\t/, $line ); push @{ $rpms{$name} }, { 'version' => defined $version ? $version : '', 'release' => defined $release ? $release : '', 'arch' => defined $arch ? $arch : '', 'installtime' => defined $installtime ? $installtime : '', }; } return \%rpms; } sub get_apt_href { my $timeout = $OPT_TIMEOUT ? $OPT_TIMEOUT : 25; # dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}' return unless my $list = timed_run( $timeout, 'dpkg-query', '-W', '-f=${binary:Package}\t${Version}\t${Architecture}\n' ); my %rpms; for my $line ( split( /\n/, $list ) ) { my ( $name, $version, $arch ) = split( /\t/, $line ); push @{ $rpms{$name} }, { 'version' => defined $version ? $version : '', 'arch' => defined $arch ? $arch : '', }; } return \%rpms; } sub get_printable_rpm_packages { my ($name) = @_; return unless my $rpms = get_rpm_href(); return unless exists $rpms->{$name}; my @list; for my $ref ( @{ $rpms->{$name} } ) { if ( SSP::Util::i_am('ubuntu') ) { push @list, $name . "-" . $ref->{version} . "." . $ref->{arch}; } else { push @list, $name . "-" . $ref->{version} . "-" . $ref->{release} . "." . $ref->{arch}; } } @list = sort @list; return @list; } sub get_flat_conf_href { # Import an unstructured, commented config file delimited by whitespace, equal, or colon after a single keyword. # Hash keys are stored lowercase if $lower_key is true (only use when configuration keywords are case-insensitive!) # The original keyword case is stored in array position 0, and the value is stored in array position 1. # Should work for sshd_config with $lowercase=1, cpanel.config and php.ini with $lowercase=0, etc. # This does not work for configuration keywords that can be used multiple times, only the last keyword found will be stored. my ( $file, $lower_key ) = @_; return unless defined $file; return unless -e $file; my %conf; open my $fh, '<', $file or return; while ( my $line = <$fh> ) { next if $line =~ m{ \A \s* ( [;#] | \s* \Z ) }xms; # Ignore comments prefixed by hash or semicolon, or empty lines my ( $key, $value ) = split( /\s*[\s=:]\s*/, $line, 2 ); # Split on whitespace, =, or :. Ignore additional whitespace. next unless defined $key; $key =~ s{ \A \s+ | \s+ \Z }{}gxms; # Strip leading/trailing whitespace $key =~ s{ \A ['"] | ['"] \Z }{}gxms; # Strip leading/trailing quotes if ( defined $value ) { $value =~ s{ \A \s+ | \s+ \Z }{}gxms; $value =~ s{ \A ['"] | ['"] \Z }{}gxms; } $conf{ $lower_key ? lc($key) : $key } = [ $key, $value ]; } close $fh; return \%conf; } sub get_myip { my ($port) = @_; $port = defined $port ? $port : '80'; # myip.cpanel.net supports HTTP ports 80, 2089 and HTTPS port 443. my $ip; my $reply = _http_get( Host => 'myip.cpanel.net', Path => '/v1.0/', ReportTimeout => 1, Port => $port ); if ( defined($reply) && $reply =~ m{ ^ \s* ([0-9]+.[0-9]+.[0-9]+.[0-9]+) \s* $ }xms ) { $ip = $1; chomp $ip; } return $ip; } sub get_external_ip { return get_myip(); } sub get_external_license_ip { return get_myip('2089'); } sub _http_get { # SSL connections have external dependencies that are not part of core Perl 5.6. my (%opts_in) = @_; return if $OPT_SKIP_NETWORKING; my %opts = ( Agent => "SSP/${VERSION}", HostReconnectDelay => 0.25, MaxReply => 50_000, MultiHomed => 1, Path => '/', Proto => 'tcp', ReportTimeout => 0, ReportTimeoutHeader => '', SSL => 0, SSLLoadFailWarning => undef, SSLVerifyHost => 1, Tries => 1, WantHeaders => 0, %opts_in ); my $try = 0; my $reply; my $scheme; die unless defined $opts{Host}; if ( $opts{SSL} ) { $scheme = 'https://'; return unless load_module_with_fallbacks( 'needed_subs' => [qw(start_SSL import)], 'modules' => [qw(IO::Socket::SSL)], 'fail_warning' => $opts{SSLLoadFailWarning}, ); $opts{Port} = 443 if not defined $opts{Port}; $opts{Timeout} = 3 if not defined $opts{Timeout}; } else { $scheme = 'http://'; $opts{Port} = 80 if not defined $opts{Port}; $opts{Timeout} = 2 if not defined $opts{Timeout}; } for ( 1 .. $opts{Tries} ) { local $@; $try++; eval { my $sock; local $SIG{'ALRM'} = sub { close $sock if defined $sock; die "alarm\n"; }; my $now = time(); if ( defined $HTTP_GET_HOST_CACHE->{ $opts{Host} } and $now - 1 - $HTTP_GET_HOST_CACHE->{ $opts{Host} } <= $opts{HostReconnectDelay} ) { select( undef, undef, undef, $opts{HostReconnectDelay} ); ## no critic (ProhibitSleepViaSelect) # old perl compat } $HTTP_GET_HOST_CACHE->{ $opts{Host} } = $now; alarm $opts{Timeout} + ( $opts{MultiHomed} ? $opts{Timeout} + 1 : 0 ); # MultiHomed = double timeout + 1 to give it a chance to work $sock = IO::Socket::INET->new( MultiHomed => $opts{MultiHomed}, PeerAddr => $opts{Host}, PeerPort => $opts{Port}, Proto => $opts{Proto}, Timeout => $opts{Timeout}, ); if ( $opts{SSL} ) { my $ssl_verify_opt; ## no critic (StringyEval) # The SSL_VERIFY_* constants may not be loaded at compile time. eval q( IO::Socket::SSL->import(qw(SSL_VERIFY_PEER SSL_VERIFY_NONE)); $ssl_verify_opt = $opts{SSLVerifyHost} ? IO::Socket::SSL::SSL_VERIFY_PEER : IO::Socket::SSL::SSL_VERIFY_NONE; ); ## use critic return unless defined $ssl_verify_opt; return unless IO::Socket::SSL->start_SSL( $sock, SSL_hostname => $opts{Host}, SSL_verify_mode => $ssl_verify_opt ); } my $header_host = ( $opts{Port} != 80 and $opts{Port} != 443 ) ? $opts{Host} . ":" . $opts{Port} : $opts{Host}; if ($sock) { print $sock "GET " . $opts{Path} . " HTTP/1.0\r\nUser-Agent: " . $opts{Agent} . "\r\nHost: " . $header_host . "\r\nAccept: */*\r\n\r\n"; my $content_length = 0; my $read_length = $opts{MaxReply}; while ( $content_length < $opts{MaxReply} ) { my $read = read $sock, my ($content), $read_length; last unless $read; $content_length += $read; $read_length -= $read; $reply .= $content; } close $sock; } alarm 0; }; if ( $@ eq "alarm\n" && $opts{ReportTimeout} ) { print_warn( $opts{ReportTimeoutHeader} . 'Request for ' . $scheme . $opts{Host} . ':' . $opts{Port} . $opts{Path} . ' timed out' ); print_warning( ( $opts{Tries} > 1 ) ? ': attempt ' . $try . ' of ' . $opts{Tries} : '' ); print RESET; next; } if ( defined $reply && length $reply ) { $reply =~ s/^.*?(\r\n){2}//s unless defined $opts{WantHeaders} && $opts{WantHeaders}; return $reply; } } return; } sub _month_to_num { # Convert "Jan" to 0 for timegm(), etc. local ($_) = @_; return 0 if /^Jan/i; return 1 if /^Feb/i; return 2 if /^Mar/i; return 3 if /^Apr/i; return 4 if /^May/i; return 5 if /^Jun/i; return 6 if /^Jul/i; return 7 if /^Aug/i; return 8 if /^Sep/i; return 9 if /^Oct/i; return 10 if /^Nov/i; return 11 if /^Dec/i; } sub print_bug_report { my $mysql_full_version = get_mysql_full_version(); my $hostinfo = get_hostinfo_href(); my $cpuinfo = get_cpuinfo_href(); my $version = _get_run_var('cpanel_original_version'); my $os = _get_run_var('os_release'); my $kernel = $hostinfo->{'kernel'} ? $hostinfo->{'kernel'} : 'Unknown'; my $arch = $hostinfo->{'hardware'} ? $hostinfo->{'hardware'} : 'Unknown'; my $environment = $hostinfo->{'environment'} ? $hostinfo->{'environment'} : 'Unknown'; my $cpu = $cpuinfo->{'model'} ? $cpuinfo->{'model'} : 'Unknown'; my $cores = $cpuinfo->{'numcores'} ? $cpuinfo->{'numcores'} : 'Unknown'; my $ticket = ( defined $ENV{HISTFILE} and $ENV{HISTFILE} =~ /ticket.(\d+)$/ ) ? $1 : ''; print <{'PRECACHED'} ? ' [precached]' : ''; print " ${func}${precached} elapsed:\n"; for my $key ( sort keys %{ $MEMOIZE_CACHE{$func}->{'PROFILE'} } ) { $combined_elapsed += $MEMOIZE_CACHE{$func}->{'PROFILE'}->{$key}->{'elapsed'}; $combined_precache += $MEMOIZE_CACHE{$func}->{'PROFILE'}->{$key}->{'elapsed'} if $precached; printf " %.3f s [%s]\n", $MEMOIZE_CACHE{$func}->{'PROFILE'}->{$key}->{'elapsed'}, $key; } } printf "\nCombined memoized functions elapsed time: %.3f s\n", $combined_elapsed; if ( defined $MEMOIZE_CACHE{'PRECACHE'} and defined $MEMOIZE_CACHE{'PRECACHE'}->{'PROFILE'} ) { my $precache_wall = $MEMOIZE_CACHE{'PRECACHE'}->{'PROFILE'}->{'elapsed'}; printf "Precache combined time elapsed: %.3f s\n", $combined_precache; printf "Precache wall time elapsed: %.3f s\n", $precache_wall; printf "Precache combined minus wall (wall time saved by precaching): %.3f s\n", ( $combined_precache - $precache_wall ); } print "\n"; my @times = times; my @labels = ( 'User', 'System', 'User (all children)', 'System (all children)' ); print "$labels[$_]: $times[$_] s\n" for 0 .. $#times; } } sub get_tiers_json_href { my $raw = _http_get( Host => 'httpupdate.cpanel.net', Path => '/cpanelsync/TIERS.json' ); return unless $raw; my $href = get_json_href($raw); return if not exists $href->{'tiers'}; return $href; } sub get_tier_info_for_version_href { my ($version) = @_; return unless my $tiers_ref = get_tiers_json_href(); my ( $parent_ver, $major_ver ) = split( /\./, $version, 3 ); my @found_tiers = (); return unless defined $parent_ver and $parent_ver =~ /^\d+$/; return unless defined $major_ver and $major_ver =~ /^\d+$/; $major_ver++ if $major_ver % 2; # Bump odd dev versions my $full_ver = join( '.', $parent_ver, $major_ver ); if ( exists $tiers_ref->{'tiers'}->{$full_ver} ) { return $tiers_ref->{'tiers'}->{$full_ver}->[0] unless ( scalar @{ $tiers_ref->{'tiers'}->{$full_ver} } gt 1 ); foreach my $tier ( @{ $tiers_ref->{'tiers'}->{$full_ver} } ) { return $tier if exists $tier->{'is_main'} && $tier->{'is_main'}; } } return; } sub i_am { ## no critic (RequireArgUnpacking) # All These Things are True my $want = 0; grep { return 0 unless exists $RUN_STATE->{type}->{$_}; $want |= $RUN_STATE->{type}->{$_} } @_; return $want == ( $want & $RUN_STATE->{STATE} ); } sub i_am_only { ## no critic (RequireArgUnpacking) # Only These Things are True my $want = 0; grep { return 0 unless exists $RUN_STATE->{type}->{$_}; $want |= $RUN_STATE->{type}->{$_} } @_; return $want == $RUN_STATE->{STATE}; } sub i_am_one_of { ## no critic (RequireArgUnpacking) # At Least One Of These Things are True return scalar grep { exists $RUN_STATE->{type}->{$_} and $RUN_STATE->{type}->{$_} & $RUN_STATE->{STATE} } @_; } sub cpanel_version_is { my ( $mode, $ver ) = @_; return version_compare( $RUN_STATE->{var}->{cpanel_numeric_version}, $mode, $ver ); } sub os_version_is { my ( $mode, $ver ) = @_; return version_compare( $RUN_STATE->{var}->{os_version}, $mode, $ver ); } sub get_hostname { # Return hostname() as-is on versions prior to 70. if ( cpanel_version_is(qw( < 11.69.0.0 )) ) { return hostname(); } my $h = timed_run( 10, 'hostname', '-f' ); chomp $h if defined $h; if ( not length($h) ) { # Fall back to Sys::Hostname $h = hostname(); } return $h; } # SUB load_module_with_fallbacks( # 'modules' => [ 'module1', 'module2', ... ], # 'needed_subs' => [ 'do_needful', ... ], # 'fallback' => sub { *do_needful = sub { ... }; return; }, # 'fail_warning' => "Oops, something went wrong, you may want to do something about this", # 'fail_fatal' => 1, # ); # # Input is HASH of options: # 'modules' => ARRAYREF of SCALAR strings corresponding to module names to attempt to import. These are attempted first. # 'needed_subs' => ARRAYREF of SCALAR strings corresponding to subroutine names you need defined from the module(s). # 'fallback' => CODEREF which defines the needed subs manually. Only used if all modules passed in above fail to load. Optional. # 'fail_warning' => SCALAR string that will convey a message to the user if the module(s) fail to load. Optional. # 'fail_fatal' => BOOL whether you want to die if you fail to load the needed subs/modules via all available methods. Optional. # # Returns the module/namespace that loaded correctly, throws if all available attempts at finding the desired needed_subs subs fail and fail_fatal is passed. sub load_module_with_fallbacks { my %opts = @_; my $namespace_loaded; foreach my $module2try ( @{ $opts{'modules'} } ) { # Don't 'require' it if we already have it. my $inc_entry = join( "/", split( "::", $module2try ) ) . ".pm"; if ( !$INC{$module2try} ) { local $@; next if !eval "require $module2try; 1"; ## no critic (StringyEval) } # Check if the imported modules 'can' do the job next if ( scalar( grep { $module2try->can($_) } @{ $opts{'needed_subs'} } ) != scalar( @{ $opts{'needed_subs'} } ) ); # Ok, we're good to go! $namespace_loaded = $module2try; last; } # Fallback to coderef, but don't do sanity checking on this, as it is presumed the caller "knows what they are doing" if passing a coderef. if ( !$namespace_loaded ) { if ( !$opts{'fallback'} || ref $opts{'fallback'} != 'CODE' ) { print_warn( 'Missing Perl Module(s): ' . join( ', ', @{ $opts{'modules'} } ) . ' -- ' . $opts{'fail_warning'} . " -- Try using /usr/local/cpanel/3rdparty/bin/perl?\n" ) if $opts{'fail_warning'}; die "Stopping here." if $opts{'fail_fatal'}; } else { $opts{'fallback'}->(); # call like main::subroutine instead of Name::Space::subroutine $namespace_loaded = 'main'; } } return $namespace_loaded; } sub _memoize { my $self = shift; my @functions = qw( check_for_non_default_permissions find_httpd_bin get_apache_modules_href get_apache_version_href get_clock_skew get_cpanel_license_file_info_href get_cpuinfo_href get_cpupdate_conf get_exim_localopts_href get_external_ip get_external_license_ip get_hostinfo_href get_hostname get_installed_ea4_php_href get_ipcs_href get_license_info get_local_ipaddrs_aref get_lsof_port_href get_lsws_version_aref get_meminfo get_mysql_conf_href get_mysql_full_version get_mysql_numeric_version get_new_backup_conf_href get_phpini_aref get_process_pid_href get_rpm_href get_tiers_file get_tiers_json_href ); for my $func (@functions) { if ( defined &{$func} ) { my $func_ref = \&{$func}; $MEMOIZE_CACHE{$func} = {}; my $cache_sub = sub { my $key = ( wantarray() ? 'L' : 'S' ) . '^' . join( ' ', @_ ); if ( not exists $MEMOIZE_CACHE{$func}->{$key} ) { if ( defined $MEMOIZE_CACHE{'PROFILING'} ) { $MEMOIZE_CACHE{$func}->{'PROFILE'}->{$key}->{'start'} = [ Time::HiRes::gettimeofday() ]; } $MEMOIZE_CACHE{$func}->{$key} = $func_ref->(@_); if ( defined $MEMOIZE_CACHE{'PROFILING'} ) { $MEMOIZE_CACHE{$func}->{'PROFILE'}->{$key}->{'elapsed'} = Time::HiRes::tv_interval( $MEMOIZE_CACHE{$func}->{'PROFILE'}->{$key}->{'start'} ); } } print STDOUT $MEMOIZE_CACHE{$func}->{'STDOUT'} if defined $MEMOIZE_CACHE{$func}->{'STDOUT'}; delete $MEMOIZE_CACHE{$func}->{'STDOUT'} if defined $MEMOIZE_CACHE{$func}->{'STDOUT'}; print STDERR $MEMOIZE_CACHE{$func}->{'STDERR'} if defined $MEMOIZE_CACHE{$func}->{'STDERR'}; delete $MEMOIZE_CACHE{$func}->{'STDERR'} if defined $MEMOIZE_CACHE{$func}->{'STDERR'}; return $MEMOIZE_CACHE{$func}->{$key}; }; no strict 'refs'; ## no critic (ProhibitNoStrict) no warnings 'redefine'; ## no critic (ProhibitNoWarnings) *{$func} = $cache_sub; } else { print STDERR "SSP DEBUG - Tried to memoize function that does not exist: $func\n"; } } return; } sub _memoize_parallel_populate_cache { ## no critic (RequireArgUnpacking) return if !load_module_with_fallbacks( 'needed_subs' => [qw{Purity Terse Indent Dump new}], 'modules' => [qw{Data::Dumper}], 'fail_warning' => 'SSP will take longer to run', ); return if defined $MEMOIZE_CACHE{'PRECACHE'} and $MEMOIZE_CACHE{'PRECACHE'}->{'disabled'}; print_start("Gathering some information, this may take a few moments...\n"); if ( defined $MEMOIZE_CACHE{'PROFILING'} ) { $MEMOIZE_CACHE{'PRECACHE'}->{'PROFILE'}->{'start'} = [ Time::HiRes::gettimeofday() ]; } for my $func (@_) { # Subs prefixed with _serialize_ can be used to serialize multiple memoized functions if ( exists $MEMOIZE_CACHE{$func} or ( index( $func, '_serialize_' ) == 0 and exists &{$func} ) ) { new_async_call( $func, \&$func ); } else { print STDERR "SSP DEBUG - Tried to populate memoize cache that does not exist: $func\n"; } } event_loop(); for my $func (@_) { if ( defined &$func ) { async_call_result($func); } } if ( defined $MEMOIZE_CACHE{'PROFILING'} ) { $MEMOIZE_CACHE{'PRECACHE'}->{'PROFILE'}->{'elapsed'} = Time::HiRes::tv_interval( $MEMOIZE_CACHE{'PRECACHE'}->{'PROFILE'}->{'start'} ); } } sub _merge_into_memoize_cache { my ( $new_ref, $job_name, $stdout_sref, $stderr_sref ) = @_; for my $key ( keys %{$new_ref} ) { next unless scalar keys %{ $new_ref->{$key} }; $MEMOIZE_CACHE{$key} = $new_ref->{$key}; } $MEMOIZE_CACHE{$job_name}->{'PRECACHED'} = 1; $MEMOIZE_CACHE{$job_name}->{'STDOUT'} = $$stdout_sref if defined $$stdout_sref; $MEMOIZE_CACHE{$job_name}->{'STDERR'} = $$stderr_sref if defined $$stderr_sref; } { # Async jobs my $fds; my %jobs; my %results; sub new_async_call { my ( $job_name, $coderef, @args ) = @_; defined $fds or $fds = ''; # Trying to use more than one pipe here can result in deadlocks # If we eventually do non-blocking reads and writes then more than one pipe could be used pipe my ( $dumper_r, $dumper_w ); my $pid = fork; die "Unable to fork: $!" unless defined $pid; if ( $pid == 0 ) { close $dumper_r; my $child_stdout; my $child_stderr; close STDOUT; close STDERR; open STDIN, '<', '/dev/null'; open STDOUT, '>', \$child_stdout; open STDERR, '>', \$child_stderr; $coderef->(@args); my $dumper = Data::Dumper->new( [ \%MEMOIZE_CACHE, $child_stdout, $child_stderr ], [qw(*child_memoize_cache child_stdout child_stderr)] ); print $dumper_w $dumper->Purity(1)->Terse(0)->Indent(0)->Dump; exit; ## no critic (NoExitsFromSubroutines) } close $dumper_w; $jobs{$job_name} = { pipes => { dumper => { buf => '', fh => $dumper_r, }, }, pid => $pid, }; vec( $fds, fileno($dumper_r), 1 ) = 1; } sub event_loop { while (%jobs) { last unless select my $ready = $fds, undef, undef, undef; foreach my $job_name ( keys %jobs ) { foreach my $pipe ( keys %{ $jobs{$job_name}{pipes} } ) { my $buffer = \$jobs{$job_name}{pipes}{$pipe}{buf}; my $fh = $jobs{$job_name}{pipes}{$pipe}{fh}; my $fileno = fileno($fh); if ( vec( $ready, $fileno, 1 ) ) { if ( read $fh, my $buf, 4096 ) { ${$buffer} .= $buf; } else { $results{$job_name}{$pipe} = $buffer; vec( $fds, $fileno, 1 ) = 0; close $fh; delete $jobs{$job_name}{pipes}{$pipe}; } } } delete $jobs{$job_name} unless scalar keys %{ $jobs{$job_name}{pipes} }; } } } sub async_call_result { my ($job_name) = @_; my %child_memoize_cache; my $child_stdout; my $child_stderr; defined $results{$job_name}{dumper} && defined ${ $results{$job_name}{dumper} } && eval ${ $results{$job_name}{dumper} }; ## no critic (StringyEval) _merge_into_memoize_cache( \%child_memoize_cache, $job_name, \$child_stdout, \$child_stderr ); } } sub get_kernel_pid_max { my $sysctl = ( split( /\s+/, timed_run( 0, 'sysctl', 'kernel.pid_max' ) ) )[2]; if ( $sysctl <= 32768 ) { return "kernel.pid_max may be too low: " . YELLOW $sysctl . CYAN " - LiteSpeed might SIGKILL MariaDB/MySQL. See:UPS-90"; } } sub check_hostname_resolution { my $lcHost = shift; my $external_ip_address = get_external_ip(); return 1 unless ($external_ip_address); chomp( my $hostname_ip = timed_run( 4, 'dig', "$lcHost", '+short' ) ); return 1 if ( $hostname_ip eq $external_ip_address ); return 0; } sub check_if_hostname_has_multiple_a_records { my $lcHost = shift; my @hostname_ips = timed_run( 4, 'dig', "$lcHost", '+short' ); my $cnt = @hostname_ips; my $cookieipvalidation_strict = 0; if ( -s '/var/cpanel/cpanel.config' ) { my %CPCONF; %CPCONF = SSP::Util::get_cpanel_conf(); if ( defined $CPCONF{'cookieipvalidation'} && $CPCONF{'cookieipvalidation'} eq 'strict' ) { $cookieipvalidation_strict = 1; } } return 1 if ( $cnt > 1 && $cookieipvalidation_strict ); return 0 if ( $cnt == 1 ); } sub _init_run_state { return if defined $RUN_STATE; $RUN_STATE = { STATE => 0, type => { cpanel => 1 << 0, solo => 1 << 1, dnsonly => 1 << 2, ea3 => 1 << 3, ea4 => 1 << 4, cloudlinux => 1 << 5, kernelcare => 1 << 6, amazon => 1 << 7, cptech => 1 << 8, wptk => 1 << 9, jetbackup => 1 << 10, almalinux => 1 << 11, ubuntu => 1 << 12, centos => 1 << 13, unsupported_os => 1 << 14, redhat => 1 << 15, mailnode => 1 << 16, dnsnode => 1 << 17, rocky => 1 << 18, wp2 => 1 << 19, databasenode => 1 << 20, }, var => { cpanel_numeric_version => "UNKNOWN", cpanel_original_version => "UNKNOWN", os_release => "UNKNOWN", os_version => "UNKNOWN", os_ises => 0, }, }; return 1; } sub _set_run_type { my ($type) = @_; print STDERR "SSP DEBUG - Runtime type ${type} doesn't exist\n" and return unless exists $RUN_STATE->{type}->{$type}; return $RUN_STATE->{STATE} |= $RUN_STATE->{type}->{$type}; } sub _set_run_var { my ( $key, $value ) = @_; print STDERR "SSP DEBUG - Runtime var ${key} doesn't exist\n" and return unless exists $RUN_STATE->{var}->{$key}; return $RUN_STATE->{var}->{$key} = $value; } sub _get_run_var { my ($key) = @_; print STDERR "SSP DEBUG - Runtime var ${key} doesn't exist\n" and return unless exists $RUN_STATE->{var}->{$key}; return $RUN_STATE->{var}->{$key}; } sub _simulate_run_state { ## no critic (RequireArgUnpacking) my ($value) = @_; _init_run_state(); _set_run_type($value); } sub _simulate_run_var { ## no critic (RequireArgUnpacking) my ( $key, $value ) = @_; _init_run_state(); _set_run_var( $key, $value ); } sub check_for_non_default_permissions { my $timeout = $OPT_TIMEOUT ? $OPT_TIMEOUT : 10; # This only applies to the recursive loop. my $hostinfo = get_hostinfo_href(); # Example: '/path' => { mode => ['0755','0555',...], user => 'root', group => 'root', perms_help => 'Additional info if mode/user/group incorrect', attr_check => [ 'IMMUTABLE' ], attr_recursive => 1, attr_help => 'Additional info if immutable/append-only/etc', symlink => '/ path', symlink_no_absolute => 1, check_missing => 1 }, # Attributes are always checked, mode is only checked if specified. # User is always checked if mode is specified, which defaults to 'root'. # A '*' can be used to specify any user or group is allowed. # Only symlink ownership can be verified, not its mode. # attr_recursive only works on directories, default is 0 (do not recurse). # attr_check is optional, default is to check all of IMMUTABLE, APPEND-ONLY, UNDELETABLE. # symlink_no_absolute defines whether the absolute target path of a symlink will be computed before comparing. Default behavior is to resolve the absolute target path. Enabling this option allows you to compare a symlink at face-value. # check_missing causes a missing object to be reported # tidyoff my %check = ( '/' => { mode => [ '0755', '0555' ], perms_help => '.ftpquota issues?', attr_help => 'This can break EA.' }, '/bin/bash' => { mode => ['0755'] }, '/bin/df' => { mode => ['0755'] }, '/bin/gtar' => { symlink => 'tar', symlink_no_absolute => 1, perms_help => 'May prevent creating backups via cPanel UI if users can not use this.' }, '/bin/gzip' => { mode => ['755'], perms_help => 'May prevent creating backups via cPanel UI if users can not use this.' }, '/bin/ln' => { mode => [ '0755', '0555' ] }, '/bin/rm' => { mode => [ '0755', '0555' ], perms_help => 'File Manager unable to delete files? This may be why.' }, '/bin/rpm' => { mode => ['0755'], perms_help => 'May cause SSL redirection to be greyed out and can_ssl_redirect uapi call to fail.' }, '/bin/tar' => { mode => ['755'], perms_help => 'May prevent creating backups via cPanel UI if users can not use this.' }, '/dev' => { mode => ['0755'], perms_help => 'Breaks many things if non-root users can\'t access this.' }, '/dev/null' => { mode => ['0666'], perms_help => 'Breaks many things if non-root users can\'t write to this.' }, '/dev/random' => { mode => [ '0666', '0664', '0644', '0444' ], perms_help => 'Breaks many things if non-root users can\'t read this.' }, '/dev/stderr' => { symlink => '/proc/self/fd/2', symlink_no_absolute => 1, check_missing => 1 }, '/dev/stdin' => { symlink => '/proc/self/fd/0', symlink_no_absolute => 1, check_missing => 1 }, '/dev/stdout' => { symlink => '/proc/self/fd/1', symlink_no_absolute => 1, check_missing => 1 }, '/dev/urandom' => { mode => [ '0666', '0664', '0644', '0444' ], perms_help => 'Breaks many things if non-root users can\'t read this.' }, '/etc' => { mode => ['0755'] }, '/etc/aliases' => { mode => ['0644'] }, '/etc/fstab' => { mode => ['0644'], check_missing => 1, perms_help => 'Missing fstab can break /scripts/fixquotas (CPANEL-6082), and bad perms can break cPanel UI (CPANEL-11201).' }, '/etc/group' => { mode => ['0644'] }, '/etc/hosts' => { mode => ['0644'] }, '/etc/localaliases' => { mode => ['0644'] }, '/etc/passwd' => { mode => ['0644'] }, '/etc/shadow' => { mode => [ '0600', '0400', '0200', '0640', '0000' ] }, '/etc/ssh/sshd_config' => { mode => ['0600', '0644'], attr_check => [ 'IMMUTABLE' ] }, '/root/.bash_history' => { mode => ['0600'], attr_check => [ 'IMMUTABLE', 'APPEND-ONLY' ], check_missing => 1 }, '/opt' => { mode => ['0755'] }, '/proc' => { mode => ['0555'], perms_help => 'Breaks quotas if we cannot read /proc/mounts, prevents setrtlimit(), is the server LXC/proxmox? See TECH-775' }, '/root/.ssh/authorized_keys' => { mode => ['0600'], attr_check => [ 'IMMUTABLE' ] }, '/sbin/ifconfig' => { mode => [ '0755', '0555' ] }, '/tmp' => { mode => ['1777'] }, '/usr' => { mode => ['0755'] }, '/usr/bin' => { mode => [ '0755', '0711', '0555' ] }, '/usr/bin/crontab' => { mode => [ '6755', '4755', '4711', '4555', '4511' ] }, '/usr/bin/passwd' => { mode => [ '6755', '4755', '4711', '4555', '4511' ] }, '/usr/local' => { mode => ['0755'] }, '/usr/bin/rpm' => { mode => ['0755'], perms_help => 'May cause SSL redirection to be greyed out and can_ssl_redirect uapi call to fail.'}, '/usr/local/bin' => { mode => [ '0755', '0711', '0555' ] }, '/usr/local/sbin' => { mode => [ '0755', '0711', '0555' ] }, '/usr/sbin' => { mode => [ '0755', '0711', '0555' ] }, '/usr/sbin/exim' => { mode => ['4755'] }, '/usr/share' => { mode => ['0755'] }, '/usr/share/zoneinfo' => { mode => ['0755'] }, '/var' => { mode => ['0755'] }, '/var/lib' => { mode => ['0755'] }, '/var/lib/mysql' => { mode => ['0751'], user => 'mysql', group => 'mysql' }, '/var/lib/mysql/mysql.sock' => { mode => ['0777'], user => 'mysql', group => 'mysql' }, '/var/log' => { mode => [ '0755', '0751', '0711', '0775' ], perms_help => 'If non-root users cannot write to log files it can cause service failure' }, '/bin/cagefs_enter.proxied' => { mode => ['4755'], perms_help => 'Issues with cPanel Terminal emulator on CloudLinux, see UPS-264' }, ); if ( SSP::Util::os_version_is(qw( >= 8 ))) { # SCREEN permissions if ( SSP::Util::i_am( 'ubuntu' )) { $check{'/usr/bin/screen'} = { mode => ['0755'], group => 'root', perms_help => 'Screen doesn\'t work? Try reinstalling "apt-get install --reinstall screen" to fix.' }; } else { $check{'/usr/bin/screen'} = { mode => ['2755'], group => 'screen', perms_help => 'Screen doesn\'t work? Run "rpm --setugids screen && rpm --setperms screen" to fix.' }; } # /etc/nsswitch.conf and/or /etc/authselect/nsswitch.conf if ( SSP::Util::i_am_one_of( 'almalinux', 'rocky' ) && ( -l '/etc/nsswitch.conf' || ! -e _ ) ) { $check{'/etc/nsswitch.conf'} = { symlink => '/etc/authselect/nsswitch.conf', check_missing => 1 }; $check{'/etc/authselect/nsswitch.conf'} = { mode => ['0644'], check_missing => 1 }; } else { $check{'/etc/nsswitch.conf'} = { mode => ['0644'], check_missing => 1 }; } if ( $hostinfo->{'environment'} =~ m{vzcontainer|virtuozzo|unknown-envtype} ) { $check{'/etc/nsswitch.conf'} = { mode => ['0644'], check_missing => 1 }; } } if ( SSP::Util::i_am('centos') and SSP::Util::os_version_is(qw( < 8))) { $check{'/dev/log'} = { mode => ['0666'], perms_help => 'CSF RESTRICT_SYSLOG can change this. Non-root users may not be able to log to syslog, including user cron jobs to /var/log/cron.' }; } else { $check{'/dev/log'} = { symlink => '/run/systemd/journal/dev-log', check_missing => 1 } } if ( i_am_one_of( 'cpanel', 'dnsonly' ) ) { %check = ( %check, '/scripts' => { symlink => '/usr/local/cpanel/scripts', check_missing => 1 }, '/usr/local/cpanel' => { mode => ['0711'] }, $CPANEL_LICENSE_FILE => { mode => ['0644'] }, '/usr/local/cpanel/cpsanitycheck.so' => { mode => ['0754'] }, '/usr/local/cpanel/logs/cphulkd.log' => { attr_check => [ 'IMMUTABLE', 'UNDELETABLE' ] }, '/usr/local/cpanel/logs/cphulkd_errors.log' => { attr_check => [ 'IMMUTABLE', 'UNDELETABLE' ] }, '/usr/local/cpanel/logs/dnsadmin_log' => { attr_check => [ 'IMMUTABLE', 'UNDELETABLE' ] }, '/usr/local/cpanel/logs/error_log' => { attr_check => [ 'IMMUTABLE', 'UNDELETABLE' ] }, '/usr/local/cpanel/logs/queueprocd.log' => { attr_check => [ 'IMMUTABLE', 'UNDELETABLE' ] }, '/usr/local/cpanel/logs/tailwatchd_log' => { attr_check => [ 'IMMUTABLE', 'UNDELETABLE' ] }, '/var/cpanel/analytics/system_id' => { attr_check => [ 'APPEND-ONLY', 'UNDELETABLE' ] }, '/var/cpanel/config' => { mode => ['0755'] }, $CPANEL_CONFIG_FILE => { mode => ['0644'] }, '/var/cpanel/datastore' => { mode => [ '0755', '0711' ], perms_help => 'Users must be able to read some of the datastore contents for cPanel UI usage stats.' }, ); } if ( i_am('cpanel') ) { %check = ( %check, '/bin/passwd' => { symlink => '/usr/local/cpanel/bin/jail_safe_passwd' }, '/etc/backupmxhosts' => { mode => ['0640'], group => 'mail' }, '/etc/cpbackup.conf' => { mode => ['0644'] }, '/etc/cpupdate.conf' => { mode => ['0644'], attr_check => [ 'IMMUTABLE' ], attr_help => 'Please tag the ticket to case TECH-853' }, '/etc/dbowners' => { mode => ['0640'], group => 'mail' }, '/etc/demodomains' => { mode => ['0640'], group => 'mail' }, '/etc/demouids' => { mode => ['0640'], group => 'mail' }, '/etc/demousers' => { mode => ['0640'], group => 'mail' }, '/etc/domainusers' => { mode => ['0640'], group => 'mail' }, '/etc/email_send_limits' => { mode => ['0640'], group => 'mail' }, '/etc/exim.conf' => { mode => ['0644'] }, '/etc/eximmailtrap' => { mode => ['0644'] }, '/etc/eximrejects' => { mode => ['0644'] }, '/etc/global_spamassassin_enable' => { mode => ['0644'] }, '/etc/greylist_common_mail_providers' => { mode => ['0644'] }, '/etc/greylist_trusted_netblocks' => { mode => ['0640'], group => 'mail' }, '/etc/localdomains' => { mode => ['0640'], group => 'mail', perms_help => 'Failing to properly create an email forwarder?' }, '/etc/mailbox_formats' => { mode => ['0640'], group => 'mail' }, '/etc/mailhelo' => { mode => ['0640'], group => 'mail' }, '/etc/mailips' => { mode => ['0640'], group => 'mail' }, '/etc/neighbor_netblocks' => { mode => [ '0640', '0644' ], group => '*' }, # 644 root:root before first account created, 640 root:mail after. '/etc/outgoing_mail_hold_users' => { mode => ['0640'], group => 'mail' }, '/etc/outgoing_mail_suspended_users' => { mode => ['0640'], group => 'mail' }, '/etc/recent_authed_mail_ips' => { mode => ['0644'] }, '/etc/recent_authed_mail_ips_users' => { mode => ['0644'] }, '/etc/recent_recipient_mail_server_ips' => { mode => ['0640'], group => 'mail' }, '/etc/remotedomains' => { mode => ['0644'], group => 'mail' }, '/etc/secondarymx' => { mode => ['0640'], group => 'mail' }, '/etc/senderverifybypasshosts' => { mode => ['0640'], group => 'mail' }, '/etc/skipsmtpcheckhosts' => { mode => ['0640'], group => 'mail' }, '/etc/spammeripblocks' => { mode => ['0640'], group => 'mail' }, '/etc/spammers' => { mode => ['0644'] }, '/etc/stats.conf' => { mode => ['0644'] }, '/etc/trueuserdomains' => { mode => ['0640'], group => 'mail' }, '/etc/trusted_mail_users' => { mode => ['0640'], group => 'mail' }, '/etc/trustedmailhosts' => { mode => ['0640'], group => 'mail' }, '/etc/userdomains' => { mode => ['0640'], group => 'mail' }, '/etc/valiases' => { mode => [ '0755', '0711', '0751' ] }, '/etc/vdomainaliases' => { mode => ['0711', '0751'] }, '/etc/vfilters' => { mode => [ '0755', '0711', '0751' ] }, '/etc/webspam' => { mode => ['0644'] }, '/home' => { mode => [ '0755', '0711' ] }, '/home1' => { mode => [ '0755', '0711' ] }, '/home2' => { mode => [ '0755', '0711' ] }, '/home3' => { mode => [ '0755', '0711' ] }, '/home4' => { mode => [ '0755', '0711' ] }, '/home5' => { mode => [ '0755', '0711' ] }, '/root/cpanel3-skel' => { mode => ['0755'] }, '/usr/local/apache' => { mode => ['0755'], attr_recursive => 1 }, '/usr/local/apache/conf' => { mode => ['0755'] }, '/usr/local/cpanel/base/3rdparty/phpMyAdmin/index.php' => { mode => ['0644'] }, '/usr/local/cpanel/base/3rdparty/phpPgAdmin/index.php' => { mode => ['0644'] }, '/usr/local/cpanel/base/3rdparty/roundcube/index.php' => { mode => ['0644'] }, '/usr/local/cpanel/base/3rdparty/roundcube/plugins/cpanellogin/cpanellogin.php' => { mode => ['0644'] }, '/usr/local/cpanel/base/3rdparty/squirrelmail/index.php' => { mode => ['0644'] }, '/usr/local/cpanel/base/horde/index.php' => { mode => ['0644'] }, '/usr/local/cpanel/bin/cpwrap' => { mode => ['0755'] }, '/usr/local/cpanel/bin/sendmail' => { mode => ['0755'] }, '/usr/local/cpanel/logs/cpdavd_error_log' => { attr_check => [ 'IMMUTABLE', 'UNDELETABLE' ] }, '/usr/local/cpanel/logs/cpdavd_error_log' => { attr_check => [ 'IMMUTABLE', 'UNDELETABLE' ] }, '/usr/local/cpanel/logs/spamd_error_log' => { attr_check => [ 'IMMUTABLE', 'UNDELETABLE' ] }, '/usr/local/cpanel/logs/stats_log' => { attr_check => [ 'IMMUTABLE', 'UNDELETABLE' ] }, '/usr/local/cpanel/php/cpanel.php' => { mode => ['0644'] }, '/var/cpanel' => { mode => [ '0755', '0711', '0555' ], attr_recursive => 1 }, '/var/cpanel/backups' => { mode => [ '0755', '0750' ] }, '/var/cpanel/backups/config' => { mode => ['0644'] }, '/var/cpanel/bandwidth.cache' => { mode => ['0711'] }, '/var/cpanel/config/apache' => { mode => ['0755'] }, '/var/cpanel/config/apache/port' => { mode => ['0644'] }, '/var/cpanel/config/email' => { mode => ['0755'] }, '/var/cpanel/config/email/query_apache_for_nobody_senders' => { mode => ['0644'] }, '/var/cpanel/config/email/trust_x_php_script' => { mode => ['0644'] }, '/var/cpanel/domain_keys_private' => { mode => ['0640'], group => 'wheel' }, '/var/cpanel/email_send_limits' => { mode => [ '0750', '0751' ] }, # 0750 before, 0751 after first account '/var/cpanel/features' => { mode => ['0755'] }, '/var/cpanel/locale' => { mode => ['0755'] }, '/var/cpanel/locale/themes' => { mode => ['0755'] }, '/var/cpanel/resellers' => { mode => ['0644'] }, '/var/cpanel/userhomes' => { mode => [ '0755', '0711' ] }, '/var/cpanel/userhomes/cpanelroundcube' => { mode => ['0711'], user => 'cpanelroundcube', group => 'cpanelroundcube' }, '/var/cpanel/users' => { mode => ['0711'] }, $CPANEL_CONFIG_FILE => { mode => ['0644'], user => 'root', group => '*', perms_help => 'Ensure this is readable by all users or it can cause cPanel UI issues.' }, '/var/log/exim_mainlog' => { mode => ['0640'], user => 'mailnull', group => 'mail' }, '/var/log/exim_paniclog' => { mode => ['0640'], user => 'mailnull', group => 'mail' }, '/var/log/exim_rejectlog' => { mode => ['0640'], user => 'mailnull', group => 'mail' }, ); } # tidyon ## If EA4 is running we need to redefine some of the paths from %check as these paths have turned into symlinks. ## We also conditionally need to add the new paths that EA4 introduces. if ( i_am('ea4') ) { $check{'/etc'} = { mode => ['0755'], perms_help => 'Users need execute permission for this directory to use cPanel MultiPHP. See CPANEL-905.' }; $check{'/etc/apache2'} = { mode => ['0755'] }; $check{'/etc/apache2/conf'} = { mode => ['0755'] }; $check{'/etc/apache2/conf.d'} = { mode => ['0755'] }; $check{'/etc/apache2/conf.modules.d'} = { mode => ['0755'] }; $check{'/etc/apache2/logs'} = { symlink => '/var/log/apache2', check_missing => 1 }; $check{'/etc/apache2/logs/domlogs'} = { mode => [ '0711', '0755' ], perms_help => 'If users can\'t access logs, stats won\'t process.' }; $check{'/etc/apache2/logs/error_log'} = { mode => ['0644'], user => '*', perms_help => 'If users can\'t read error_log, cPanel Errors (Last 300) won\'t work.' }; $check{'/etc/apache2/logs/suexec_log'} = { mode => ['0644'], user => '*', perms_help => 'If users can\'t read suexec_log, cPanel Errors (Last 300) won\'t work.' }; $check{'/etc/apache2/run'} = { symlink => '../../var/run/apache2', symlink_no_absolute => 1, check_missing => 1 }; $check{'/etc/cpanel'} = { mode => [ '0755', '0751', '0711' ], perms_help => 'Users need execute permission for this directory to use cPanel MultiPHP. See CPANEL-905.' }; $check{'/etc/cpanel/ea4'} = { mode => ['0755'], perms_help => 'Users need read/execute permission for this directory to use cPanel MultiPHP. See CPANEL-905.' }; $check{'/usr/local/apache/bin/apachectl'} = { symlink => '/usr/sbin/apachectl', check_missing => 1 }; $check{'/usr/local/apache/bin/httpd'} = { symlink => '/usr/sbin/httpd', check_missing => 1 }; $check{'/usr/local/apache/bin/suexec'} = { symlink => '/usr/sbin/suexec', check_missing => 1 }; $check{'/usr/local/apache/conf/httpd.conf'} = { symlink => '/etc/apache2/conf/httpd.conf', check_missing => 1 }; $check{'/usr/local/apache/conf/includes'} = { symlink => '/etc/apache2/conf.d/includes', check_missing => 1 }; $check{'/usr/local/apache/conf/mime.types'} = { symlink => '/etc/apache2/conf/mime.types', check_missing => 1 }; $check{'/usr/local/apache/conf/php.conf'} = { symlink => '/etc/apache2/conf.d/php.conf', check_missing => 1 }; $check{'/usr/local/apache/domlogs'} = { symlink => '/etc/apache2/logs/domlogs', check_missing => 1 }; $check{'/usr/local/apache/logs'} = { symlink => '/etc/apache2/logs', check_missing => 1 }; $check{'/usr/local/apache/modules'} = { symlink => '/etc/apache2/modules', check_missing => 1 }; $check{'/usr/sbin/httpd'} = { mode => ['0755'] }; $check{'/var/log/apache2'} = { mode => ['0711'] }; $check{'/var/www/html'} = { mode => ['0755'] }; if ( defined( $hostinfo->{'hardware'} ) && $hostinfo->{'hardware'} eq 'i386' ) { $check{'/etc/apache2/modules'} = { symlink => '/usr/lib/apache2/modules', check_missing => 1 }; } else { $check{'/etc/apache2/modules'} = { symlink => '/usr/lib64/apache2/modules', check_missing => 1 }; } } if ( -l '/var/tmp' ) { $check{'/var/tmp'} = { symlink => '/tmp' }; } else { $check{'/var/tmp'} = { mode => ['1777'] }; } # Add defined homedir to the checklist my $wwwacctconf = '/etc/wwwacct.conf'; if ( open my $wwwacctconf_fh, '<', $wwwacctconf ) { while (<$wwwacctconf_fh>) { if (m{ ^ HOMEDIR \s ([/\-_A-Za-z0-9]+) $ }x) { $check{$1} = { mode => [ '0755', '0711' ] }; last; } } close $wwwacctconf_fh; } if ( os_version_is(qw( < 7 )) or i_am('amazon') ) { $check{'/bin'} = { mode => [ '0755', '0711', '0555' ] }; $check{'/sbin'} = { mode => [ '0755', '0711', '0555' ] }; $check{'/var/run'} = { mode => ['0755'], perms_help => 'Exim auth can fail if this is too restrictive.' }; if ( i_am('cpanel') ) { $check{'/bin/crontab'} = { symlink => '/usr/local/cpanel/bin/jail_safe_crontab' }; $check{'/bin/passwd'} = { symlink => '/usr/local/cpanel/bin/jail_safe_passwd' }; } } if ( os_version_is(qw( >= 7 )) and not i_am('amazon') ) { # https://access.redhat.com/documentation/en-US/ Red_Hat_Enterprise_Linux/7/html/Migration_Planning_Guide/sect-Red_Hat_Enterprise_Linux-Migration_Planning_Guide-File_System_Layout.html $check{'/bin'} = { symlink => '/usr/bin', check_missing => 1 }; $check{'/lib'} = { symlink => '/usr/lib', check_missing => 1 }; $check{'/lib64'} = { symlink => '/usr/lib64', check_missing => 1 }; $check{'/run'} = { mode => ['0755'], perms_help => 'Exim auth can fail if this is too restrictive.' }; $check{'/sbin'} = { symlink => '/usr/sbin', check_missing => 1 }; $check{'/var/lock'} = { symlink => '/run/lock', check_missing => 1 }; $check{'/var/run'} = { symlink => '/run', check_missing => 1 }; if ( i_am('cpanel') ) { $check{'/usr/local/bin/crontab'} = { symlink => '/usr/local/cpanel/bin/jail_safe_crontab', check_missing => 1 }; $check{'/usr/local/bin/passwd'} = { symlink => '/usr/local/cpanel/bin/jail_safe_passwd', check_missing => 1 }; } } if ( defined $CPCONF{'skipawstats'} && $CPCONF{'skipawstats'} == 0 ) { $check{'/usr/local/cpanel/3rdparty/bin/awstats.pl'} = { mode => ['0755'] }; } if ( i_am('cloudlinux') ) { if ( os_version_is(qw( >= 7 )) && os_version_is(qw( < 8 )) ) { $check{'/usr/bin/python'} = { symlink => '/usr/bin/python2', check_missing => 1, perms_help => 'If the Python binary is not executable by non-root users it can break CloudLinux functions in cPanel UI' }; $check{'/usr/bin/python2'} = { symlink => '/usr/bin/python2.7', check_missing => 1, perms_help => 'If the Python binary is not executable by non-root users it can break CloudLinux functions in cPanel UI' }; $check{'/usr/bin/python2.7'} = { mode => ['0755'], perms_help => 'If the Python binary is not executable by non-root users it can break CloudLinux functions in cPanel UI' }; } if ( os_version_is(qw( >= 8 )) && os_version_is(qw( < 9 )) ) { # python2 # The symlink for /usr/bin/python is completely missing. No idea why. $check{'/usr/bin/python2'} = { symlink => '/usr/bin/python2.7', check_missing => 1, perms_help => 'If the Python binary is not executable by non-root users it can break CloudLinux functions in cPanel UI' }; $check{'/usr/bin/python2.7'} = { mode => ['0755'], perms_help => 'If the Python binary is not executable by non-root users it can break CloudLinux functions in cPanel UI' }; # python3 $check{'/usr/bin/python3'} = { symlink => '/etc/alternatives/python3', check_missing => 1, perms_help => 'If the Python binary is not executable by non-root users it can break CloudLinux functions in cPanel UI' }; $check{'/etc/alternatives/python3'} = { symlink => '/usr/bin/python3.6', check_missing => 1, perms_help => 'If the Python binary is not executable by non-root users it can break CloudLinux functions in cPanel UI' }; $check{'/usr/bin/python3.6'} = { symlink => '/usr/libexec/platform-python3.6', check_missing => 1, perms_help => 'If the Python binary is not executable by non-root users it can break CloudLinux functions in cPanel UI' }; $check{'/usr/libexec/platform-python3.6'} = { mode => ['0755'], perms_help => 'If the Python binary is not executable by non-root users it can break CloudLinux functions in cPanel UI' }; } if ( os_version_is(qw( >= 9 )) ) { # This one has a symlink for /usr/bin/python to /usr/bin/python3 There is no python2 at all. $check{'/usr/bin/python'} = { symlink => '/usr/bin/python3', check_missing => 1, perms_help => 'If the Python binary is not executable by non-root users it can break CloudLinux functions in cPanel UI' }; $check{'/usr/bin/python3'} = { symlink => '/usr/bin/python3.9', check_missing => 1, perms_help => 'If the Python binary is not executable by non-root users it can break CloudLinux functions in cPanel UI' }; $check{'/usr/bin/python3.9'} = { mode => ['0755'], perms_help => 'If the Python binary is not executable by non-root users it can break CloudLinux functions in cPanel UI' }; } } $check{'/etc/exim.conf.localopts'} = { mode => ['0644'], perms_help => 'If users can\'t read this file it can affect the Email Deliverability feature.' }; # Add configured backup path(s) to the checklist my $new_backup_conf = get_new_backup_conf_href(); my @backupdirs; push @backupdirs, $new_backup_conf->{BACKUPDIR} if defined $new_backup_conf; foreach my $backup_path (@backupdirs) { if ( defined $backup_path ) { my @backup_path_parts = split( '/', $backup_path ); # the first element will be an empty string. We don't want to check the permissions of /. shift @backup_path_parts; my $backup_path_parent; foreach my $part (@backup_path_parts) { $backup_path_parent .= '/' . $part; $check{$backup_path_parent} = { mode => [ '0755', '0751', '0711' ], perms_help => "Ensure that all backup directories are traversable (+x) by all users. See CPANEL-4336." }; } } } # Process the checklist for my $path ( sort keys %check ) { my ( $mode, $uid, $gid ) = ( lstat($path) )[ 2, 4, 5 ]; # _ can now be used in place of $path for -d, -e, -f, -l, etc... if ( exists $check{$path}->{check_missing} ) { my $symlink = ''; my $perms_help = ''; if ( exists $check{$path}->{symlink} && !-l _ ) { $symlink = exists $check{$path}->{symlink} ? ' symlink to ' . $check{$path}->{symlink} : ''; } if ( $symlink || !-e _ ) { $perms_help = exists $check{$path}->{perms_help} ? ' - ' . $check{$path}->{perms_help} : ''; if ( -d _ ) { $perms_help .= " - $path currently exists as a directory. An RPM that owns this path may fail to install. See EA-7912. "; } elsif ( -f _ ) { $perms_help .= " - $path currently exists as a file."; } elsif ( -e _ ) { $perms_help .= " - $path currently exists and is not a file or directory."; } print_warn('Missing: '); print_warning( $path . $symlink . $perms_help ); } } if ( -l _ ) { my $linktarget = readlink($path); if ( !exists $check{$path}{'symlink_no_absolute'} || $check{$path}{'symlink_no_absolute'} == 0 ) { if ( !( $linktarget =~ m{ \A / }x ) ) { # If a symlink has a relative prefix, try to prepend the base path and convert to absolute my @basepath = split( m{/}, $path ); $linktarget = abs_path( join( '/', @basepath[ 0 .. $#basepath - 1 ] ) . '/' . $linktarget ); } elsif ( $linktarget =~ m{ / .{1,2} / }x ) { # If a symlink is relative, convert to absolute $linktarget = defined( abs_path($linktarget) ) ? abs_path($linktarget) : $linktarget; } } if ( !exists $check{$path}{'symlink'} || $linktarget ne $check{$path}{'symlink'} ) { my $perms_help = exists $check{$path}->{perms_help} ? ' - ' . $check{$path}->{perms_help} : ''; print_warn('Non-default symlink: '); print_warning( '[ ' . $path . ' -> ' . $linktarget . ' ] ( default -> ' . ( exists $check{$path}{'symlink'} ? $check{$path}{'symlink'} : 'no symlink' ) . ' )' . $perms_help ); } } elsif ( -e _ ) { if ( exists $check{$path}->{mode} ) { $mode &= oct(7777); $mode = sprintf "%lo", $mode; my $user = getpwuid($uid); $user = $user || $uid; my $group = getgrgid($gid); $group = $group || $gid; my $checkuser = defined( $check{$path}->{user} ) ? $check{$path}->{user} : 'root'; my $checkgroup = defined( $check{$path}->{group} ) ? $check{$path}->{group} : '*'; my $perms_help = exists $check{$path}->{perms_help} ? ' - ' . $check{$path}->{perms_help} : ''; if ( !( $checkuser eq '*' || $user eq $checkuser ) || !( $checkgroup eq '*' || $group eq $checkgroup ) ) { print_warn('Non-default Perms: '); print_warning( $path . ' [owner ' . $user . ':' . $group . '] (default ' . $checkuser . ':' . $checkgroup . ')' . $perms_help ); } my $default = 0; for my $checkmode ( @{ $check{$path}->{mode} } ) { if ( $mode == $checkmode ) { $default = 1; } } if ( !$default == 1 ) { print_warn('Non-default Perms: '); print_warning( $path . ' [mode ' . sprintf( "%04d", $mode ) . '] (default ' . join( ' or ', @{ $check{$path}->{mode} } ) . ')' . $perms_help ); } } my @attr_check = exists $check{$path}->{attr_check} ? @{ $check{$path}->{attr_check} } : qw( APPEND-ONLY IMMUTABLE UNDELETABLE ); my $attr_help = exists $check{$path}->{attr_help} ? ' - ' . $check{$path}->{attr_help} : ''; my $is_recursive = exists $check{$path}->{attr_recursive} ? $check{$path}->{attr_recursive} : 0; my $recursive_report_limit = 15; my %recursive_report_count_by_path; my $test_ref = sub { my $linktarget; if ($is_recursive) { return if substr( $_, -10 ) eq "quota.user"; $linktarget = -l $_ ? readlink($_) : ""; return if exists $check{$_} && !exists $check{$_}->{attr_recursive}; # If we check a path directly then we don't need to check it in a recursive sweep. return if exists $check{$linktarget}; # Don't check a link target that we've explicitly checked. $recursive_report_count_by_path{$path} = 0 if !defined( $recursive_report_count_by_path{$path} ); return if $recursive_report_count_by_path{$path} >= $recursive_report_limit; } if ( ( -f $_ || -d _ ) && ( my %attr = get_attributes( $_, @attr_check ) ) ) { my $attributes = join( ' & ', keys(%attr) ); my $linktext = $linktarget ? " -> " . $linktarget : ""; print_warn('Non-default Perms: '); print_warning( $_ . $linktext . ' [' . $attributes . '] ' . $attr_help ); $recursive_report_count_by_path{$path}++; if ( $recursive_report_count_by_path{$path} >= $recursive_report_limit ) { print_warn('Non-default Perms: '); print_warning( 'recursive reporting limit reached for ' . $path . ' -- there may be more files like this!' ); } } }; if ($is_recursive) { eval { local $SIG{'ALRM'} = sub { print_warn('Non-default Perms: '); print_warning( 'recursive check of ' . $path . ' timed out after ' . $timeout . ' seconds.' ); die; }; alarm $timeout; find( { wanted => $test_ref, no_chdir => 1 }, $path ); alarm 0; }; } else { local $_ = $path; &$test_ref(); } } } } sub check_for_bash_secadv_20140924 { chomp( my $bash_output = timed_run( 0, 'env x=\'() { :;}; echo vulnerable\' bash -c ""' ) ); return if !( $bash_output =~ m{ vulnerable }xms ); print_critical(); print_crit('Installed \'bash\' shell is vulnerable to remote code injection. Verify by running the following at a shell prompt, it should return "vulnerable":'); print_critical(); print_critical(' env x=\'() { :;}; echo vulnerable\' bash -c ""'); print_critical('Send customer this premade: "SECURITY - Bash advisory 2014-09-24 - Discovery"'); print_critical(); } sub get_os_info { ## no critic qw(Subroutines::ProhibitExcessComplexity) my ( $distro, $ver, $ver_id ); if ( !-s "/etc/os-release" && -e "/etc/redhat-release" ) { # cloudlinux 6 open( my $cr_fh, "<", "/etc/redhat-release" ) or die "Could not open \"/etc/redhat-release\" for reading: $!\n"; ## no critic (InputOutput::RequireBriefOpen) my $line = <$cr_fh>; ($ver) = $line =~ m/(\d+(:?\.\d+))/; $ver .= ".0" if $ver =~ m/^\d+\.\d+$/; ($distro) = $line =~ m/^(\w+)/; $distro = lc $distro; } else { open( my $os_fh, "<", "/etc/os-release" ) or die "Could not open \"/etc/os-release\" for reading: $!\n"; my $line; # buffer while ( $line = <$os_fh> ) { if ( !$distro && substr( $line, 0, 3 ) eq "ID=" ) { $distro = _clean_value( $line => 3 ); } if ( !$ver_id && substr( $line, 0, 11 ) eq "VERSION_ID=" ) { $ver_id = _clean_value( $line => 11 ); } if ( !$ver && substr( $line, 0, 8 ) eq "VERSION=" ) { $ver = _clean_value( $line => 8 ); $ver = substr( $ver, 0, index( $ver, " " ) ); } last if $distro && $ver && $ver_id; } close $os_fh; } # We only want to do this on distros we know only do 2 version parts in /etc/os-release (/etc/redhat-release in CL6) # otherwise we may set it incorrectly when attempting to get any missing bits later in the code $ver .= ".0" if ( $distro eq "cloudlinux" || $distro eq "almalinux" || $distro eq "rhel" || $distro eq "rocky" ) && $ver =~ m/^\d+\.\d+$/; my ( $major, $minor, $build ) = split( /\./, $ver ); if ( !length $build || !length $minor ) { if ( $ver =~ m/(\Q$ver_id\E(?:\.[0-9]+)+) / ) { my $ver = $1; ( $major, $minor, $build ) = split( /\./, $ver ); } if ( !length $build || !length $minor ) { for my $release_file (qw(/etc/system-release /etc/redhat-release)) { if ( open my $r_fh, '<', $release_file ) { my $release = <$r_fh>; if ( $release =~ m/(\Q$ver_id\E(?:\.[0-9]+)+)/ ) { my $ver = $1; ( $major, $minor, $build ) = split( /\./, $ver ); last if length $build && length $minor; } close $r_fh; } } } } return ( $^O, $distro, $major, $minor, $build ); } sub _clean_value { my ( $str, $strip ) = @_; die "Internal _clean_value() called without a value (did you make changes to this code recently?)\n" if !defined $str; chomp $str; $str = substr( $str, $strip ); if ( substr( $str, 0, 1 ) eq '"' ) { $str = substr( $str, 1, length($str) ); $str = substr( $str, 0, length($str) - 1 ); } return $str; } sub _tail_log_file { my $logpath = shift; my @logdata; my @cpanel_ips = qw( 184.94.197.2 184.94.197.3 184.94.197.4 184.94.197.5 184.94.197.6 208.74.123.98 ); if ( open my $fh, "<", $logpath ) { while (<$fh>) { chomp($_); foreach my $cpanel_auth_ip (@cpanel_ips) { chomp($cpanel_auth_ip); if ( $_ =~ m/$cpanel_auth_ip/ ) { push( @logdata, $_ ); } } } close($fh); my $total_lines = ( scalar @logdata > 10 ) ? 10 : @logdata; my @reversed = reverse(@logdata); my $cnt = 0; while ( $cnt < $total_lines ) { print CYAN "\t\\_ $reversed[$cnt]\n"; $cnt++; } if ( $total_lines == 0 ) { print CYAN "\t\\_ No cPanel IP's found in $logpath.\n"; } } else { print RED "\t\\_ Error opening the $logpath file ($!)\n"; } } sub is_os_vulnerable { my $tcOSData = shift; my @tcOSData = split /\s+/, $tcOSData; my $os_vulnerable = 0; if ( $tcOSData eq 'ALL' ) { return 1; } foreach my $tcOSLine (@tcOSData) { chomp($tcOSLine); my ( $tcOSDist, $tcOSVer ) = ( split( /\-/, $tcOSLine ) ); chomp($tcOSDist); chomp($tcOSVer); my $op = '>='; my ( $os, $dist, $maj, $min, $bld ) = get_os_info(); my $os_version = $maj . "." . $min; if ( $dist eq $tcOSDist ) { if ( SSP::Util::version_compare( $os_version, $op, $tcOSVer ) ) { $os_vulnerable = 1; last; } } } return $os_vulnerable; } sub is_kernel { my $tcPkg = shift; if ( $tcPkg =~ m{kernel|linux-headers} ) { my $uname = SSP::Util::timed_run( 0, 'uname', '-r' ); chomp($uname); if ( SSP::Util::i_am('ubuntu') ) { $tcPkg = "linux-headers-$uname"; } else { $tcPkg = "kernel-$uname"; } $gl_is_kernel = 1; } return $tcPkg; } sub found_in_changelog { my $tcPkg = shift; my $tcCVE = shift; my $in_chglog = 0; my $in_chglog1 = 0; if ( SSP::Util::i_am("ubuntu") ) { if ( !-e "/usr/share/doc/$tcPkg/changelog.Debian.gz" ) { SSP::Util::print_crit("Changelog Missing: "); SSP::Util::print_critical("/usr/share/doc/$tcPkg/changelog.Debian.gz not found. CVE CHeck Failed!"); return 0; } $in_chglog1 = ( SSP::Util::timed_run( 0, 'zgrep', '-E', "$tcCVE", "/usr/share/doc/$tcPkg/changelog.Debian.gz" ) ) ? 1 : 0; $in_chglog = 1 unless ( $in_chglog1 == 0 ); } else { $in_chglog1 = SSP::Util::timed_run( 0, 'rpm', '-q', "$tcPkg", '--changelog' ); $in_chglog = ( grep { /$tcCVE/ } $in_chglog1 ) ? 1 : 0; } # If pkg is a kernel/linux-header and is not found in changelog - See if KernelCare is installed and check using --patch-info if ( $in_chglog == 0 && $gl_is_kernel == 1 ) { return $in_chglog unless ( -x '/usr/bin/kcarectl' ); my $patchinfo = SSP::Util::timed_run( 3, 'kcarectl', '--patch-info' ); my @patchinfo = split /\n/, $patchinfo; my $in_chglog = ( grep { /$tcCVE/ } @patchinfo ) ? 1 : 0; return $in_chglog; } return $in_chglog; } sub is_installed { my $tcPkg = shift; my $is_installed = 0; my $pkgversion = 0; if ( SSP::Util::i_am('ubuntu') ) { my $installed_package = SSP::Util::timed_run( 0, 'dpkg-query', '-W', '-f=${binary:Package}\n', $tcPkg ); if ($installed_package) { $is_installed = 1; } return $is_installed; } else { my $is_installed1 = SSP::Util::timed_run( 0, 'rpm', '-q', $tcPkg ); chomp($is_installed1); my $is_installed = !grep { /is not installed/ } $is_installed1; return $is_installed; } } sub get_pkg_version { my $tcPkg = shift; my $pkgversion; if ( SSP::Util::i_am('ubuntu') ) { $pkgversion = SSP::Util::timed_run( 0, 'dpkg-query', '-W', '-f=${Version}\n', "$tcPkg" ); } else { $pkgversion = SSP::Util::timed_run( 0, 'rpm', '-q', '--queryformat', '%{Version}-%{Release}', "$tcPkg" ); } if ( $gl_is_kernel == 0 ) { $pkgversion =~ s/$tcPkg//g; } chomp($pkgversion); if ( substr( $tcPkg, 0, 7 ) eq "cpanel-" ) { $pkgversion =~ s/-\d+.cp\d+.*//a; } else { $pkgversion =~ s/^\.\.//; $pkgversion =~ s/^\-\-//; $pkgversion =~ s/[a-zA-Z].*//g; $pkgversion =~ s/(\.x86_64|\.cpanel|\.cloudlinux|\.deb.*|\.noarch|ubuntu.*|\.cp\+d.*|\.el.*|\+.*|\-.*)//g; } return $pkgversion; } sub digit_to_alpha { my $tcPkgVer = shift; return unless ( $tcPkgVer =~ /(\d+)\.(\d+)\.(\d+)([a-z])([a-z]?)/ ); my $retPkgVer; my ( $maj, $min, $patch ) = ( $1, $2, $3 ); # If we map the alphas into a number and sum the values the version will be compatible with version_compare() # and save us a lot of trouble, i.e. h=8, m=13, and za=27 my %al2num = map { ( "a" .. "z" )[ $_ - 1 ] => $_ } ( 1 .. 26 ); my $sub = 0; if ($4) { $sub += $al2num{ lc($4) } } if ($5) { $sub += $al2num{ lc($5) } } $retPkgVer = join( '.', $maj, $min, $patch, $sub ); return $retPkgVer; } sub alpha_to_digit { my $tcPkgVer = shift; my @letters = ( "a" .. "z" ); my $retPkgVer; my ( $maj, $min, $patch, $sub ) = ( split( /\./, $tcPkgVer ) ); my $sub1 = $letters[ $sub - 1 ]; $retPkgVer = join( '.', $maj, $min, $patch, $sub1 ); } sub usercount { open( my $fh, '<', '/etc/trueuserdomains' ) or return 0; my $cnt = 0; while (<$fh>) { $cnt++; } close($fh); return $cnt; } 1; SSP_UTIL $fatpacked{"SSP/Warn.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'SSP_WARN'; #!/usr/bin/perl # SSP - System Status Probe (warn module) # Find and print useful troubleshooting info on cPanel servers =head1 COPYRIGHT This software is Copyright 2024 WebPros International, LLC. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THE SOFTWARE LICENSED HEREUNDER IS PROVIDED "AS IS" AND WEBPROS INTERNATIONAL, LLC D/B/A/ CPANEL (CPANEL) HEREBY DISCLAIMS ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, RELATING TO THE SOFTWARE, ITS THIRD PARTY COMPONENTS, AND ANY DATA ACCESSED THEREFROM, OR THE ACCURACY, TIMELINESS, COMPLETENESS, OR ADEQUACY OF THE SOFTWARE, ITS THIRD PARTY COMPONENTS, AND ANY DATA ACCESSED THEREFROM, INCLUDING THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. CPANEL DOES NOT WARRANT THAT THE SOFTWARE OR ITS THIRD PARTY COMPONENTS ARE ERROR-FREE OR WILL OPERATE WITHOUT INTERRUPTION. IF THE SOFTWARE, ITS THIRD PARTY COMPONENTS, OR ANY DATA ACCESSED THEREFROM IS DEFECTIVE, YOU ASSUME THE SOLE RESPONSIBILITY FOR THE ENTIRE COST OF ALL REPAIR OR INJURY OF ANY KIND, EVEN IF CPANEL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DEFECTS OR DAMAGES. NO ORAL OR WRITTEN INFORMATION OR ADVICE GIVEN BY CPANEL, ITS AFFILIATES, LICENSEES, DEALERS, SUB-LICENSORS, AGENTS OR EMPLOYEES SHALL CREATE A WARRANTY OR IN ANY WAY INCREASE THE SCOPE OF ANY WARRANTY. =cut package SSP::Warn; use 5.006; use strict; use warnings; use File::Find; use Time::Piece; use Time::Seconds; use Term::ANSIColor qw(:constants); $Term::ANSIColor::AUTORESET = 1; our $OPT_SKIP_NETWORKING; # Disable network calls our $OPT_TIMEOUT; # How long to wait for system commands to finish executing our $CRIT_BUFFER_HACKED; # Things that are the same but used many places our $CPANEL_LICENSE_FILE = '/usr/local/cpanel/cpanel.lisc'; our $CPANEL_VERSION_FILE = '/usr/local/cpanel/version'; our $CPANEL_CONFIG_FILE = '/var/cpanel/cpanel.config'; our $MYSQL_CONF_FILE = '/etc/my.cnf'; our $PURE_FTPD_CONF_FILE = '/etc/pure-ftpd.conf'; # Global variables initialized at application initialization our %CPCONF; # cpanel.config our $ORIGINAL_PATH; $ORIGINAL_PATH = $ENV{'PATH'}; our %SOCKET; # Dispatcher for optional Socket module usage our $RUN_STATE; our $HTTP_GET_HOST_CACHE; our %MEMOIZE_CACHE; our $YUM_OR_DNF; our $YUM_CONF; sub run { my $self = shift; SSP::Util::init(); if ( SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ) ) { %CPCONF = SSP::Util::get_cpanel_conf(); } $self->check_for_license_error(); $self->check_for_license_creds(); $self->check_var_cpanel_users(); $self->check_port_hash(); $self->check_runlevel(); $self->check_multiuser_target(); $self->check_queueprocd_for_infinite_loops(); $self->check_for_missing_root_cron(); $self->check_for_missing_usr_bin_crontab(); $self->check_if_upcp_is_running(); $self->check_valid_upcp(); $self->check_cpupdate_conf(); $self->check_interface_lo(); $self->check_mainip_file(); $self->check_for_port_25_blocks(); $self->check_cpanelconfig_filetype(); $self->check_cpanelsync_exclude_no_chmod(); $self->check_cpanelsync_exclude(); $self->check_entropy_avail(); $self->check_user_beancounters_failcnt(); $self->check_for_local_templates(); $self->check_dovecot_conf_for_wrap(); $self->check_for_missing_account_suspensions_conf(); $self->check_for_custom_apache_includes(); $self->check_for_tomcatoptions(); $self->check_for_sneaky_htaccess(); $self->check_ea4_paths_conf(); $self->check_ea4_missing_cl_repo(); $self->check_apache_modules(); $self->check_apache_niceness(); $self->check_other_niceness(); $self->check_if_splitlogs_running(); $self->check_cp_major_version_yum_dnf(); $self->check_perl_sanity(); SSP::Util::check_for_non_default_permissions(); $self->check_for_non_default_file_capabilities(); $self->check_for_non_default_sysctl(); $self->check_for_stale_lockfiles(); $self->check_root_suspended(); $self->check_limitsconf(); $self->check_disk_space(); $self->check_disk_inodes(); $self->check_mounts(); $self->check_for_hooks_in_scripts_directory(); $self->check_for_huge_logs(); $self->check_easy_skip_cpanelsync(); $self->check_pkgacct_override(); $self->check_for_yunsuo(); $self->check_for_gdm(); $self->check_for_criu(); $self->check_for_mcafee(); $self->check_for_nsiv(); $self->check_for_perl_env_var(); $self->check_for_redhat_firewall(); $self->verify_ca_certificates(); $self->check_for_unsupported_nat(); $self->check_for_oracle_linux(); $self->check_for_usr_local_cpanel_hooks(); $self->check_for_domain_forwarding(); $self->check_for_empty_apache_templates(); $self->check_for_empty_postgres_config(); $self->check_for_proc_mdstat_recovery(); $self->check_usr_local_cpanel_path_for_symlinks(); $self->check_for_system_mem_below_required(); $self->check_yum_conf(); $self->check_yum_plugins(); $self->check_for_yum_protections(); $self->check_for_ubuntu_excludes(); $self->check_for_cpanel_files(); $self->check_bash_history_for_certain_commands(); $self->check_roots_cron_for_certain_commands(); $self->check_for_missing_or_commented_customlog(); $self->check_for_cpsources_conf(); $self->check_for_apache_rlimits(); $self->check_for_non_default_modsec_rules(); $self->check_etc_hosts_sanity(); $self->check_for_valid_resolv_conf(); $self->check_localhost_resolution(); $self->check_for_apache_listen_host_is_localhost(); $self->check_roundcube_mysql_pass_mismatch(); $self->check_for_hooks_from_manage_hooks(); $self->check_mysqld_warnings_errors(); $self->check_mysql_config(); $self->check_mysql_datadir(); $self->check_for_extra_mysql_config_files(); $self->check_perl_version_less_than_588(); $self->check_for_low_ulimit_for_root(); $self->check_for_fork_bomb_protection(); $self->check_for_harmful_php_mode_600_cron(); $self->check_for_custom_exim_conf_local(); $self->check_for_maxclients_or_maxrequestworkers_reached(); $self->check_for_non_default_umask(); $self->check_for_multiple_imagemagick_installs(); $self->check_eximstats_size(); $self->check_for_broken_mysql_tables(); $self->check_for_clock_skew(); $self->check_if_httpdconf_ipaddrs_exist(); $self->check_for_custom_repos(); $self->check_for_rpm_overrides(); $self->check_var_cpanel_immutable_files(); $self->check_for_noxsave_in_grub_conf(); $self->check_for_rpm_dist_ver_unknown(); $self->check_for_networkmanager(); $self->check_for_prefix_in_network(); $self->check_for_dhclient(); $self->check_for_var_cpanel_roundcube_install(); $self->check_for_missing_etc_localtime(); $self->check_cpanel_config(); $self->check_pure_ftpd_conf_for_upload_script_and_dead(); $self->check_for_disabled_services(); $self->check_for_cpbackup_exclude_everything(); $self->check_for_bw_module_and_more_than_1024_vhosts(); $self->check_for_uppercase_chars_in_hostname(); $self->check_for_bad_permissions_on_named_ca(); $self->check_var_db_nscd_directory(); $self->check_for_jailshell_additional_mounts_trailing_slash(); $self->check_for_allow_query_localhost(); $self->check_for_nocloudlinux_touchfile(); $self->check_for_stupid_touchfile(); $self->check_for_noverify_SSL_touchfile(); $self->check_for_team_manager_touchfile(); $self->check_team_user_count(); $self->check_for_invalid_HOMEDIR(); $self->check_if_hostname_missing_from_localdomains(); $self->check_for_eximstats_newline(); $self->check_for_processes_killed_by_lfd(); $self->check_for_processes_killed_by_oom(); $self->check_for_processes_killed_by_prm(); $self->check_for_broken_userdatadomains(); $self->check_ssl_db_perms(); $self->check_for_stray_index_php(); $self->check_for_port_80_not_apache(); $self->check_for_missing_groups(); $self->check_for_noquotafs(); $self->check_for_roundcube_overlay(); $self->check_for_hostname_park_zoneexists(); $self->check_for_pgpass_colon_in_password_field(); $self->check_for_extra_uid_0_user(); $self->check_sudoers_files(); $self->check_for_allow_update_in_named_conf(); $self->check_for_broken_mysqldump(); $self->check_exim_log_sanity(); $self->check_exim_localopts(); $self->check_updatelog(); $self->check_for_readonly_filesystems(); $self->check_for_cl_unsupported_memory_limits(); $self->check_memory_limit_in_bytes_value(); $self->check_for_eblockers(); $self->check_cloudlinux_sanity(); $self->check_for_modsec2_stage_files(); $self->check_for_cron_allow(); $self->check_for_dev_sandbox(); $self->check_for_jail_owner(); $self->check_sshd_config(); $self->check_for_saltstack(); $self->check_for_puppet_agent(); $self->check_for_etc_ubic_dir(); $self->check_for_acls_cpconf(); $self->check_for_port_53_dnsmasq(); $self->check_for_port_21_ftp(); $self->check_root_dns_resolvers(); $self->check_cloudlinux_phphandler_file(); $self->check_ftpusers_file(); $self->check_mylogincnf(); $self->check_themesconf(); $self->check_whm_for_themes(); $self->check_use_apache_md5_for_htaccess(); $self->check_for_wordpress_manager_rpms(); $self->count_undefined_packages(); } ############################## # BEGIN [WARN] CHECKS ############################## sub check_for_license_error { my $cpanel_error_log = '/usr/local/cpanel/logs/error_log'; my $license_error_file = '/usr/local/cpanel/logs/license_error.display'; # TECH-695 # These can occur at any time so we need to handle it before bailing out when $license_error_file doesn't exist. # Look for lines like the following, discard anything BEFORE LAST "cpsrvd started" line indicating most recent cpsrvd restart, if any. # ==> cpsrvd 11.78.0.11 started # License Check Failed during startup of cpsrvd: License corrupted, please fetch a new license file. # License Check Failed: License corrupted, please fetch a new license file. if ( SSP::Util::cpanel_version_is(qw(>= 11.78.0.11)) ) { my $start_match = qr{ \A ==> \s cpsrvd \s .+ \s started }xms; my $error_match = qr{ \A License \s Check \s Failed }xms; my $match_limit = 4; my $error_ar = get_last_lines_from_file( $cpanel_error_log, qr{ (?: $start_match | $error_match ) }xms, $match_limit ); tail_array_after_match( $error_ar, $start_match ); if ( @{$error_ar} ) { my $indent = "\n \\_ "; SSP::Util::print_warn('License Error: '); SSP::Util::print_warning( "found in $cpanel_error_log after last cpsrvd restart:$indent" . join $indent, @{$error_ar} ); } } # Everything after this depends on something in $license_error_file return unless -f $license_error_file && -s _; my $license_error; if ( open( my $license_error_fh, '<', $license_error_file ) ) { while (<$license_error_fh>) { if (m{\AThe exact message was: (.+)\Z}ms) { $license_error = $1; chomp $license_error; last; } } close $license_error_fh; } return unless defined $license_error; if ( SSP::Util::cpanel_version_is(qw( < 11.32.0.0 )) ) { SSP::Util::print_warn('License Error: '); SSP::Util::print_warning( '[ ' . $license_error . ' ]' ); SSP::Util::print_warning(' \_ Try updating to WHM 11.32 or later to resolve any license-related problems.'); return; } # Everything after this is WHM 11.32+ if ( $license_error =~ m{ \A \QThe hostname must be a Fully Qualified Domain Name! (\E.+\) | \QAbort, Retry, Fail?\E \Z }xms ) { SSP::Util::print_warn('License Error: '); SSP::Util::print_warning( '[ ' . $license_error . ' ]' ); SSP::Util::print_warning(' \_ If this license error is not resolved after correctly setting the hostname AND ensuring that the hostname can be pinged from the local host ( ping `hostname` ), then fork ticket for license issue if not related to current issue, send "ESCALATE - License issue to Dev" response, and escalate ticket to "QA/Development".'); } elsif ( # This one is CRIT because it requires escalation $license_error =~ m{ \A \QDoes not compute!\E | \QReturn without Gosub.\E | \QPrinting is not supported on this printer.\E | \QCannot issue a license to \E[^ ]+\Q without a \E(DISTRO|OSVER)\. \Z }xms ) { SSP::Util::print_crit('License Error: '); SSP::Util::print_critical( '[ ' . $license_error . ' ]' ); SSP::Util::print_critical(' \_ Fork ticket for license issue if not related to current issue, send "ESCALATE - License issue to Dev" response, and escalate ticket to "QA/Development".'); } else { SSP::Util::print_warn('License Error: '); SSP::Util::print_warning( '[ ' . $license_error . ' ]' ); } } sub check_for_license_creds { return unless SSP::Util::i_am('cpanel'); my $license_creds_file = '/var/cpanel/licenseid_credentials.json'; my $license_creds_raw; if ( open( my $fh, '<', $license_creds_file ) ) { local $/ = undef; $license_creds_raw = <$fh>; close($fh); } my $license_creds_href = $license_creds_raw && SSP::Util::get_json_href($license_creds_raw); if ( !$license_creds_href || !keys(%$license_creds_href) ) { SSP::Util::print_warn('License Error: '); SSP::Util::print_warning( "$license_creds_file" . ' is missing or empty. Is port 2083 inbound open? This can cause login issues or "Unauthorized" error in AutoSSL' ); } } sub get_last_lines_from_file { # Returns array-ref containing the LAST $return_limit (number) lines that match $pattern which exist within $bytes_limit bytes of the end of $file. # Everything but $file is optional. # TODO: Read file in reverse similar to File::ReadBackwards (not using it right now because not a core module) my ( $file, $pattern, $return_limit, $bytes_limit ) = @_; return [] unless -e $file; return [] unless my $size = ( stat($file) )[7]; $bytes_limit = defined $bytes_limit ? $bytes_limit + 0 : 5_000_000; # Default is last 5MB of file $return_limit = defined $return_limit ? $return_limit + 0 : 1; # Default is 1 line returned $pattern = defined $pattern ? $pattern : qr{}; # Default is any match my @lines; if ( open my $fh, '<', $file ) { seek $fh, -$bytes_limit, 2; while (<$fh>) { if (m{$pattern}xms) { chomp; shift @lines if scalar @lines >= $return_limit; push @lines, $_; } } close $fh; } return \@lines; } sub tail_array_after_match { # Modifies $aref in place by searching backwards for first $pattern match and discarding the match and everything before it. my ( $aref, $pattern ) = @_; for ( my $i = $#{$aref}; $i >= 0; $i-- ) { next unless $aref->[$i] =~ $pattern; splice( @{$aref}, 0, $i + 1 ); last; } } sub check_port_hash { my $ports = SSP::Util::get_lsof_port_href(); return if scalar keys(%$ports); SSP::Util::print_warn('lsof: '); SSP::Util::print_warning('Did not return a list of TCP ports in LISTEN state. Either lsof is broken or there are zero listening services. Some port-based checks will be skipped!'); } sub check_selinux_status { my @selinux_status = split /\n/, SSP::Util::timed_run( 0, 'sestatus' ); return if !@selinux_status; for my $line (@selinux_status) { if ( $line =~ m{ \A SELinux \s status: \s+ ([^\s\n]+) }xms ) { return if $1 eq "disabled"; } elsif ( $line =~ m{ \A Current \s mode: \s+ ([^\s\n]+) }xms ) { if ( $1 eq "permissive" ) { SSP::Util::print_info('SELinux: '); SSP::Util::print_normal('Permissive'); return; } else { SSP::Util::print_warn('SELinux: '); SSP::Util::print_warning('is ENFORCING!'); return; } } } } sub check_runlevel { return if ( SSP::Util::i_am('ubuntu') ); my $runlevel; my $who_r = SSP::Util::timed_run( 0, 'who', '-r' ); # CentOS 5.7, 5.8: # run-level 3 2012-01-25 10:38 last=S if ( $who_r =~ m{ \A \s* run-level \s (\S+) }xms ) { $runlevel = $1; if ( $runlevel ne "3" ) { SSP::Util::print_warn('Runlevel: '); SSP::Util::print_warning("runlevel is not 3 (current runlevel: $runlevel)"); } } } sub check_multiuser_target { return if ( SSP::Util::i_am('ubuntu') ); return if SSP::Util::os_version_is(qw( < 7 )) || SSP::Util::i_am('unsupported_os'); chomp( my $target = SSP::Util::timed_run( 0, 'systemctl', 'get-default' ) || return ); chomp( my $active = SSP::Util::timed_run( 0, 'systemctl', 'is-active', 'multi-user.target' ) || return ); my $warn; $warn .= ' [current ' . $target . '] (default multi-user.target)' if ( $target ne 'multi-user.target' ); $warn .= ' [current ' . $active . '] (default active)' if ( $active ne 'active' ); if ($warn) { SSP::Util::print_warn('Boot Target:'); SSP::Util::print_warning( $warn . ' This can prevent tailwatchd or queueprocd from properly starting, see TECH-362/TECH-1024' ); } } sub check_for_missing_root_cron { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); my $cron = '/var/spool/cron/root'; $cron = '/var/spool/cron/crontabs/root' if ( SSP::Util::i_am('ubuntu') ); return if -f $cron; SSP::Util::print_warn('Missing cron: '); SSP::Util::print_warning("root's cron file $cron is missing!"); } sub check_for_missing_usr_bin_crontab { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); my $crontab = '/usr/bin/crontab'; return if -f $crontab; SSP::Util::print_warn('Missing crontab binary: '); SSP::Util::print_warning( 'file ' . $crontab . ' is missing! Seeing "warn [jail_safe_crontab] Cpanel::Wrap::send_cpwrapd_request error"? This may be why.' ); } sub check_if_upcp_is_running { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); if ( SSP::Util::exists_process_cmd( qr{ cPanel \s Update \s \(upcp\) }xms, 'root' ) ) { SSP::Util::print_warn('upcp check: '); SSP::Util::print_warning('upcp is currently running'); } elsif ( -e '/usr/local/cpanel/upgrade_in_progress.txt' ) { SSP::Util::print_warn('upcp check: '); SSP::Util::print_warning('/usr/local/cpanel/upgrade_in_progress.txt found, but upcp doesn\'t appear to be running. Last run failed? If Tweak Settings is not loading, this may be why.'); } } sub check_valid_upcp { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); #my $updatenow_static = '/scripts/updatenow.static'; my $updatenow_static = '/usr/local/cpanel/scripts/updatenow.static'; if ( !-f $updatenow_static ) { SSP::Util::print_warn('Valid updatenow.static: '); SSP::Util::print_warning("$updatenow_static does not exist as a file!"); } else { my $update_now_text = ''; if ( open( my $updatenow_fh, '<', $updatenow_static ) ) { local $/ = undef; $update_now_text = readline($updatenow_fh); close $updatenow_fh; } if ( $update_now_text !~ m/our \$VERSION_BUILD/s ) { SSP::Util::print_warn('Valid updatenow.static: '); SSP::Util::print_warning("No VERSION_BUILD info found in $updatenow_static, could be broken!"); } } } sub check_cpupdate_conf { return unless my $cpupdate_conf = SSP::Util::get_cpupdate_conf(); ( $YUM_OR_DNF, $YUM_CONF ) = SSP::Util::os_version_is(qw( >= 8)) ? ( 'dnf', '/etc/dnf/dnf.conf' ) : ( 'yum', '/etc/yum.conf' ); $YUM_OR_DNF = ( SSP::Util::i_am('ubuntu') ) ? "apt" : $YUM_OR_DNF; return unless ( -f $YUM_CONF ); my $_is_allowed = sub { my ($type) = @_; return 0 if ( defined $cpupdate_conf->{$type} and ( $cpupdate_conf->{$type} eq "never" or $cpupdate_conf->{$type} eq "manual" ) ); return 1; }; unless ( $_is_allowed->('UPDATES') ) { SSP::Util::print_warn('/etc/cpupdate.conf: '); SSP::Util::print_warning('UPDATES set to never or manual -- do not run /scripts/upcp without customer approval. Recommend enabling automatic updates if the issue would be resolved by an update.'); } unless ( $_is_allowed->('RPMUP') ) { SSP::Util::print_warn('/etc/cpupdate.conf: '); SSP::Util::print_warning( 'RPMUP set to never or manual -- prevents automatic updates to EA4 and other ' . $YUM_OR_DNF . '-managed packages. Recommend enabling automatic updates if the issue would be resolved by an update.' ); } unless ( $_is_allowed->('SARULESUP') ) { SSP::Util::print_warn('/etc/cpupdate.conf: '); SSP::Util::print_warning('SARULESUP set to never or manual -- prevents automatic updates of SpamAssassin rules. Recommend enabling automatic updates if the issue would be resolved by an update.'); } } sub check_interface_lo { my $output = SSP::Util::timed_run( 0, 'ip', 'addr', 'show', 'dev', 'lo' ); $output ||= SSP::Util::timed_run( 0, 'ifconfig', 'lo' ); return check_loopback_connection() if $output =~ /UP.LOOPBACK|LOOPBACK.UP/; # ip addr and ifconfig swap the LOOPBACK and UP keywords SSP::Util::print_warn('Loopback Interface: '); SSP::Util::print_warning('loopback interface is not up!'); } sub check_mainip_file { if ( open my $file_fh, '<', '/var/cpanel/mainip' ) { while (<$file_fh>) { chomp; if (/^$/) { SSP::Util::print_warn('The /var/cpanel/mainip '); SSP::Util::print_warning('file contains a carriage-return. - Run /usr/local/cpanel/scripts/mainipcheck to fix!'); } } close $file_fh; } } sub check_loopback_connection { return if $OPT_SKIP_NETWORKING; return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); my @ports = qw( 25 80 2086 ); my $connected = 0; for my $port (@ports) { my $sock = IO::Socket::INET->new( PeerAddr => '127.0.0.1', PeerPort => $port, Proto => 'tcp', Timeout => '1', ); if ($sock) { $connected = 1; close $sock; last; } } if ( !$connected ) { SSP::Util::print_warn('Loopback connectivity: '); SSP::Util::print_warning('could not connect to 127.0.0.1 on port 25, 80, or 2086'); } } sub check_cpanelconfig_filetype { return unless -e $CPANEL_CONFIG_FILE; chomp( my $file = SSP::Util::timed_run( 0, 'file', $CPANEL_CONFIG_FILE ) ); if ( $file !~ m{ \A \Q$CPANEL_CONFIG_FILE\E: \s ASCII \s ( English \s)? text (, \s with \s very \s long \s lines)? \z }xms ) { SSP::Util::print_warn("$CPANEL_CONFIG_FILE: "); SSP::Util::print_warning("filetype is something other than 'ASCII text'! ($file)"); } } sub check_cpanelsync_exclude_no_chmod { my $cpanelsync_exclude_no_chmod = '/etc/cpanelsync.no_chmod'; return unless ( -f $cpanelsync_exclude_no_chmod && -s _ ); SSP::Util::print_warn('cpanelsync exclude: '); SSP::Util::print_warning("$cpanelsync_exclude_no_chmod is not empty!"); } sub check_cpanelsync_exclude { my $cpanelsync_exclude = '/etc/cpanelsync.exclude'; return unless -f $cpanelsync_exclude; return unless -s $cpanelsync_exclude; my $rpmversions_file = '/usr/local/cpanel/etc/rpm.versions'; SSP::Util::print_warn('cpanelsync exclude: '); SSP::Util::print_warning("$cpanelsync_exclude is not empty!"); if ( open my $file_fh, '<', $cpanelsync_exclude ) { while (<$file_fh>) { chomp; if (m{ \A \s* $rpmversions_file \s* \z }xms) { SSP::Util::print_warn('cpanelsync exclude: '); SSP::Util::print_warning("$rpmversions_file found! This should NEVER be done!"); last; } } close $file_fh; } } sub check_for_local_templates { return unless SSP::Util::i_am('cpanel'); my @templatedirs = qw( /var/cpanel/templates/apache2_4 /var/cpanel/templates/apache2_2 /var/cpanel/templates/apache2_0 /var/cpanel/templates/apache2 /var/cpanel/templates/apache1_3 /var/cpanel/templates/apache1 /var/cpanel/templates/dovecot2.2 /var/cpanel/templates/dovecot2.3 /var/cpanel/templates/dovecotSNI ); # Order is somewhat important above for cosmetic reasons, due to symlinks my %templatedirs = (); for my $templatedir (@templatedirs) { # Canonicalize symlinks so we only check a real path once, but store original name for printing. next if !-d $templatedir; $templatedirs{ SSP::Util::abs_path($templatedir) } = $templatedir; } for my $templatedir ( sort( keys(%templatedirs) ) ) { my @dir_contents = (); if ( opendir( my $dir_fh, $templatedir ) ) { @dir_contents = readdir $dir_fh; closedir $dir_fh; } my $templates = undef; for my $template (@dir_contents) { if ( $template =~ m{ \.local \z }xms ) { $templates .= YELLOW " $template"; if ( -z "$templatedir/$template" ) { $templates .= MAGENTA " [EMPTY!]"; } $templates .= YELLOW; } } if ($templates) { SSP::Util::print_warn( 'Custom templates (' . $templatedirs{$templatedir} . '): ' ); SSP::Util::print_warning($templates); } } } sub check_dovecot_conf_for_wrap { return unless ( -s '/etc/dovecot/dovecot.conf' ); my @dovecot_conf; if ( open my $conf_fh, '<', '/etc/dovecot/dovecot.conf' ) { @dovecot_conf = <$conf_fh>; close($conf_fh); } my $dovecot_wrap_found = grep { /dovecot-wrap/ } @dovecot_conf; return unless ($dovecot_wrap_found); SSP::Util::print_warn('Dovecot Conf: '); SSP::Util::print_warning('legacy dovecot authentication (dovecot-wrap) found in /etc/dovecot.conf - Seeing incorrect authentication data?'); } sub check_for_missing_account_suspensions_conf { return unless SSP::Util::i_am('cpanel'); my @templates; if ( SSP::Util::i_am('ea4') ) { return unless -f '/etc/apache2/conf.d/includes/account_suspensions.conf'; @templates = qw ( /var/cpanel/templates/apache2_4/ea4_main.local ); } my %templates = (); for my $template (@templates) { # Canonicalize symlinks so we only check a real path once, but store original name for printing. next unless -f $template; $templates{ SSP::Util::abs_path($template) }[0] = $template; } } sub check_for_custom_apache_includes { return unless SSP::Util::i_am('cpanel'); my $include_dir = SSP::Util::i_am('ea4') ? '/etc/apache2/conf.d/includes' : '/usr/local/apache/conf/includes'; return if !-d $include_dir; my @includes = qw( post_virtualhost_1.conf post_virtualhost_2.conf post_virtualhost_global.conf pre_main_1.conf pre_main_2.conf pre_main_global.conf pre_virtualhost_1.conf pre_virtualhost_2.conf pre_virtualhost_global.conf ); my $custom_includes; for my $include (@includes) { if ( -s "${include_dir}/${include}" ) { if ( $include eq 'pre_virtualhost_global.conf' ) { my $md5 = SSP::Util::timed_run( 0, 'md5sum', $include_dir . 'pre_virtualhost_global.conf' ); next if ( $md5 && $md5 =~ m{ \A 1693b9075fa54ede224bfeb8ad42a182 \s }xms ); } $custom_includes .= ' [' . $include . ']'; } } if ($custom_includes) { SSP::Util::print_warn( 'Apache Includes [' . $include_dir . ']:' ); SSP::Util::print_warning($custom_includes); } } sub check_for_tomcatoptions { return unless SSP::Util::i_am('cpanel'); my $tomcat_options = '/var/cpanel/tomcat.options'; if ( -f $tomcat_options and not -z $tomcat_options ) { my $md5 = SSP::Util::timed_run( 0, 'md5sum', '/var/cpanel/tomcat.options' ); return if ( $md5 && $md5 =~ m{ \A 0cb9b170cbb81795c2669f8ebf08d0dd \s }xms ); ## -Xss2m SSP::Util::print_warn('Tomcat options: '); SSP::Util::print_warning("$tomcat_options exists"); } } sub check_for_sneaky_htaccess { return unless SSP::Util::i_am('cpanel'); ## this is lazy checking. ideally we'd check HOMEMATCH from wwwacct.conf and go from there. ## but then, nothing guarantees the current HOMEMATCH has always been the same, either. my @dirs = qw( / /var/www/html/ /home/ /home2/ /home3/ /home4/ /home5/ /home6/ /home7/ /home8/ /home9/ ); my $htaccess; for my $dir (@dirs) { if ( -f $dir . '.htaccess' and not -z $dir . '.htaccess' ) { $htaccess .= $dir . '.htaccess '; } } if ($htaccess) { SSP::Util::print_warn('Sneaky .htaccess file(s) found: '); SSP::Util::print_warning($htaccess); } } sub check_ea4_paths_conf { return unless SSP::Util::i_am('ea4'); my $paths_conf = '/etc/cpanel/ea4/paths.conf'; lstat($paths_conf); # Can now use _ for file tests. if ( !-e _ ) { SSP::Util::print_warn('EA4: '); SSP::Util::print_warning('/etc/cpanel/ea4/paths.conf is missing!'); return; } if ( !-f _ ) { SSP::Util::print_warn('EA4: '); SSP::Util::print_warning('/etc/cpanel/ea4/paths.conf is not a normal file!'); return; } if ( -z _ ) { SSP::Util::print_warn('EA4: '); SSP::Util::print_warning('/etc/cpanel/ea4/paths.conf is empty!'); return; } if ( !-T _ ) { SSP::Util::print_warn('EA4: '); SSP::Util::print_warning('/etc/cpanel/ea4/paths.conf does not appear to be an ASCII text file!'); return; } my $unknown_count; my %conf; my %default_conf = ( # From ea-apache24-config-runtime-1.0-81.81.4.cpanel.noarch 'bin_apachectl' => '/usr/sbin/apachectl', 'bin_httpd' => '/usr/sbin/httpd', 'bin_suexec' => '/usr/sbin/suexec', 'dir_base' => '/etc/apache2', 'dir_conf' => '/etc/apache2/conf.d', 'dir_conf_includes' => '/etc/apache2/conf.d/includes', 'dir_conf_userdata' => '/etc/apache2/conf.d/userdata', 'dir_docroot' => '/var/www/html', 'dir_domlogs' => '/etc/apache2/logs/domlogs', 'dir_logs' => '/etc/apache2/logs', 'dir_modules' => '/etc/apache2/modules', 'dir_run' => '/run/apache2', 'file_access_log' => '/etc/apache2/logs/access_log', 'file_conf' => '/etc/apache2/conf/httpd.conf', 'file_conf_mime_types' => '/etc/apache2/conf/mime.types', 'file_conf_php_conf' => '/etc/apache2/conf.d/php.conf', 'file_conf_srm_conf' => '/etc/apache2/conf.d/srm.conf', 'file_error_log' => '/etc/apache2/logs/error_log', ); if ( SSP::Util::os_version_is(qw( < 7.0 )) or SSP::Util::i_am('unsupported_os') ) { $default_conf{'dir_run'} = '/var/run/apache2'; } if ( open my $conf_fh, '<', $paths_conf ) { while (<$conf_fh>) { next if /^(#|$)/; if (m{ \A \s* ([^=]+?) \s* = \s* ([^\$]*?) \Z }x) { # Cpanel::Config::LoadConfig::loadConfig( $path, $conf, '\s*=\s*', undef, '^\s*' ); $conf{$1} = $2; } } close $conf_fh; if ( !scalar keys %conf ) { SSP::Util::print_warn('EA4: '); SSP::Util::print_warning('/etc/cpanel/ea4/paths.conf does not appear to contain any valid configuration!'); return; } foreach my $key ( sort keys %conf ) { if ( !exists $default_conf{$key} ) { # EA4 appears to ignore any unknown options, but count them. $unknown_count++; next; } if ( $default_conf{$key} ne $conf{$key} ) { SSP::Util::print_warn('EA4: '); SSP::Util::print_warning( '/etc/cpanel/ea4/paths.conf non-default setting: [ ' . $key . '=' . $conf{$key} . ' ]' ); } } foreach my $key ( sort keys %default_conf ) { next if exists $conf{$key}; SSP::Util::print_warn('EA4: '); SSP::Util::print_warning( '/etc/cpanel/ea4/paths.conf missing default setting: [ ' . $key . ' ]' ); } if ($unknown_count) { SSP::Util::print_warn('EA4: '); SSP::Util::print_warning( '/etc/cpanel/ea4/paths.conf contains ' . $unknown_count . ' unknown configuration setting(s)!' ); } } } sub check_ea4_missing_cl_repo { return unless SSP::Util::i_am( 'cloudlinux', 'ea4' ); my $clea4repo = '/etc/yum.repos.d/cloudlinux-ea4.repo'; return if -f $clea4repo; SSP::Util::print_warn('Missing Repo: '); SSP::Util::print_warning( $clea4repo . ' - This can break EA4, see TECH-764.' ); } sub check_apache_modules { return unless SSP::Util::i_am('cpanel'); my $installed_modules = SSP::Util::get_apache_modules_href(); return unless scalar keys %{$installed_modules}; my $apache_version = SSP::Util::get_apache_version(); my ( $lsws_full_version, $lsws_numeric_version ) = @{ SSP::Util::get_lsws_version_aref() }; # Example: 'foo_module' => { help => [ 'Some help text.' ], check_missing => 1 } # or: push @{ $check{'foo_module'}{'help'} }, 'More help text.'; # Set check_missing => 1 to report missing instead of installed module my %check = ( 'evasive20_module' => { help => ['Can result in random 403s. Check /var/log/apache2/mod_evasive/ if relevant.'] }, 'evasive24_module' => { help => ['Can result in random 403s. Check /var/log/apache2/mod_evasive/ if relevant.'] }, 'headers_module' => { help => ['May cause proxy subdomains to redirect infinitely, see CPANEL-12707.'], check_missing => 1 }, 'hive_module' => { help => ['Third-party - 1H Hive. Not supported.'] }, 'lua_module' => { help => ['Experimental. Potential security issues in shared hosting environments.'] }, 'rpaf_module' => { help => ['May prevent mod_http2 from working -- see 8772327. May prevent .htaccess from denying access -- see 4422297.'] }, 'spdy_module' => { help => ['May break proxy subdomains. See 4973361.'] }, ); my $add = sub { my ( $mod, $text, $check_missing ) = @_; push @{ $check{$mod}{'help'} }, $text; $check{$mod}{'check_missing'} = 1 if $check_missing; }; $add->( 'http2_module', 'Causes segfaults in Apache 2.4.25, see EAL-3153.' ) if SSP::Util::version_compare( $apache_version, qw ( == 2.4.25 ) ); $add->( 'userdir_module', 'Does not work with passenger_module.' ) if defined $installed_modules->{'passenger_module'}; $add->( 'userdir_module', 'Does not work with ruid2_module.' ) if defined $installed_modules->{'ruid2_module'}; $add->( 'userdir_module', 'Does not work with mpm_itk_module.' ) if defined $installed_modules->{'mpm_itk_module'}; $add->( 'ruid2_module', 'Can cause file permission problems when using LiteSpeed Web Server' ) if $lsws_full_version; if ( SSP::Util::i_am('ea4') ) { $add->( 'fcgid_module', 'Has many caveats, see https://docs.cpanel.net/ea4/apache/apache-module-fcgid/' ); $add->( 'userdir_module', 'PHP scripts accessed by userdir will not be executed via PHP-FPM.' ); my $ea4_php = SSP::Util::get_installed_ea4_php_href(); if ( defined($ea4_php) && defined( $ea4_php->{default} ) && defined( $ea4_php->{ $ea4_php->{default} }->{handler} ) && $ea4_php->{ $ea4_php->{default} }->{handler} eq "cgi" ) { $add->( 'userdir_module', 'Will not execute PHP scripts via CGI handler.' ); } } if ( defined $installed_modules->{'security_module'} or defined $installed_modules->{'security2_module'} ) { $add->( 'mpm_itk_module', 'Incompatible with ModSecurity SecDataDir (collections). See case EA-4093.' ); $add->( 'ruid2_module', 'Incompatible with ModSecurity SecDataDir (collections). See case EA-4093.' ); } if ( defined $CPCONF{'jailapache'} && $CPCONF{'jailapache'} == 1 ) { $add->( 'ruid2_module', 'Enabled with Jail Apache Virtual Hosts tweak. This can break some Mailman URLs. See CPANEL-9501 and CPANEL-18127.' ); } if ( SSP::Util::i_am('cloudlinux') ) { $add->( 'mpm_itk_module', 'CloudLinux LVE memory limits not imposed on Apache processes, and not compatible with PHP Selector - https://docs.cloudlinux.com/index.html?compatiblity_matrix.html' ); $add->( 'ruid2_module', 'CloudLinux LVE memory limits not imposed on Apache processes, and not compatible with PHP Selector - https://docs.cloudlinux.com/index.html?compatiblity_matrix.html' ); # TECH-1314 - Check for SuExec if ALT-PHP is installed and SuExec is not. WARN user that ALT-PHP may not function. my $rpms = SSP::Util::get_rpm_href(); my @alt_php_rpms = grep { /^alt-php[0123456789][0123456789]/ } keys( %{$rpms} ); @alt_php_rpms = map { SSP::Util::get_printable_rpm_packages($_) } @alt_php_rpms; $add->( 'suexec_module', 'CloudLinux and ALT-PHP detected, SuExec module required for ALT-PHP to properly function', check_missing => 1 ) unless ( !@alt_php_rpms ); } for my $module ( sort keys %check ) { my $help_text = join( "\n" . ' ' x ( length($module) + 23 ) . '\_ - ', @{ $check{$module}{'help'} } ); if ( defined $check{$module}{'check_missing'} and not defined $installed_modules->{$module} ) { SSP::Util::print_warn('Apache: '); SSP::Util::print_warning( 'Missing ' . $module . ' - ' . $help_text ); } if ( not defined $check{$module}{'check_missing'} and defined $installed_modules->{$module} ) { SSP::Util::print_warn('Apache: '); SSP::Util::print_warning( ' Loaded ' . $module . ' - ' . $help_text ); } } } sub check_apache_niceness { return unless my $httpd_bin = SSP::Util::find_httpd_bin(); return unless my %procs = SSP::Util::grep_process_cmd( $httpd_bin, 'root' ); my $apache_nice; my $apache_ionice; for my $pid ( sort keys %procs ) { $apache_nice = $procs{$pid}->{'NICE'}; $apache_ionice = SSP::Util::timed_run( 0, 'ionice', '-p', $pid ); chomp $apache_ionice; last; } my $cp20037_nice = '18'; my $cp20037_bw_ionice = defined $CPCONF{'ionice_bandwidth_processing'} ? $CPCONF{'ionice_bandwidth_processing'} : '6'; my $cp20037_log_ionice = defined $CPCONF{'ionice_log_processing'} ? $CPCONF{'ionice_log_processing'} : '7'; my $cp20037_ionice_regex = '\A best-effort: \s prio \s (?:' . $cp20037_bw_ionice . '|' . $cp20037_log_ionice . ') \Z'; my $cp20037info = ' - See CPANEL-20037 for a possible cause.'; # Make the text conditional on build version after CPANEL-20037 is published if ($apache_nice) { # Anything other than 0 SSP::Util::print_warn('Apache: '); SSP::Util::print_warning( 'has unexpected nice value [ ' . $apache_nice . ' ] - May result in Apache performance issues' . ( $apache_nice eq $cp20037_nice ? $cp20037info : '' ) ); } if ( $apache_ionice and not $apache_ionice =~ m{ \A (?:none|unknown): \s prio \s [04] \Z }xms ) { # "none: prio 0", "unknown: prio 0", "none: prio 4", "unknown: prio 4" all acceptable SSP::Util::print_warn('Apache: '); SSP::Util::print_warning( 'has unexpected ionice value [ ' . $apache_ionice . ' ] - May result in Apache performance issues' . ( $apache_ionice =~ m{ $cp20037_ionice_regex }xms ? $cp20037info : '' ) ); } } sub check_other_niceness { my @binstocheck = qw( cPhulkd cpsrvd dbus daemon dovecot exim mysqld named nscd rsyslogd spamd systemd ); my $cp20037_nice = '18'; my $cp20037_bw_ionice = $CPCONF{'ionice_bandwidth_processing'} // '6'; my $cp20037_log_ionice = $CPCONF{'ionice_log_processing'} // '7'; my $cp20037_ionice_regex = '\A best-effort: \s prio \s (?:' . $cp20037_bw_ionice . '|' . $cp20037_log_ionice . ') \Z'; my $niceval; my $ioniceval; foreach my $bintocheck (@binstocheck) { chomp($bintocheck); next unless my %procs = SSP::Util::grep_process_cmd($bintocheck); my $ucBin = ucfirst($bintocheck); for my $pid ( sort keys %procs ) { $niceval = $procs{$pid}->{'NICE'}; $ioniceval = SSP::Util::timed_run( 0, 'ionice', '-p', $pid ); chomp($ioniceval); last; } if ($niceval) { SSP::Util::print_warn( $ucBin . ': ' ); SSP::Util::print_warning( 'has unexpected nice value [ ' . $niceval . ' ] - May result in performance issues' . ( $niceval eq $cp20037_nice ? $cp20037_nice : '' ) ); } if ( $ioniceval and not $ioniceval =~ m{ \A (?:none|unknown): \s prio \s [04] \Z }xms ) { # "none: prio 0", "unknown: prio 0", "none: prio 4", "unknown: prio 4" all acceptable if ( $ioniceval =~ m{ $cp20037_ionice_regex }xms ) { SSP::Util::print_warn( $ucBin . ': ' ); SSP::Util::print_warning( 'has unexpected ionice value [ ' . $ioniceval . ' ] - May result in performance issues' ); } } } } sub check_perl_sanity { return unless SSP::Util::i_am('cpanel'); my $usr_bin_perl = '/usr/bin/perl'; my $usr_local_bin_perl = '/usr/local/bin/perl'; if ( !-e $usr_bin_perl ) { SSP::Util::print_warn('perl: '); SSP::Util::print_warning("$usr_bin_perl does not exist!"); } if ( -l $usr_bin_perl and -l $usr_local_bin_perl ) { my $usr_bin_perl_link = readlink $usr_bin_perl; my $usr_local_bin_perl_link = readlink $usr_local_bin_perl; if ( -l $usr_bin_perl_link and -l $usr_local_bin_perl_link ) { SSP::Util::print_warn('perl: '); SSP::Util::print_warning("$usr_bin_perl and $usr_local_bin_perl are both symlinks!"); } } ## a symlink will test true for both -x AND -l if ( -x $usr_bin_perl and not -l $usr_bin_perl ) { if ( -x $usr_local_bin_perl and not -l $usr_local_bin_perl ) { SSP::Util::print_warn('perl: '); SSP::Util::print_warning("$usr_bin_perl and $usr_local_bin_perl are both binaries!"); } } if ( -x $usr_bin_perl and not -l $usr_bin_perl ) { my $mode = ( stat($usr_bin_perl) )[2] & oct(7777); $mode = sprintf "%lo", $mode; if ( $mode != 755 ) { SSP::Util::print_warn('Perl Permissions: '); SSP::Util::print_warning("$usr_bin_perl is $mode"); } } if ( -x $usr_local_bin_perl and not -l $usr_local_bin_perl ) { my $mode = ( stat($usr_local_bin_perl) )[2] & oct(7777); $mode = sprintf "%lo", $mode; if ( $mode != 755 ) { SSP::Util::print_warn('Perl Permissions: '); SSP::Util::print_warning("$usr_local_bin_perl is $mode"); } } } sub check_for_non_default_file_capabilities { $YUM_OR_DNF = SSP::Util::os_version_is(qw( >= 8)) ? 'dnf' : 'yum'; $YUM_OR_DNF = ( SSP::Util::i_am('ubuntu') ) ? "apt" : $YUM_OR_DNF; # Check for at least these capabilities, more is OK # Example: '/path' => { cap => ['cap_setgid','cap_setuid+ep'], help => 'Some help text' }, # tidyoff my %check = ( ); # tidyon if ( SSP::Util::i_am('ea4') and not SSP::Util::i_am('cloudlinux') ) { $check{'/usr/sbin/suexec'} = { cap => [ 'cap_setgid', 'cap_setuid', 'ep' ], help => 'Will break cpanel/webmail redirects and other suexec usage, "' . $YUM_OR_DNF . ' reinstall ea-apache24" to fix' }; } for my $path ( sort keys %check ) { next unless -e $path; my @result = SSP::Util::get_fcap($path); next unless scalar @result; my @missing; foreach my $cap ( @{ $check{$path}->{cap} } ) { push @missing, $cap unless grep { /^\Q${cap}\E$/ } @result; } if ( scalar @missing ) { my $help = exists $check{$path}->{help} ? ' - ' . $check{$path}->{help} : ''; SSP::Util::print_warn('Non-default capabilities: '); SSP::Util::print_warning( $path . ' is missing ' . join( ',', @missing ) . $help ); } } } sub check_for_non_default_sysctl { my $sysctl = { map { split( /\s=\s/, $_, 2 ) } split( /\n/, SSP::Util::timed_run( 0, 'sysctl', '-a' ) ) }; my %check = ( # 'sysctl_key' => [ ['default1', 'default2', ...], 'Additional info' ] 'fs.enforce_symlinksifowner' => [ [ '0', '1' ], ' - Invalid setting - https://docs.cloudlinux.com/index.html?symlink_owner_match_protection.html' ], 'fs.protected_hardlinks_create' => [ ['0'], '' ], 'fs.protected_symlinks_create' => [ ['0'], ' - Not recommended. Can prevent creation of access_log symlink in user homes, or switching to a custom cPanel style that is not owned by the "linksafe" group.' ], 'kernel.user_ptrace' => [ ['1'], '' ], 'net.ipv4.tcp_tw_recycle' => [ ['0'], ' - This should generally never be enabled. Clients behind NAT or Proxy can have problems connecting to this server.' ], 'net.ipv4.tcp_tw_reuse' => [ [ '0', '2' ], ' - This should generally never be enabled. Clients behind NAT or Proxy can have problems connecting to this server.' ], 'vm.overcommit_memory' => [ [ '0', '1' ], ' - Seeing "Out of memory" but there is free memory and no limits? This might be why.' ], 'net.ipv4.tcp_fin_timeout' => [ ['60'], ' - Custom value for net.ipv4.tcp_fin_timeout, may interfere with TCP timeouts' ], 'net.netfilter.nf_conntrack_tcp_timeout_established' => [ ['432000'], ' - Custom value for net.netfilter.nf_conntrack_tcp_timeout_established, may interfere with TCP timeouts' ], 'net.netfilter.nf_conntrack_generic_timeout' => [ ['600'], ' - Custom value for net.netfilter.nf_conntrack_generic_timeout, may interfere with TCP timeouts' ], ); if ( defined( $sysctl->{'fs.enforce_symlinksifowner'} && $sysctl->{'fs.enforce_symlinksifowner'} ) ) { $check{'fs.symlinkown_gid'} = [ [ '99', '65534' ], ' - Incorrect GID - https://docs.cloudlinux.com/index.html?symlink_owner_match_protection.html' ]; } my $sysctl_inotify = ( split( /\s+/, SSP::Util::timed_run( 0, 'sysctl', 'fs.inotify.max_user_watches' ) ) )[2]; if ( $sysctl_inotify < 8192 ) { SSP::Util::print_warn('Non-default sysctl: '); SSP::Util::print_warning("fs.inotify.max_user_watches ($sysctl->{'fs.inotify.max_user_watches'}) is less than the default of 8192 - Seeing \"No space left on device\" errors? This may be why."); } $sysctl_inotify = ( split( /\s+/, SSP::Util::timed_run( 0, 'sysctl', 'fs.inotify.max_queued_events' ) ) )[2]; if ( $sysctl_inotify < 16384 ) { SSP::Util::print_warn('Non-default sysctl: '); SSP::Util::print_warning("fs.inotify.max_queued_events ($sysctl->{'fs.inotify.max_queued_events'}) is less than the default of 16384 - Seeing \"No space left on device\" errors? This may be why."); } $sysctl_inotify = ( split( /\s+/, SSP::Util::timed_run( 0, 'sysctl', 'fs.inotify.max_user_instances' ) ) )[2]; if ( $sysctl_inotify < 4096 ) { SSP::Util::print_warn('Non-default sysctl: '); SSP::Util::print_warning("fs.inotify.max_user_instances ($sysctl->{'fs.inotify.max_user_instances'}) is less than the default of 4096 - Seeing \"No space left on device\" errors? This may be why."); } for my $key ( sort keys %check ) { if ( exists $sysctl->{$key} ) { my $default = 0; for my $checksysctl ( @{ $check{$key}[0] } ) { if ( $sysctl->{$key} eq $checksysctl ) { $default = 1; } } if ( !$default == 1 ) { SSP::Util::print_warn('Non-default sysctl: '); SSP::Util::print_warning( "$key = $sysctl->{$key} (default: " . join( ' or ', @{ $check{$key}[0] } ) . ")$check{$key}[1]" ); } } } } sub check_for_stale_lockfiles { my %check = ( # '/path' => [ 'type', 'Additional info' ]; # type is pid, fcntl, touch, etc., for future use. '/etc/digestshadow.lock' => [ 'fcntl', 'Can prevent modifying system digestshadow file, check if active with lsof and MOVE ONLY IF STALE.' ], '/etc/group.lock' => [ 'fcntl', 'Can prevent modifying system group file, check if active with lsof and MOVE ONLY IF STALE.' ], '/etc/gshadow.lock' => [ 'fcntl', 'Can prevent modifying system gshadow file, check if active with lsof and MOVE ONLY IF STALE.' ], '/etc/gtmp' => [ 'touch', 'Can prevent modifying system group file, check if active with lsof and MOVE ONLY IF STALE.' ], '/etc/passwd.lock' => [ 'fcntl', 'Can prevent modifying system passwd file, check if active with lsof and MOVE ONLY IF STALE.' ], '/etc/ptmp' => [ 'touch', 'Can prevent modifying system passwd file, check if active with lsof and MOVE ONLY IF STALE.' ], '/etc/shadow.lock' => [ 'fcntl', 'Can prevent modifying system shadow file, check if active with lsof and MOVE ONLY IF STALE.' ], '/etc/subgid.lock' => [ 'touch', 'Can prevent WHM logins. MOVE ONLY IF STALE.' ], ); for my $resource ( sort keys %check ) { if ( -e $resource ) { SSP::Util::print_warn('Lockfile exists: '); SSP::Util::print_warning( $resource . ' (' . $check{$resource}[1] . ')' ); } } } sub check_var_cpanel_users { return unless SSP::Util::i_am('cpanel'); my $var_cpanel_users = '/var/cpanel/users'; return unless -d $var_cpanel_users; return unless opendir( my $dir_fh, $var_cpanel_users ); if ( -e "/var/cpanel/users/nobody" ) { SSP::Util::print_warn('nobody user '); SSP::Util::print_warning("was found within the /var/cpanel/users/ directory. - Causes many issues"); } my @files = grep { !m/\.sw?|^(?:\.\.?|root|system|nobody|^cptkt\w{11})$/ } readdir $dir_fh; closedir $dir_fh; my $group_root_files; for my $file (@files) { next if ( $file !~ /^[a-z0-9]+$/ ); my $gid = ( stat( '/var/cpanel/users/' . $file ) )[5]; if ( $gid == 0 ) { $group_root_files .= " $file"; } } if ($group_root_files) { SSP::Util::print_warn('/v/c/users file(s) owned by group "root": '); SSP::Util::print_warning($group_root_files); } # No need to continue if no users return unless scalar @files; my $userdatadomains = '/etc/userdatadomains'; if ( !-e $userdatadomains ) { SSP::Util::print_warn('Missing file: '); SSP::Util::print_warning("$userdatadomains (new server with no accounts, perhaps)"); } elsif ( -f $userdatadomains and -z $userdatadomains ) { SSP::Util::print_warn('Empty file: '); SSP::Util::print_warning("$userdatadomains (generate it with /scripts/updateuserdatacache --force)"); } my $usercnt = SSP::Util::usercount(); if ( SSP::Util::license_file_is_solo() and $usercnt > 1 ) { SSP::Util::print_crit('License: '); SSP::Util::print_critical('Solo, too many accounts, please escalate this ticket to L3 for further review!'); } } sub check_root_suspended { return unless SSP::Util::i_am('cpanel'); if ( -e '/var/cpanel/suspended/root' ) { SSP::Util::print_warn('root suspended: '); SSP::Util::print_warning('the root account is suspended! Unsuspend it to avoid problems.'); } } sub check_limitsconf { my @limitsconf; if ( open my $limitsconf_fh, '<', '/etc/security/limits.conf' ) { while (<$limitsconf_fh>) { push @limitsconf, $_; } close $limitsconf_fh; } @limitsconf = grep { !/^(\s+|#)/ } @limitsconf; if (@limitsconf) { SSP::Util::print_warn('/etc/security/limits.conf: '); SSP::Util::print_warning('customizations found. DON\'T move/alter! Seeing "Unable to set uids"? See CronUnableToSetUID article, FB-76597.'); } } sub check_themesconf { my $themesconf = '/var/cpanel/themes.conf'; return unless ( -s $themesconf ); SSP::Util::print_warn('/var/cpanel/themes.conf: '); SSP::Util::print_warning('limits the themes users can select in WHM/cPanel, see TECH-303'); } sub check_whm_for_themes { if ( -s '/root/.whmtheme' ) { SSP::Util::print_warn('Custom WHM Themes: '); SSP::Util::print_warning('/root/.whmtheme file present.'); } my $whmthemesdir = '/usr/local/cpanel/whostmgr/docroot/themes/'; return unless opendir( my $dir_fh, $whmthemesdir ); my @dir_contents = grep { !/^(x|\.\.?)$/ } readdir $dir_fh; closedir $dir_fh; if (@dir_contents) { SSP::Util::print_warn('Custom WHM themes directory found: '); SSP::Util::print_warning( "$whmthemesdir contains custom themes. These have been deprecated since v102.\n" . CYAN "\t\\_ @dir_contents" ); } } sub check_disk_space { my @df = split /\n/, SSP::Util::timed_run( 0, 'df' ); for my $line (@df) { if ( $line =~ m{ (9[8-9]|100)% \s+ (.*) }xms ) { my ( $usage, $partition ) = ( $1, $2 ); next if $partition =~ m{ / ( snap | virtfs | ( dev | proc | optimumcache ) \Z ) }xms; SSP::Util::print_warn('Disk space: '); SSP::Util::print_warning( $usage . '% usage on ' . $partition ); } } } sub check_for_yum_protections { return if ( SSP::Util::i_am('ubuntu') ); my @files = ( '/etc/yum/plugins.d/priorities.conf', '/etc/yum/plugins.d/protectbase.conf' ); foreach my $file (@files) { if ( -s $file ) { SSP::Util::print_warn('YUM Protected Repos: '); SSP::Util::print_warning("$file exists - Seeing excluded packages on yum?"); } } } sub check_for_ubuntu_excludes { return unless ( SSP::Util::i_am('ubuntu') ); if ( -e '/etc/dpkg/dpkg.cfg.d/excludes' && -e '/usr/local/sbin/unminimize' ) { SSP::Util::print_crit('Minimized Install: '); SSP::Util::print_critical("detected, some packages may be missing - please run /usr/locall/sbin/unminimize"); } } sub check_disk_inodes { my @df_i = split /\n/, SSP::Util::timed_run( 0, 'df', '-i' ); for my $line (@df_i) { if ( $line =~ m{ (9[8-9]|100)% \s+ (.*) }xms ) { my ( $usage, $partition ) = ( $1, $2 ); next if $partition =~ m{ / ( snap | virtfs | ( dev | proc | optimumcache ) \Z ) }xms; SSP::Util::print_warn('Disk inodes: '); SSP::Util::print_warning("${usage}% inode usage on $partition"); } } } sub check_for_hooks_in_scripts_directory { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); if ( -f '/usr/local/cpanel/Cpanel/CustomEventHandler.pm' ) { SSP::Util::print_warn('Hooks: '); SSP::Util::print_warning('/usr/local/cpanel/Cpanel/CustomEventHandler.pm exists!'); } my @hooks; if ( -d '/scripts' ) { opendir( my $scripts_fh, '/scripts' ); @hooks = sort grep { /^(pre|post)/ } readdir $scripts_fh; closedir $scripts_fh; } # these exist by default @hooks = grep { !/post_sync_cleanup/ && !/post_snapshot/ } @hooks; # CloudLinux stuff @hooks = grep { !/\.l\.v\.e-manager\.bak/ } @hooks; my @custom_hooks; my $comment_pattern = '\s*(?:\#.*)?'; my $cl_post_pattern = '(?:\.(?:pl|py|sh))?(?:\s+"\$@")?'; for my $hook (@hooks) { my $hook_file = '/scripts/' . $hook; next if -z $hook_file; if ( open my $hook_fd, '<', $hook_file ) { ## no critic (BriefOpen) my $is_custom; my $first_line = readline $hook_fd; if ( $first_line and $first_line !~ m{ \A #!/bin/bash $comment_pattern \Z }xms ) { $is_custom = 1; } while (<$hook_fd>) { next if m{ \A $comment_pattern \Z }xms; if ( SSP::Util::i_am('cloudlinux') ) { next if m{ \A ( \Q/usr/share/cagefs/cpanel/cagefs_\E $hook \Q_hook\E $cl_post_pattern | \Q/usr/share/l.v.e-manager/cpanel/hooks/\E (lve|l\.v\.e-) \Qmanager_\E $hook \Q_hook\E $cl_post_pattern | \Q/usr/share/lve/dbgovernor/cpanel/upgrade-mysql-disabler.sh\E ) $comment_pattern \Z }xms; if ( $hook eq 'postrestoreacct' ) { next if m{ \A ( \Q/usr/share/cagefs/cpanel/cagefs_postwwwacct_hook.pl\E \s+ user \s+ \"\$1\" ) $comment_pattern \Z }xms; } if ( $hook eq 'postkillacct' ) { next if m{ \A ( \Q/usr/share/lve/dbgovernor/cpanel/upgrade-mysql-disabler.sh\E ) $comment_pattern \Z }xms; } if ( $hook eq 'postupcp' ) { next if m{ \A ( \Q/usr/share/cloudlinux-linksafe/cpanel/hooks/cloudlinux_linksafe_hook.sh\E | \Q/usr/share/lve/dbgovernor/cpanel/upgrade-mysql-disabler.sh\E ) $comment_pattern \Z }xms; } if ( $hook eq 'prekillacct' ) { next if m{ \A ( \Q/usr/share/cagefs-plugins/hooks/terminate_cagefs_account\E $cl_post_pattern ) $comment_pattern \Z }xms; } } push @custom_hooks, $hook_file; last; } close $hook_fd; } } if ( scalar @custom_hooks ) { SSP::Util::print_warn('Custom Hooks: '); SSP::Util::print_warning( join( ' ', @custom_hooks ) ); } } sub check_for_huge_logs { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); # Default size is 2_100_000_000, with no additional help text. # Example: '/path/to/file' => { info_size => 500_000_000, warn_size => 1_000_000_000, help => 'If file is too hyooge, THERE IS MUCH FAIL' } my %logs = ( '/usr/local/apache/logs/access_log' => {}, '/usr/local/apache/logs/error_log' => {}, '/usr/local/apache/logs/mod_jk.log' => {}, '/usr/local/apache/logs/modsec_audit.log' => {}, '/usr/local/apache/logs/modsec_debug.log' => {}, '/usr/local/apache/logs/suexec_log' => {}, '/usr/local/apache/logs/suphp_log' => {}, '/var/cpanel/secdatadir/ip.pag' => { help => 'Apache using a lot of CPU for no good reason? Try moving aside and restarting Apache. See EA-4092.' }, '/var/cpanel/secdatadir/default_SESSION.pag' => { help => 'Apache using a lot of CPU for no good reason? Try moving aside and restarting Apache. See UPS-134.' }, '/var/named/data/named.run' => {}, '/var/cpanel/backups/metadata.sqlite' => { info_size => 1_000_000_000, warn_size => 10_000_000_000 }, '/var/cpanel/eximstats_db.sqlite3' => { warn_size => 5_000_000_000 }, ); for my $log ( keys(%logs) ) { if ( -e $log ) { my $size = ( stat($log) )[7]; my $info_size = exists( $logs{$log}->{info_size} ) ? $logs{$log}->{info_size} : undef; my $warn_size = exists( $logs{$log}->{warn_size} ) ? $logs{$log}->{warn_size} : 2_100_000_000; my $help = exists( $logs{$log}->{help} ) ? ' - ' . $logs{$log}->{help} : ''; my $print_size = sprintf( "%0.2fGB", $size / 1073741824 ); if ( $size > $warn_size ) { SSP::Util::print_warn('M-M-M-MONSTER LOG!: '); SSP::Util::print_warning( $log . ' (' . $print_size . ')' . $help ); } elsif ( $info_size and ( $size > $info_size ) ) { SSP::Util::print_info('Large Log: '); SSP::Util::print_normal( $log . ' (' . $print_size . ')' . $help ); } } } } sub check_easy_skip_cpanelsync { if ( -e '/var/cpanel/easy_skip_cpanelsync' ) { SSP::Util::print_warn('Touchfile: '); SSP::Util::print_warning('/var/cpanel/easy_skip_cpanelsync exists! '); } } sub check_pkgacct_override { if ( -d '/var/cpanel/lib/Whostmgr' ) { SSP::Util::print_warn('pkgacct override: '); SSP::Util::print_warning(' /var/cpanel/lib/Whostmgr exists, override may exist'); } } sub check_for_yunsuo { return unless SSP::Util::exists_process_cmd( qr{ yunsuo_agent }xms, 'root' ); SSP::Util::print_warn('yunsuo_agent process found: '); SSP::Util::print_warning('Can break account creation/termination.'); } sub check_for_gdm { return unless SSP::Util::exists_process_cmd( qr{ gdm }xms, 'root' ); SSP::Util::print_warn('gdm Process: '); SSP::Util::print_warning('is running'); } sub check_for_mcafee { return unless SSP::Util::exists_process_cmd( qr{ /opt/McAfee/ }xms, 'root' ); SSP::Util::print_warn('McAfee Process: '); SSP::Util::print_warning('is running. Can cause issues with permissions on /etc/shadow file'); } sub check_for_criu { return unless SSP::Util::exists_process_cmd( qr{ criu }xms, 'root' ); SSP::Util::print_warn('CRIU Process: '); SSP::Util::print_warning('running. Not recommended/supported due to stability issues. See: https://go.cpanel.net/criu'); } sub check_for_nsiv { my $nsiv_bin = '/usr/local/sbin/nsiv'; my $nsiv_dir = '/usr/local/nsiv'; return unless ( -x $nsiv_bin || -d $nsiv_dir ); SSP::Util::print_warn('NSIV: '); SSP::Util::print_warning('Network Socket Inode Validation is installed. Seeing PDNS killed and named started? This is probably why.'); } sub check_for_redhat_firewall { return if ( SSP::Util::i_am('ubuntu') ); return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); if ( SSP::Util::timed_run( 0, 'iptables', '-L', 'RH-Firewall-1-INPUT' ) ) { SSP::Util::print_warn('Default Redhat Firewall Check: '); SSP::Util::print_warning('RH-Firewall-1-INPUT table exists. /scripts/configure_rh_firewall_for_cpanel to open ports.'); } } sub check_mounts { my @mounts = split /\n/, SSP::Util::timed_run( 0, 'mount' ); return unless scalar @mounts; my $new_backup_conf = SSP::Util::get_new_backup_conf_href(); my $has_nfs = 0; my $has_fuse = 0; my $old_backups_dir; my $old_backups_dir_nfs; my $new_backups_dir; my $new_backups_dir_nfs; if ( defined $new_backup_conf and defined $new_backup_conf->{'BACKUPDIR'} and defined $new_backup_conf->{'BACKUPENABLE'} and $new_backup_conf->{'BACKUPENABLE'} =~ /yes/i ) { $new_backups_dir = $new_backup_conf->{'BACKUPDIR'}; $new_backups_dir .= '/' unless substr( $new_backups_dir, -1 ) eq '/'; } for my $mount (@mounts) { my $nfs_mount_path; if ( $mount =~ m{ on \s ([^\s]+) \s type \s nfs[34]? \s }xms ) { $nfs_mount_path = $1; $nfs_mount_path .= '/' unless substr( $nfs_mount_path, -1 ) eq '/'; $has_nfs = 1; if ( $old_backups_dir and index( $old_backups_dir, $nfs_mount_path ) == 0 ) { $old_backups_dir_nfs = 1; } if ( $new_backups_dir and index( $new_backups_dir, $nfs_mount_path ) == 0 ) { $new_backups_dir_nfs = 1; } } if ( $mount =~ m{on ([^\s]+) type fuse\.} ) { $has_fuse = 1; } if ( $mount =~ m{ \s on \s (/home([^\s]?)) \s (:?.*) noexec }xms ) { my $noexec_partition = $1; SSP::Util::print_warn('mounted noexec: '); SSP::Util::print_warning($noexec_partition); } } if ($has_nfs) { SSP::Util::print_warn('NFS: '); SSP::Util::print_warning('filesystem(s) with NFS detected.'); } if ($has_fuse) { SSP::Util::print_warn('FUSE: '); SSP::Util::print_warning('filesystem(s) with FUSE detected.'); } if ($old_backups_dir_nfs) { SSP::Util::print_warn('Backups: '); SSP::Util::print_warning("$old_backups_dir is NFS (used by old backup system)"); } if ($new_backups_dir_nfs) { SSP::Util::print_warn('Backups: '); SSP::Util::print_warning("$new_backups_dir is NFS (used by new backup system)"); } } ## compare external IP addr with local IP addrs, OR ## check if only internal IP addrs are bound to server (this is not as reliable, ## as NAT can still be used with external IP addrs of course) sub check_for_unsupported_nat { return if -e '/var/cpanel/cpnat'; my @local_ipaddrs = @{ SSP::Util::get_local_ipaddrs_aref() }; my @external_ipaddrs; my $external_ip_address = SSP::Util::get_external_ip(); if ( defined($external_ip_address) ) { if ( !grep { /$external_ip_address/ } @local_ipaddrs ) { SSP::Util::print_warn('NAT: '); SSP::Util::print_warning("external IP address $external_ip_address is not bound to server and /var/cpanel/cpnat does not exist"); } return; } for my $ipaddr (@local_ipaddrs) { # Matches any local IP (127.0.0.0/8) or RFC-1918 IP (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 if ( $ipaddr !~ /(?:10\.|127\.|172\.(?:1[6-9]|2[0-9]|3[0-1])\.|192\.168\.)/ ) { push @external_ipaddrs, $ipaddr; } } if ( !@external_ipaddrs ) { SSP::Util::print_warn('NAT: '); SSP::Util::print_warning('no external IP addresses detected'); } } sub check_for_oracle_linux { my $centos_5_oracle_release_file = '/etc/enterprise-release'; my $centos_6_oracle_release_file = '/etc/oracle-release'; if ( -f $centos_5_oracle_release_file ) { SSP::Util::print_warn('Oracle Linux: '); SSP::Util::print_warning("$centos_5_oracle_release_file detected!"); } elsif ( -f $centos_6_oracle_release_file ) { SSP::Util::print_warn('Oracle Linux: '); SSP::Util::print_warning("$centos_6_oracle_release_file detected!"); } } sub check_for_port_25_blocks { my @smtp_providers = qw( smtp.aol.com smtp.comcast.net smtp.netzero.net smtp.charter.net mx1.cpanel.net ); my $smtp_provider = $smtp_providers[ rand @smtp_providers ]; my $connection = IO::Socket::INET->new( PeerHost => $smtp_provider, PeerPort => '25', Proto => 'tcp', Timeout => '5', ); if ( !$connection ) { SSP::Util::print_warn('Port 25 Blocked: '); SSP::Util::print_warning( 'Could not connect outbound to ' . YELLOW $smtp_provider . ':25' . RED ' - provider may be blocking port' ); } } sub check_for_usr_local_cpanel_hooks { my $hooks; my $dir = '/usr/local/cpanel/hooks'; return unless -d $dir; my @usr_local_cpanel_hooks; # items in /usr/local/cpanel/hooks/ find( sub { my $file = $File::Find::name; if ( -f $file and $file !~ m{ ( README | \.example ) \z }xms ) { $file =~ s#/usr/local/cpanel/hooks/##; push @usr_local_cpanel_hooks, $file; } }, $dir ); # default CloudLinux hooks that can be ignored my %hooks_ignore = qw( 677da3bdd8fbd16d4b8917a9fe0f6f89 /usr/local/cpanel/hooks/addondomain/addaddondomain 677da3bdd8fbd16d4b8917a9fe0f6f89 /usr/local/cpanel/hooks/addondomain/deladdondomain 677da3bdd8fbd16d4b8917a9fe0f6f89 /usr/local/cpanel/hooks/subdomain/addsubdomain 677da3bdd8fbd16d4b8917a9fe0f6f89 /usr/local/cpanel/hooks/subdomain/delsubdomain 289b2b4c8b5293103def4557d3538060 /usr/local/cpanel/hooks/mysql/adduser 289b2b4c8b5293103def4557d3538060 /usr/local/cpanel/hooks/mysql/deluser ); for my $hook (@usr_local_cpanel_hooks) { my $tmp_hook = '/usr/local/cpanel/hooks/' . $hook; if ( -f $tmp_hook and not -z $tmp_hook ) { chomp( my $checksum = SSP::Util::timed_run( 0, 'md5sum', $tmp_hook ) ); $checksum =~ s/\s.*//g; next if exists $hooks_ignore{$checksum}; $hooks .= "$hook "; } } if ($hooks) { SSP::Util::print_warn("$dir: "); SSP::Util::print_warning($hooks); } } sub get_mysql_full_version { return unless my $mysql_output = SSP::Util::timed_run( 0, 'mysql', '-V' ); chomp $mysql_output; return $mysql_output; } sub get_mysql_numeric_version { return unless my $version = SSP::Util::get_mysql_full_version(); my $numeric; if ( $version =~ m{Distrib \s* ([0-9A-Za-z.]+)}xms ) { $numeric = $1; } return $numeric; } sub get_mysql_datadir { my $datadir = '/var/lib/mysql/'; my $mysql_conf = SSP::Util::get_mysql_conf_href(); if ( defined $mysql_conf and defined $mysql_conf->{'mysqld'} and defined $mysql_conf->{'mysqld'}{'datadir'} ) { $datadir = $mysql_conf->{'mysqld'}{'datadir'}[1]; if ( $datadir !~ m{ / \z }xms ) { $datadir .= '/'; } } return $datadir; } sub check_mysqld_warnings_errors { foreach my $mysql_err ( grep { m{\[(?:err)}i } split( /\n/, SSP::Util::timed_run_trap_stderr( 0, 'mysqld', '-u', 'mysql', '--help' ) ) ) { SSP::Util::print_warn('Database config errors: '); SSP::Util::print_warning($mysql_err); } } sub check_for_domain_forwarding { return unless SSP::Util::i_am('ea4'); my $domainfwdip = '/var/cpanel/domainfwdip'; if ( -f $domainfwdip and not -z $domainfwdip ) { SSP::Util::print_warn('Domain Forwarding: '); SSP::Util::print_warning("cat $domainfwdip to see what is being forwarded!"); } } sub check_for_empty_apache_templates { return unless SSP::Util::i_am('ea4'); return unless my $apache_version = SSP::Util::get_apache_version(); my $apache2_template_dir = '/var/cpanel/templates/apache2'; if ( SSP::Util::version_compare( $apache_version, qw( >= 2.4.0 ) ) ) { $apache2_template_dir = '/var/cpanel/templates/apache2_4'; } my @dir_contents; my $empty_templates; if ( -d $apache2_template_dir ) { opendir( my $dir_fh, $apache2_template_dir ); @dir_contents = grep { !/^\.\.?$/ } readdir $dir_fh; closedir $dir_fh; } if ( !@dir_contents ) { SSP::Util::print_warn('Apache templates: '); SSP::Util::print_warning("none found in $apache2_template_dir !"); } else { for my $template (@dir_contents) { if ( -z "$apache2_template_dir/$template" ) { $empty_templates .= "$template "; } } } if ($empty_templates) { SSP::Util::print_warn("Empty Apache templates in $apache2_template_dir (this can affect the ability to remove domains): "); SSP::Util::print_warning("$empty_templates"); } } sub check_for_empty_postgres_config { return if SSP::Util::i_am('dnsonly'); my $postgres_config = '/var/lib/pgsql/data/pg_hba.conf'; if ( -f $postgres_config and -z $postgres_config ) { SSP::Util::print_warn('Postgres config: '); SSP::Util::print_warning("$postgres_config is empty (install via WHM >> Postgres Config)"); } } sub check_for_proc_mdstat_recovery { my $mdstat = '/proc/mdstat'; my $recovery = 0; if ( open my $mdstat_fh, '<', $mdstat ) { while (<$mdstat_fh>) { if (/recovery/) { $recovery = 1; last; } } close $mdstat_fh; } if ( $recovery == 1 ) { SSP::Util::print_warn('Software RAID recovery: '); SSP::Util::print_warning("cat $mdstat to check the status"); } } sub check_usr_local_cpanel_path_for_symlinks { my @dirs = qw( /usr /usr/local /usr/local/cpanel ); for my $dir (@dirs) { if ( -l $dir ) { SSP::Util::print_warn('Directory is a symlink: '); SSP::Util::print_warning("$dir (this can cause Internal Server Errors for redirects like /cpanel, etc)"); } } } sub check_for_additional_rpms { return unless my $rpms = SSP::Util::get_rpm_href(); my @additional_rpms = grep { /^(php-|kde-|psa-|clamav|clamd|rrdtool)|(http|apache|pear|sendmail|libhttp)/ } keys( %{$rpms} ); @additional_rpms = grep { !/httpd-tools|^cpanel-|alt-php|apache-tomcat-apis|^ea-|^libnet|^liblwp|^libnghttp2|^libhttp-|httplib2/ } @additional_rpms; @additional_rpms = map { SSP::Util::get_printable_rpm_packages($_) } @additional_rpms; return unless @additional_rpms; print "\n"; SSP::Util::print_magenta('This is informational only. Unless these rpms/apts directly relate to an issue, they can be ignored:'); @additional_rpms = sort @additional_rpms; for my $rpm (@additional_rpms) { SSP::Util::print_start('Additional Package: '); SSP::Util::print_warning($rpm); } } sub check_for_perl_env_var { if ( exists( $ENV{'PERL5LIB'} ) ) { SSP::Util::print_warn('PERL5LIB env var: '); SSP::Util::print_warning('exists! This can break cPanel\'s perl.'); } if ( exists( $ENV{'PERL_LOCAL_LIB_ROOT'} ) ) { SSP::Util::print_warn('PERL_LOCAL_LIB_ROOT env var: '); SSP::Util::print_warning('exists! This can break cPanel\'s perl.'); } if ( exists( $ENV{'PERL_MB_OPT'} ) ) { SSP::Util::print_warn('PERL_MB_OPT env var: '); SSP::Util::print_warning('exists! This can break cPanel\'s perl.'); } if ( exists( $ENV{'PERL_MM_OPT'} ) ) { SSP::Util::print_warn('PERL_MM_OPT env var: '); SSP::Util::print_warning('exists! This can break cPanel\'s perl.'); } if ( grep { /\/perl5?\// } $ORIGINAL_PATH ) { SSP::Util::print_warn('Custom perl in PATH env var: '); SSP::Util::print_warning('exists! This can break cPanel\'s perl or cause other unexpected system-perl behavior.'); } } sub check_for_system_mem_below_required { my $meminfo = SSP::Util::get_meminfo(); # Calculations have a 64-96MB fudge factor for overhead return unless defined( $meminfo->{installed} ) && $meminfo->{installed} =~ /[0-9]+/; my $memtotal = int( $meminfo->{installed} / 1024 ); my $memmin = 704; # 768 - 64 my $memmintext = "768MB"; if ( SSP::Util::os_version_is(qw( >= 7 )) ) { $memmin = 928; # 1024 - 96 $memmintext = "1024MB"; } if ( SSP::Util::os_version_is(qw( >= 8 )) ) { ## CX-1052 $memmin = 1920; # 2048 - 128 $memmintext = "2048MB"; } if ( $memtotal < $memmin ) { SSP::Util::print_warn('Memory: '); SSP::Util::print_warning( "Server has less than ${memmintext} installed memory! [ " . SSP::Util::format_meminfo( $meminfo->{installed} ) . " ]" ); } #TECH-74 recommends >= 1024MB swap on systems with <= 1024MB RAM return if $memtotal > 928; my $swaptotal = 0; $swaptotal = int( $meminfo->{swapinstalled} / 1024 ) if defined( $meminfo->{swapinstalled} ) && $meminfo->{swapinstalled} =~ /[0-9]+/; my $swapmin = 928; my $swapmintext = "1024MB"; if ( $swaptotal < $swapmin ) { SSP::Util::print_warn('Memory: '); SSP::Util::print_warning( "Server has less than ${swapmintext} swap! [ " . SSP::Util::format_meminfo( $meminfo->{swapinstalled} ) . " ]" ); } } sub check_apt_conf { return unless ( SSP::Util::i_am('ubuntu') ); my @APT_CONF_DEFAULTS; if ( SSP::Util::os_version_is(qw( == 20.04)) ) { @APT_CONF_DEFAULTS = qw( 01autoremove 01-vendor-ubuntu 10periodic 15update-stamp 20apt-esm-hook.conf 20archive 20auto-upgrades 20packagekit 20snapd.conf 50apt-file.conf 50command-not-found 50extracttemplates 50unattended-upgrades 70debconf 99needrestart 99update-notifier apt-universal-hooks.conf ); } else { ## Ubuntu 22.04 @APT_CONF_DEFAULTS = qw( 01autoremove 01-vendor-ubuntu 20auto-upgrades 20packagekit 20snapd.conf 50apt-file.conf 50extracttemplates 50unattended-upgrades 70debconf 99needrestart apt-universal-hooks.conf ); } foreach my $apt_conf_default (@APT_CONF_DEFAULTS) { chomp($apt_conf_default); if ( !-e "/etc/apt/apt.conf.d/$apt_conf_default" ) { SSP::Util::print_warn('APT:'); SSP::Util::print_warning(" /etc/apt/apt.conf.d/$apt_conf_default is missing!"); } if ( -z "/etc/apt/apt.conf.d/$apt_conf_default" ) { SSP::Util::print_warn('APT:'); SSP::Util::print_warning(" /etc/apt/apt.conf.d/$apt_conf_default is empty!"); } } my $exclude_kernel; my $exclude_wget; my $exclude_perl; my $apt_held = SSP::Util::timed_run( 0, 'apt-mark', 'showhold' ); my @heldapts = split /\n/, $apt_held; foreach my $apt_marked_held (@heldapts) { if ( $apt_marked_held =~ m{ \A \s* kernel }xmsi ) { $exclude_kernel = 1; } if ( $apt_marked_held =~ m{ \A \s* wget }xmsi ) { $exclude_wget = 1; } if ( $apt_marked_held =~ m{ \A \s* perl }xmsi ) { $exclude_perl = 1; } } if ($exclude_kernel) { SSP::Util::print_warn('APT:'); SSP::Util::print_warning(' may be excluding kernel updates!'); } if ($exclude_wget) { SSP::Util::print_warn('APT:'); SSP::Util::print_warning(' may be excluding wget updates!'); } if ($exclude_perl) { SSP::Util::print_warn('APT:'); SSP::Util::print_warning(' may be excluding system perl updates!'); } } sub check_yum_conf { return check_apt_conf() if ( SSP::Util::i_am('ubuntu') ); ( $YUM_OR_DNF, $YUM_CONF ) = SSP::Util::os_version_is(qw( >= 8)) ? ( 'dnf', '/etc/dnf/dnf.conf' ) : ( 'yum', '/etc/yum.conf' ); $YUM_OR_DNF = ( SSP::Util::i_am('ubuntu') ) ? "apt" : $YUM_OR_DNF; my $exclude_line_count = 0; my $exclude_kernel; my $exclude_wget; my $exclude_perl; my $distroverpkg_cloudlinux; my $plugins_enabled; # Default is disabled, needs to be explicitly enabled. my $repo_gpgcheck_enabled; my $assumeyes_enabled; my $remove_leaf_only_enabled; my $exclude_ea; if ( !-e $YUM_CONF ) { SSP::Util::print_warn( uc $YUM_OR_DNF . ': ' ); SSP::Util::print_warning( $YUM_CONF . ' is missing!' ); return; } elsif ( -z $YUM_CONF ) { SSP::Util::print_warn( uc $YUM_OR_DNF . ': ' ); SSP::Util::print_warning( $YUM_CONF . ' is empty!' ); return; } if ( open my $file_fh, '<', $YUM_CONF ) { ## no critic (BriefOpen) while (<$file_fh>) { if (m{ \A \s* exclude }xmsi) { $exclude_line_count++; } if (m{ \A \s* exclude .* kernel }xmsi) { $exclude_kernel = 1; } if (m{ \A \s* exclude .* wget }xmsi) { $exclude_wget = 1; } if (m{ \A \s* exclude .* perl }xmsi) { $exclude_perl = 1; } if (m{ \A \s* exclude .* ea- }xmsi) { $exclude_ea = 1; } if (m{ \A \s* distroverpkg \s* = \s* cloudlinux-release }xmsi) { $distroverpkg_cloudlinux = 1; } if (m{ \A \s* plugins \s* = \s* 1 \s* \Z }xmsi) { $plugins_enabled = 1; } if (m{ \A \s* repo_gpgcheck \s* = \s* 1 \s* \Z }xmsi) { $repo_gpgcheck_enabled = 1; } if (m{ \A \s* assumeyes \s* = \s* 1 \s* \Z }xmsi) { $assumeyes_enabled = 1; } if (m{ \A \s* remove_leaf_only \s* = \s* 1 \s* \Z }xmsi) { $remove_leaf_only_enabled = 1; } } close $file_fh; } if ( $exclude_line_count > 1 ) { SSP::Util::print_warn( uc $YUM_OR_DNF . ': ' ); SSP::Util::print_warning( $YUM_CONF . ' contains multiple "exclude" lines!' ); } if ($exclude_kernel) { SSP::Util::print_warn( uc $YUM_OR_DNF . ': ' ); SSP::Util::print_warning( $YUM_CONF . ' may be excluding kernel updates!' ); } if ($exclude_wget) { SSP::Util::print_warn( uc $YUM_OR_DNF . ': ' ); SSP::Util::print_warning( $YUM_CONF . ' may be excluding wget updates!' ); } if ($exclude_perl) { SSP::Util::print_warn( uc $YUM_OR_DNF . ': ' ); SSP::Util::print_warning( $YUM_CONF . ' may be excluding system perl updates!' ); } if ($exclude_ea) { SSP::Util::print_warn( uc $YUM_OR_DNF . ': ' ); SSP::Util::print_warning( $YUM_CONF . ' may be excluding ea-* updates!' ); } if ($distroverpkg_cloudlinux) { SSP::Util::print_warn( uc $YUM_OR_DNF . ': ' ); SSP::Util::print_warning( $YUM_CONF . ' has distroverpkg=cloudlinux-release set! This is known to cause issues with installing EA4.' ); } unless ($plugins_enabled) { SSP::Util::print_warn( uc $YUM_OR_DNF . ': ' ) unless ( SSP::Util::i_am('dnsonly') ); SSP::Util::print_warning( 'plugins=1 not found in ' . $YUM_CONF . '. If plugins are disabled it can cause issues with RHEL/CloudLinux and EA4 updates.' ) unless ( SSP::Util::i_am('dnsonly') ); } if ($repo_gpgcheck_enabled) { SSP::Util::print_warn( uc $YUM_OR_DNF . ': ' ); SSP::Util::print_critical( 'repo_gpgcheck=1 found in ' . $YUM_CONF . '. GPG Armor not yet supported by RHEL, ' . $YUM_OR_DNF . ' update may fail with 404 error on repomd.xml.asc.' ); } if ($assumeyes_enabled) { SSP::Util::print_warn( uc $YUM_OR_DNF . ': ' ); SSP::Util::print_critical( 'assumeyes=1 found in ' . $YUM_CONF . '. Be careful, ' . $YUM_OR_DNF . ' will NOT ask to proceed before performing an action. Use "--assumeno" option to override.' ); } if ($remove_leaf_only_enabled) { SSP::Util::print_warn( uc $YUM_OR_DNF . ': ' ); SSP::Util::print_warning( 'remove_leaf_only=1 found in ' . $YUM_CONF . '. This is known to cause issues with EA4 provisioning.' ); } } sub check_yum_plugins { return if ( SSP::Util::i_am('ubuntu') ); my $plugin_dir = '/etc/yum/pluginconf.d/'; my $univhooksfile = SSP::Util::os_version_is(qw ( >= 8 )) ? "universal_hooks.conf" : "universal-hooks.conf"; my %plugins = ( 'ea4' => { $univhooksfile => { msg => 'This can break MultiPHP, see TECH-655', }, }, 'cloudlinux' => { 'rhnplugin.conf' => { msg => 'This breaks CloudLinux updates', }, }, ); $YUM_OR_DNF = SSP::Util::os_version_is(qw( >= 8)) ? 'dnf' : 'yum'; $YUM_OR_DNF = ( SSP::Util::i_am('ubuntu') ) ? "apt" : $YUM_OR_DNF; if ( !-e $plugin_dir ) { SSP::Util::print_crit( uc $YUM_OR_DNF . ': ' ); SSP::Util::print_critical( $plugin_dir . ' does not exist! This can break MultiPHP, see TECH-655' ); return; } foreach my $type ( keys %plugins ) { next unless SSP::Util::i_am($type); foreach my $plugin ( keys %{ $plugins{$type} } ) { if ( !-e $plugin_dir . $plugin || -z $plugin_dir . $plugin ) { return if ( SSP::Util::os_version_is(qw( >= 8)) && SSP::Util::i_am_one_of( 'cloudlinux', 'almalinux', 'clsolo' ) ); SSP::Util::print_warn( uc $YUM_OR_DNF . ': ' ); SSP::Util::print_warning( $plugin_dir . $plugin . ' is either empty or missing! ' . $plugins{$type}{$plugin}{'msg'} ); } elsif ( open my $fh, '<', $plugin_dir . $plugin ) { while (<$fh>) { $plugins{$type}{$plugin}{'enabled'} = 1 if (m/ \A \s* enabled \s* = \s* 1 \s* \Z /xmsi); } close($fh); return if ( SSP::Util::os_version_is(qw( >= 8)) && SSP::Util::i_am_one_of( 'cloudlinux', 'almalinux', 'clsolo' ) ); if ( !$plugins{$type}{$plugin}{'enabled'} ) { SSP::Util::print_warn( uc $YUM_OR_DNF . ': ' ); SSP::Util::print_warning( $plugin_dir . $plugin . ' is not enabled! ' . $plugins{$type}{$plugin}{'msg'} ); } } } } } sub check_for_cpanel_files { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); my @files = qw( /usr/local/cpanel/cpanel /usr/local/cpanel/cpsrvd ); for my $file (@files) { if ( !-e $file ) { SSP::Util::print_warn('Critical file missing: '); SSP::Util::print_warning("$file"); } } } sub check_bash_history_for_certain_commands { my $bash_history = '/root/.bash_history'; my %history_commands = (); my $commands; if ( -l $bash_history ) { my $link = readlink $bash_history; SSP::Util::print_warn("$bash_history: "); SSP::Util::print_warning("is a symlink! Linked to $link"); } elsif ( -f $bash_history ) { if ( open my $history_fh, '<', $bash_history ) { while (<$history_fh>) { if (/chattr/) { $history_commands{'chattr'} = 1; } if (/chmod/) { $history_commands{'chmod'} = 1; } if (/openssl(?:.*)\.tar/) { $history_commands{'openssl*.tar'} = 1; } } close $history_fh; } } if (%history_commands) { while ( my ( $key, $value ) = each(%history_commands) ) { $commands .= "[$key] "; } SSP::Util::print_warn("$bash_history commands found: "); SSP::Util::print_warning($commands); } } sub check_roots_cron_for_certain_commands { my %missing = (); my %warning = (); return unless my $crons_aref = SSP::Util::get_cron_files(); for my $cron (@$crons_aref) { next if ( $cron =~ m{ /( freshclam | kill_orphaned_php-cron | lvedbgovernor-utils-cron | makewhatis\.cron | maldet | man-db | man-db.cron | modsecparse\.pl | rpm | apt | apport | popularity-contest ) $ }x ); # Common false-positives to filter out entirely. if ( open my $cron_fh, '<', $cron ) { while (<$cron_fh>) { if (m{ \A [^#]+ /usr/local/cpanel/(bin/backup|scripts/upcp) \s }xms) { $missing{$1} = 1; } if (m{ \A [^#]* (?:^|\s|\/)(rm|unlink|ch(?:mod|own|attr)|(?:p|s)?kill(?:all)?5?|tmpwatch|cpkeyclt)\s }x) { $warning{$cron}{$1} = 1; } } close $cron_fh; } } if ( !exists $missing{'bin/backup'} ) { SSP::Util::print_warn("cron: "); SSP::Util::print_warning("cPanel backup cron is missing (/usr/local/cpanel/bin/backup)"); } if ( !exists $missing{'scripts/upcp'} ) { SSP::Util::print_warn("cron: "); SSP::Util::print_warning("cPanel update cron is missing (/usr/local/cpanel/scripts/upcp)"); } if (%warning) { for my $cron ( keys(%warning) ) { SSP::Util::print_warn("cron: "); SSP::Util::print_warning( $cron . " contains [ " . join( ' ', sort( keys( %{ $warning{$cron} } ) ) ) . " ]" ); } } } sub check_for_missing_or_commented_customlog { return unless SSP::Util::i_am('ea4'); return unless my $apache_version = SSP::Util::get_apache_version(); my $commented_templates; my $missing_customlog_templates; my $httpdconf = SSP::Util::i_am('ea4') ? '/etc/apache2/conf/httpd.conf' : '/usr/local/apache/conf/httpd.conf'; my $httpdconf_commented_customlog; my $httpdconf_customlog_exists; my $templates_dir = '/var/cpanel/templates/apache2'; if ( SSP::Util::version_compare( $apache_version, qw( >= 2.4.0 ) ) ) { $templates_dir = '/var/cpanel/templates/apache2_4'; } my %templates = ( 'ea4_main.default' => 0, 'ea4_main.local' => 0, 'main.default' => 0, 'main.local' => 0, 'vhost.default' => 0, 'vhost.local' => 0, 'ssl_vhost.default' => 0, 'ssl_vhost.local' => 0, ); for my $template ( keys %templates ) { my $template_full_path = $templates_dir . '/' . $template; if ( -f $template_full_path ) { if ( open my $template_fh, '<', $template_full_path ) { while (<$template_fh>) { if (/#(?:\s+)?CustomLog\s/i) { $commented_templates .= "$template_full_path "; $templates{$template} = 1; last; } elsif (/CustomLog\s/i) { $templates{$template} = 1; } } close $template_fh; } } } while ( my ( $template, $value ) = each(%templates) ) { if ( $value == 0 and -f "$templates_dir/$template" ) { $missing_customlog_templates .= "$templates_dir/$template "; } } if ( open my $httpdconf_fh, '<', $httpdconf ) { local $/ = undef; my $httpdconf_txt = readline($httpdconf_fh); close $httpdconf_fh; if ( $httpdconf_txt =~ m/\n[\t ]*#[\t ]*CustomLog\s/si ) { $httpdconf_commented_customlog = 1; } if ( $httpdconf_txt =~ m/\n[\t ]*CustomLog\s/si ) { $httpdconf_customlog_exists = 1; } } if ($httpdconf_commented_customlog) { $commented_templates .= ' httpd.conf'; } elsif ( !$httpdconf_customlog_exists ) { $missing_customlog_templates .= ' httpd.conf'; } if ($commented_templates) { SSP::Util::print_warn('CustomLog commented out: '); SSP::Util::print_warning($commented_templates); } if ($missing_customlog_templates) { SSP::Util::print_warn('CustomLog entries missing: '); SSP::Util::print_warning($missing_customlog_templates); } } sub check_for_cpsources_conf { my $cpsources_conf = '/etc/cpsources.conf'; return unless -f $cpsources_conf and not -z $cpsources_conf; SSP::Util::print_warn('/etc/cpsources.conf: '); SSP::Util::print_warning('exists! This can affect upcp and EA.'); my @cpsources_servers; if ( open( my $cpsources_conf_fh, '<', $cpsources_conf ) ) { while (<$cpsources_conf_fh>) { my ( $key, $value ) = split( '=', $_ ); if ( $key eq 'HTTPUPDATE' ) { push @cpsources_servers, $value; } } close $cpsources_conf_fh; } foreach my $cpsources_server (@cpsources_servers) { chomp $cpsources_server; my ( $host, $port ) = split( ':', $cpsources_server ); if ( !defined($port) ) { $port = 80; } my $tiers_data = SSP::Util::_http_get( Host => $host, Port => $port, Path => '/cpanelsync/TIERS', WantHeaders => 1 ); if ( !$tiers_data ) { SSP::Util::print_warn('/etc/cpsources.conf: '); SSP::Util::print_warning("Unresponsive mirror: http://$host:$port"); next; } ( my $http_response ) = split( '\n', $tiers_data ); ($http_response) = split( '\r', $http_response ); if ( $http_response =~ /^HTTP\/(\d*?)\.(\d*?) (\d\d\d) (.*?)$/ ) { if ( $3 != 200 ) { SSP::Util::print_warn('/etc/cpsources.conf: '); SSP::Util::print_warning("Server $host:$port responded with [ $http_response ] when fetching TIERS file."); } } else { SSP::Util::print_warn('/etc/cpsources.conf: '); SSP::Util::print_warning("Server $host:$port responds with weird HTTP response [ $http_response ]"); } } } sub check_for_apache_rlimits { return unless SSP::Util::i_am('ea4'); my $httpdconf = '/etc/apache2/conf/httpd.conf'; my ( $rlimitmem, $rlimitcpu ); my $output; if ( open my $httpdconf_fh, '<', $httpdconf ) { while (<$httpdconf_fh>) { if (m{\A \s* RLimitMEM \s+ (\d+)}xmsi) { $rlimitmem = $1; } if (m{\A \s* RLimitCPU \s+ (\d+)}xmsi) { $rlimitcpu = $1; } last if m{\A \s* {'security_module'} or defined $modules->{'security2_module'}; my $conf_prefix = SSP::Util::i_am('ea4') ? '/etc/apache2/conf.d/' : '/usr/local/apache/conf/'; my $modsec_prefix = SSP::Util::i_am('ea4') ? 'modsec/' : ''; my $modsec2_conf = $conf_prefix . 'modsec2.conf'; my $modsec2_user_conf = $conf_prefix . $modsec_prefix . 'modsec2.user.conf'; my $modsec_rules_dir = $conf_prefix . $modsec_prefix . 'modsec_rules'; if ( my $modsec2_conf_size = -s $modsec2_conf ) { my $modsec2_conf_max_size = 1617; if ( $modsec2_conf_size > $modsec2_conf_max_size ) { SSP::Util::print_warn('Mod Security: '); SSP::Util::print_warning("$modsec2_conf is > $modsec2_conf_max_size bytes, may contain custom rules"); } } if ( -s $modsec2_user_conf ) { SSP::Util::print_warn('Mod Security: '); SSP::Util::print_warning("$modsec2_user_conf is not empty, may contain custom rules"); } if ( -d $modsec_rules_dir ) { SSP::Util::print_warn('Mod Security: '); SSP::Util::print_warning("$modsec_rules_dir exists, 3rd-party rules may be in use"); } } sub check_etc_hosts_sanity { my $hosts = '/etc/hosts'; my ( $localhost, $httpupdate, $localhost_not_127, $hostname_entry ) = ( 0, 0, 0, 0 ); my $hostname = SSP::Util::get_hostname(); if ( !-f $hosts ) { SSP::Util::print_warn("$hosts: "); SSP::Util::print_warning('missing!'); return; } if ( open my $hosts_fh, '<', $hosts ) { while ( my $line = <$hosts_fh> ) { chomp $line; next if ( $line =~ /^(\s+)?#/ ); if ( $line =~ m{ 127\.0\.0\.1 (.*) localhost }xms ) { $localhost = 1; } if ( ( $line =~ m{ \s localhost (\s|\z) }xmsi ) and ( $line !~ m{ 127\.0\.0\.1 | ::1 }xms ) ) { $localhost_not_127 = 1; } if ( $line =~ m{ httpupdate\.cpanel\.net }xmsi ) { $httpupdate = 1; } if ( $line =~ m{ $hostname }xmsi ) { $hostname_entry = 1; } } close $hosts_fh; } if ( !$localhost ) { SSP::Util::print_warn("$hosts: "); SSP::Util::print_warning('no entry for localhost, or commented out'); } if ($httpupdate) { SSP::Util::print_warn("$hosts: "); SSP::Util::print_warning('contains an entry for httpupdate.cpanel.net'); } if ($localhost_not_127) { SSP::Util::print_warn("$hosts: "); SSP::Util::print_warning('contains an entry for "localhost" that isn\'t 127.0.0.1! This can break webmail logins'); } if ( !$hostname_entry ) { SSP::Util::print_warn("$hosts: "); SSP::Util::print_warning("no entry found for the server's hostname! [$hostname] (Can break Apache when mod_unique_id is enabled)"); } } sub check_for_valid_resolv_conf { my @valid_config = qw( nameserver search sortlist options ); return unless ( open( my $fh, '<', '/etc/resolv.conf' ) ); while (<$fh>) { chomp($_); next if ( substr( $_, 0, 1 ) =~ m{^(#|;|$)} ); my ($config) = ( split( /\s+/, $_ ) )[0]; next if ( grep { /$config/ } @valid_config ); SSP::Util::print_warn("/etc/resolv.conf: "); SSP::Util::print_warning( "contains invalid configuration option: " . MAGENTA $_ . RED " - See man resolv.conf" ); } close($fh); } sub check_localhost_resolution { return if $OPT_SKIP_NETWORKING; # At this time we only require localhost to resolve to "127.0.0.1", but "::1" is accounted for my $print_check; my $found_127_forward; my @localhost = SSP::Util::_resolve( 'localhost', 1 ); foreach my $addr (@localhost) { unless ( $addr eq '127.0.0.1' or $addr eq '::1' ) { SSP::Util::print_warn('Resolver: '); SSP::Util::print_warning( 'returned unexpected address [ ' . $addr . ' ] (out of ' . scalar @localhost . ' total addresses) for "localhost"!' ); $print_check = 1; } $found_127_forward = 1 if $addr eq '127.0.0.1'; } unless ($found_127_forward) { SSP::Util::print_warn('Resolver: '); SSP::Util::print_warning('did not return expected "127.0.0.1" when resolving "localhost"!'); $print_check = 1; } # Check 127.0.0.1 -> name (may be "localhost.domain") -> 127.0.0.1 match my $found_127_reverse; my @reverse = SSP::Util::_resolve( '127.0.0.1', 1 ); foreach my $host (@reverse) { unless ( $host =~ '^localhost' ) { SSP::Util::print_warn('Resolver: '); SSP::Util::print_warning( 'returned unexpected name [ ' . $host . ' ] (out of ' . scalar @reverse . ' total names) when resolving "127.0.0.1"!' ); $print_check = 1; } my @forward = SSP::Util::_resolve( $host, 1 ); foreach my $addr (@forward) { unless ( $addr eq '127.0.0.1' or $addr eq '::1' ) { SSP::Util::print_warn('Resolver: '); SSP::Util::print_warning( '"127.0.0.1" resolved to [ ' . $host . ' ] (out of ' . scalar @forward . ' total addresses) which resolved to unexpected localhost address [ ' . $addr . ' ]!' ); $print_check = 1; } $found_127_reverse = 1 if $addr eq '127.0.0.1'; } } unless ($found_127_reverse) { SSP::Util::print_warn('Resolver: '); SSP::Util::print_warning('No full reverse path for "127.0.0.1" found! ( 127.0.0.1 -> localhost -> 127.0.0.1 )'); $print_check = 1; } if ($print_check) { SSP::Util::print_warn('Resolver: '); SSP::Util::print_warning('Check /etc/{hosts,host.conf,nsswitch.conf,resolv.conf} for sanity. Localhost resolution problems could cause errant behavior.'); } } sub check_for_apache_listen_host_is_localhost { return unless SSP::Util::i_am('ea4'); return if not defined $CPCONF{'apache_port'}; my $apache_setting = $CPCONF{'apache_port'}; $apache_setting =~ s/:.*//g; if ( $apache_setting eq '127.0.0.1' ) { SSP::Util::print_warn('Apache listen host: '); SSP::Util::print_warning('Apache may only be listening on 127.0.0.1'); } } sub check_roundcube_mysql_pass_mismatch { return unless SSP::Util::i_am('cpanel'); return if ( defined $CPCONF{'roundcube_db'} and $CPCONF{'roundcube_db'} ne 'mysql' ); my $roundcubepass; my $rc_mysql_pass; return unless open my $rc_pass_fh, '<', '/var/cpanel/roundcubepass'; while (<$rc_pass_fh>) { chomp( $roundcubepass = $_ ); } close $rc_pass_fh; return unless open my $db_inc_fh, '<', '/usr/local/cpanel/base/3rdparty/roundcube/config/db.inc.php'; while (<$db_inc_fh>) { if (m{ \A \$rcmail_config\['db_dsnw'\] \s = \s 'mysql://roundcube:(.*)\@(?:.*)/roundcube'; }xms) { $rc_mysql_pass = $1; } } close $db_inc_fh; return if ( not $roundcubepass or not $rc_mysql_pass ); if ( $roundcubepass ne $rc_mysql_pass ) { SSP::Util::print_warn('RoundCube: '); SSP::Util::print_warning('password mismatch [/var/cpanel/roundcubepass] [/usr/local/cpanel/base/3rdparty/roundcube/config/db.inc.php]'); } } sub check_for_hooks_from_manage_hooks { return unless my $hooks_list = SSP::Util::timed_run( 0, '/usr/local/cpanel/bin/manage_hooks', 'list' ); my ( @hooks_tmp, @hooks ); foreach ( split( /\n/, $hooks_list ) ) { if (/hook: (.*)/) { # tidyoff - Ignore default Attracta hooks next if ( $1 =~ /CCSHooks|NginxHooks|attracta|Attracta|wp-toolkit|\/usr\/local\/cpanel\/scripts|cloudlinux|\/scripts\/|cagefs|l.v.e-manager|libexec/ ); push @hooks_tmp, "$1 "; } } for my $hook (@hooks_tmp) { push @hooks, $hook; } my @unique = do { my %seen; grep { !$seen{$_}++ } @hooks }; @hooks = sort(@unique); if ( scalar @hooks > 0 ) { SSP::Util::print_warn("Hooks in /var/cpanel/hooks/data:\n"); for my $hook (@hooks) { SSP::Util::print_magenta("\t \\_ $hook"); } } } sub check_mysql_config { return unless my $mysql_conf = SSP::Util::get_mysql_conf_href(); return unless defined $mysql_conf->{'mysqld'}; my $meminfo = SSP::Util::get_meminfo(); # Example: 'optionwithoutdashesorunderscores' => { default => 'defaultvalue', check_missing => 1, orig_name => 'option_with_underscores-or-dashes', help => '- Help text' }, # default, check_missing, and help are optional, but orig_name should be defined if check_missing is used so that its name can be properly printed # If check_missing exists, a warning is generated if the item is NOT in my.cnf and does not match the default (if given) my %mysqld_checks = ( 'datadir' => { default => '/var/lib/mysql' }, 'innodbforcerecovery' => { default => '0', help => 'Makes all InnoDB databases read-only, will break database upgrades.' }, 'logerror' => { default => '/var/lib/mysql/' . SSP::Util::get_hostname() . '.err' }, 'lowercasetablenames' => { default => '0', help => 'Will break space usage reporting of databases with mixed-case names. See CPANEL-8453.' }, 'oldpasswords' => { default => '0', help => 'Not recommended, non-native passwords are incompatible with future versions.' }, 'skipnameresolve' => { help => 'Seeing "Can\'t find any matching row"? That may be why.' }, 'skipnetworking' => { help => 'Webmail or other database related items not functioning properly? That may be why.' }, 'sqlmode' => { help => 'Seeing "Field \'ssl_cipher\' doesn\'t have a default value"? That may be why.' } ); if ( defined( $meminfo->{memtotal} ) ) { my $mem_mb = int( $meminfo->{memtotal} / 1024 ); # Systems w/2GB RAM report around 1800MB total after overhead, so 1700 should be a good tipping point if ( $mem_mb < 1700 && SSP::Util::version_compare( $CPCONF{'mysql-version'}, qw( >= 5.6 ) ) ) { $mysqld_checks{'performanceschema'} = { default => '0', check_missing => 1, orig_name => "performance_schema", help => 'performance_schema is enabled by default and could use a significant amount of memory, recommend "performance_schema = 0" with less than 2GB RAM [detected ' . $mem_mb . 'MB].' }; } } for my $check ( sort( keys(%mysqld_checks) ) ) { $mysqld_checks{$check}->{default} = "" unless defined( $mysqld_checks{$check}->{default} ); $mysqld_checks{$check}->{check_missing} = 0 unless defined( $mysqld_checks{$check}->{check_missing} ); $mysqld_checks{$check}->{help} = "" unless defined( $mysqld_checks{$check}->{help} ); my $help = $mysqld_checks{$check}->{help} ? ' - ' . $mysqld_checks{$check}->{help} : ''; if ( defined( $mysql_conf->{'mysqld'}{$check} ) && !( $mysql_conf->{'mysqld'}{$check}[1] eq $mysqld_checks{$check}->{default} ) ) { SSP::Util::print_warn("Database $MYSQL_CONF_FILE: "); if ( $mysql_conf->{'mysqld'}{$check}[1] eq "enabled" ) { SSP::Util::print_warning("[ $mysql_conf->{'mysqld'}{$check}[0] ] found $help"); } else { SSP::Util::print_warning("[ $mysql_conf->{'mysqld'}{$check}[0] = $mysql_conf->{'mysqld'}{$check}[1] ] $help"); } } elsif ( $mysqld_checks{$check}->{check_missing} && !defined( $mysql_conf->{'mysqld'}{$check} ) ) { my $optname = defined( $mysqld_checks{$check}->{orig_name} ) ? $mysqld_checks{$check}->{orig_name} : $check; SSP::Util::print_warn("Database $MYSQL_CONF_FILE: "); SSP::Util::print_warning("[ $optname ] not found. $help"); } } } sub check_mysql_datadir { return unless SSP::Util::os_version_is(qw( >= 7 )); return unless my $mysql_numeric_version = SSP::Util::get_mysql_numeric_version(); return unless SSP::Util::version_compare( $mysql_numeric_version, qw( >= 10.1.16 ) ); my $datadir = SSP::Util::get_mysql_datadir(); my $bad_path_regex = '^\/(home|usr|etc|boot)(\/|\s*$)'; if ( defined $datadir and $datadir =~ m/$bad_path_regex/ ) { SSP::Util::print_warn('Database: '); SSP::Util::print_warning("$MYSQL_CONF_FILE datadir points to a systemd protected directory which can prevent database servers (MariaDB/MySQL) from starting. See CPANEL-15633."); } if ( -l '/var/lib/mysql' and readlink('/var/lib/mysql') =~ m/$bad_path_regex/ ) { SSP::Util::print_warn('Database: '); SSP::Util::print_warning('/var/lib/mysql symlink resolves to a systemd protected directory which can prevent MariaDB 10.1.16+ from starting. See CPANEL-15633.'); } } sub check_for_extra_mysql_config_files { # It's silly how many locations mysqld looks for a configuration file. # These locations are reported by mysqld --help and by looking through /usr/bin/mysqld_safe code my @extra_locations = qw( /etc/mysql/my.cnf /usr/my.cnf /usr/etc/my.cnf /var/lib/mysql/my.cnf ); my @found_locations = grep { -f $_ } @extra_locations; return if !@found_locations; SSP::Util::print_warn('Database - extra my.cnf files found: '); SSP::Util::print_warning( '[ ' . join( " ", @found_locations ) . ' ]' ); SSP::Util::print_warning(" \\_ These may replace or be merged with $MYSQL_CONF_FILE settings!"); } sub check_cpanel_config { return unless keys(%CPCONF); # Example: 'exact_option_name' => { default => 'defaultvalue', check_missing => 1, help => '- Help text' }, # default, check_missing, and help are optional # If check_missing exists, a warning is generated if the item is NOT in cpanel.config and does not match the default (if given) or is empty my %cpanel_checks = ( 'enablecompileroptimizations' => { default => '0', help => 'Tweak setting "Enable optimizations for the C compiler" enabled. If Sandy Bridge CPU, problems MAY occur.' }, 'ftpserver' => { check_missing => 1 }, 'mailserver' => { check_missing => 1 }, 'mysql-version' => { check_missing => 1 }, 'nativessl' => { default => '1', help => 'Native SSL support for WHM/cPanel services is disabled' }, 'pma_disableis' => { default => '0', help => 'Can cause various issues with phpMyAdmin displaying databases -- see CPANEL-16866, CPANEL-16867, CPANEL-18742.' }, 'root' => { default => '/usr/local/cpanel', help => 'An invalid root setting can cause WHM to fail to start.' }, 'skiphttpauth' => { default => '1', help => 'HTTP auth enabled' }, 'skipparentcheck' => { default => '0', help => 'Allows other applications to run the cPanel and admin binaries' }, 'acls' => { default => '0', help => 'Can cause issues with FileProtect -- see CPANEL-19578' }, ); if ( defined $CPCONF{'maxmem'} && $CPCONF{'maxmem'} < 512 ) { $cpanel_checks{'maxmem'} = { default => '512', help => '< 512M, phpmyadmin may fail' }; } for my $check ( sort( keys(%cpanel_checks) ) ) { $cpanel_checks{$check}->{check_missing} = 0 unless defined( $cpanel_checks{$check}->{check_missing} ); $cpanel_checks{$check}->{help} = "" unless defined( $cpanel_checks{$check}->{help} ); my $help = $cpanel_checks{$check}->{help} ? ' - ' . $cpanel_checks{$check}->{help} : ''; if ( defined( $CPCONF{$check} ) && defined( $cpanel_checks{$check}->{default} ) && !( $CPCONF{$check} eq $cpanel_checks{$check}->{default} ) ) { SSP::Util::print_warn('cpanel.config: '); SSP::Util::print_warning("[ $check = $CPCONF{$check} ] $help"); } elsif ( $cpanel_checks{$check}->{check_missing} && ( !defined( $CPCONF{$check} ) || ( defined( $CPCONF{$check} ) && $CPCONF{$check} eq "" ) ) ) { SSP::Util::print_warn('cpanel.config: '); SSP::Util::print_warning("[ $check ] not found or has empty value. $help"); } } } sub check_for_low_ulimit_for_root { my $ulimit_m = SSP::Util::timed_run( 0, 'echo `ulimit -m`' ); my $ulimit_v = SSP::Util::timed_run( 0, 'echo `ulimit -v`' ); chomp( $ulimit_m, $ulimit_v ); if ( $ulimit_m =~ /\d+/ ) { $ulimit_m = sprintf( '%.0f', $ulimit_m / 1024 ); } if ( $ulimit_v =~ /\d+/ ) { $ulimit_v = sprintf( '%.0f', $ulimit_v / 1024 ); } if ( $ulimit_m =~ /\d+/ and $ulimit_m <= 256 or $ulimit_v =~ /\d+/ and $ulimit_v <= 256 ) { if ( $ulimit_m =~ /\d+/ ) { $ulimit_m .= 'MB'; } if ( $ulimit_v =~ /\d+/ ) { $ulimit_v .= 'MB'; } SSP::Util::print_warn('ulimit: '); SSP::Util::print_warning("-m [ $ulimit_m ] -v [ $ulimit_v ] Low ulimits can cause some commands to fail when run via the shell"); } } sub check_for_fork_bomb_protection { if ( -f '/etc/profile.d/limits.sh' or -f '/etc/profile.d/limits.csh' ) { SSP::Util::print_warn('Fork Bomb Protection: '); SSP::Util::print_warning('enabled!'); } } sub check_for_custom_exim_conf_local { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); my $exim_conf_local = '/etc/exim.conf.local'; my $is_customized = 0; if ( open my $file_fh, '<', $exim_conf_local ) { while ( my $line = <$file_fh> ) { chomp $line; if ( $line !~ m{ \A ( @ | \Z | chunking_advertise_hosts="" \Z ) }xms ) { $is_customized = 1; last; } } close $file_fh; } if ($is_customized) { SSP::Util::print_warn('Exim: '); SSP::Util::print_warning("$exim_conf_local contains customizations"); } } sub check_for_maxclients_or_maxrequestworkers_reached { return unless SSP::Util::i_am( 'ea4' ); my $apache_version = SSP::Util::get_apache_version(); my $log = SSP::Util::i_am('ea4') ? '/etc/apache2/logs/error_log' : '/usr/local/apache/logs/error_log'; my $size = ( stat($log) )[7]; my $bytes_to_check = 20_971_520 / 2; # 10M limit of logs to check, may need adjusting, depending how much time it adds to SSP my $seek_position = 0; my $log_data; my @logs; my $limit_last_hit_date; return if !$size; if ( $size > $bytes_to_check ) { $seek_position = ( $size - $bytes_to_check ); } if ( open my $file_fh, '<', $log ) { seek $file_fh, $seek_position, 0; read $file_fh, $log_data, $bytes_to_check; close $file_fh; } my $apache24 = ( $apache_version ) ? SSP::Util::version_compare( $apache_version, qw( >= 2.4.0 ) ) : 1; if ( $log_data =~ m/(?:MaxClients|MaxRequestWorkers)/s ) { @logs = split /\n/, $log_data; undef $log_data; @logs = reverse @logs; for my $log_line (@logs) { if ( $apache24 and $log_line =~ m{ \A \[ (\S+ \s+ \S+ \s+ \S+ \s+ \S+ \s+ \S+ ) \] \s .* server \s reached \s MaxRequestWorkers }xms ) { # [Fri Feb 08 09:58:45.875187 2013] [mpm_prefork:error] [pid 23220] AH00161: server reached MaxRequestWorkers $limit_last_hit_date = $1; last; } elsif ( not $apache24 and $log_line =~ m{ \A \[ (\S+ \s+ \S+ \s+ \S+ \s+ \S+ \s+ \S+ ) \] \s+ \[error\] \s+ server \s+ reached \s+ MaxClients }xms ) { # [Wed Nov 14 05:55:04 2012] [error] server reached MaxClients setting, consider raising the MaxClients setting $limit_last_hit_date = $1; last; } } } return unless $limit_last_hit_date; if ($apache24) { SSP::Util::print_warn('Apache MaxRequestWorkers: '); } else { SSP::Util::print_warn('Apache MaxClients: '); } SSP::Util::print_warning("limit last reached at $limit_last_hit_date"); } sub check_for_non_default_umask { my $umask = SSP::Util::timed_run( 0, 'echo `umask`' ); chomp $umask; return if !$umask || $umask =~ /2$/; SSP::Util::print_warn('umask: '); SSP::Util::print_warning("Non-default value [$umask] (check FB-62683 if permissions error when running convert_roundcube_mysql2sqlite)"); } sub check_for_multiple_imagemagick_installs { return unless SSP::Util::i_am('cpanel'); if ( -x '/usr/bin/convert' and not -l '/usr/bin/convert' ) { if ( -x '/usr/local/bin/convert' and not -l '/usr/local/bin/convert' ) { SSP::Util::print_warn('ImageMagick: '); SSP::Util::print_warning('multiple "convert" binaries found [/usr/bin/convert] [/usr/local/bin/convert]'); } } } sub check_for_broken_rpm { return unless my $rpms = SSP::Util::get_rpm_href(); $YUM_OR_DNF = SSP::Util::os_version_is(qw( >= 8)) ? 'dnf' : 'yum'; $YUM_OR_DNF = ( SSP::Util::i_am('ubuntu') ) ? "apt" : $YUM_OR_DNF; my %check = ( # 'rpm name' => { check => ['check1', 'check2', ...], help => 'Additional info' } # Check types should be ordered by dependency, first failure skips other checks for that RPM. 'bind-chroot' => { check => ['exists'], help => 'Not supported -- should be removed and excluded in yum.conf. See https://docs.cpanel.net/installation-guide/customize-your-installation/#exclude-packages' }, 'portreserve' => { check => ['exists'], help => 'Can cause ports to be locked to a specific service.' }, 'bitninja' => { check => ['exists'], help => 'May cause AutoSSL to fail.' }, 'cpuspeed' => { check => ['exists'], help => 'May cause ' . $YUM_OR_DNF . ' to crash when updating kernel. Send "cpuspeed detected" predefined response.' }, 'crypto-utils' => { check => ['exists'], help => '"certwatch" cron job can send email notices regarding expiring certificates.' }, 'letsencrypt-cpanel' => { check => ['exists'], help => 'Third-party FleetSSL Let\'s Encrypt plugin -- no direct support provided, use "3RDP - FleetSSL/LetsEncrypt" predef if relevant.' }, ); if ( SSP::Util::i_am('ubuntu')) { # FOR FUTURE USE # %check = ( # 'pkg name' => { check => ['check1', 'check2', ...], help => 'Additional info' } # Check types should be ordered by dependency, first failure skips other checks for that RPM. # ); } $check{'httpd-tools'} = { check => ['exists'], help => 'Conflicts with ea-apache24-tools, will break EA4.' }; my %types = ( 'exists' => { help => 'Package exists', run => sub { # Only care that the Package exists. my $rpm = shift; return 1 if exists $rpms->{$rpm}; return 0; } }, 'missing' => { help => 'Missing Package', run => sub { # Only care that the Package is missing. my $rpm = shift; return 0 if exists $rpms->{$rpm}; return 1; } }, 'verify-fail' => { help => 'Verify Failed', run => sub { # Only performs check if the Package exists. my $rpm = shift; return 0 unless exists $rpms->{$rpm}; my $output = SSP::Util::timed_run( 0, 'rpm', '-V', $rpm ); return 0 if $output eq ""; return 1 if $output =~ m{ \A missing }xms; return 1 if $output =~ m{ \A ..5 }xms; return 0; } } ); for my $rpm ( sort keys %check ) { for my $type ( @{ $check{$rpm}{check} } ) { if ( $types{$type}{run}->($rpm) ) { my $additional_info = $check{$rpm}{help} ? ("( $check{$rpm}{help} )") : ''; SSP::Util::print_warn('Package check: '); SSP::Util::print_warning("$types{$type}{help} - [ $rpm ] $additional_info"); last; } } } } sub check_for_ea4_mismatch { return unless SSP::Util::i_am('ea4'); # CX-972 Skip this check if CL9+/AlmaLinux9+ - CL9/AL9 repos now only include rpms they modify. return if ( SSP::Util::os_version_is(qw( > 8)) && SSP::Util::i_am_one_of( 'cloudlinux', 'almalinux' ) ); return unless my $rpms = SSP::Util::get_rpm_href(); my ( $cp_count, $cl_count ) = ( 0, 0 ); for my $name ( keys %{$rpms} ) { next unless index( $name, 'ea-' ) == 0; foreach my $rpm_ref ( @{ $rpms->{$name} } ) { if ( SSP::Util::i_am('ubuntu') ) { $cp_count++ if index( $rpm_ref->{'version'}, '.cpanel' ) != -1; $cl_count++ if index( $rpm_ref->{'version'}, '.cloudlinux' ) != -1; } else { $cp_count++ if index( $rpm_ref->{'release'}, '.cpanel' ) != -1; $cl_count++ if index( $rpm_ref->{'release'}, '.cloudlinux' ) != -1; } } } if ( SSP::Util::i_am('cloudlinux') ) { return unless $cp_count; SSP::Util::print_warn('EA4 Packages: '); SSP::Util::print_warning(qq{Found $cp_count "ea-*.cpanel" Packages on a CloudLinux system! Using wrong EA4 repo?}); return; } return unless $cl_count; SSP::Util::print_warn('EA4 Packages '); SSP::Util::print_warning(qq{Found $cl_count "ea-*.cloudlinux" Packages on a non-CloudLinux system! Using wrong EA4 repo?}); if ( -e '/etc/yum.repos.d/imunify360-ea-php-hardened.repo' ) { SSP::Util::print_warning(' \_ Imunify360 Hardened EA-PHP repo found. It is probably OK if all of the Packages originated from this repo.'); } } sub check_eximstats_size { return unless SSP::Util::i_am('cpanel'); return unless my $mysql_datadir = SSP::Util::get_mysql_datadir(); my $eximstats_dir = $mysql_datadir . 'eximstats/'; return unless -d $eximstats_dir; my @dir_contents; my $size; opendir( my $dir_fh, $eximstats_dir ); @dir_contents = grep { /(defers|failures|sends|smtp)\.(frm|MYI|MYD)$/ } readdir $dir_fh; closedir $dir_fh; for my $file (@dir_contents) { $file = $eximstats_dir . $file; $size += ( stat($file) )[7]; } if ( $size && $size > 5_000_000_000 ) { $size = sprintf( "%0.2fGB", $size / 1073741824 ); SSP::Util::print_warn('eximstats db: '); SSP::Util::print_warning($size); } } sub check_for_broken_mysql_tables { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); my %broken; my @schemas; my $sql_schemas; push @schemas, 'horde' if SSP::Util::cpanel_version_is(qw ( < 11.53.0.0 )); push @schemas, 'whmxfer' if SSP::Util::cpanel_version_is(qw ( < 11.57.0.0 )); push @schemas, 'modsec' if SSP::Util::cpanel_version_is(qw ( < 11.61.0.0 )); push @schemas, 'eximstats' if SSP::Util::cpanel_version_is(qw ( < 11.63.0.0 )); push @schemas, 'cphulkd' if SSP::Util::cpanel_version_is(qw ( < 11.65.0.0 )); push @schemas, 'roundcube' if defined $CPCONF{roundcube_db} && $CPCONF{roundcube_db} eq 'mysql'; return unless scalar @schemas; for my $schema ( sort @schemas ) { $sql_schemas .= ' OR ' if defined $sql_schemas && length $sql_schemas; $sql_schemas .= 'table_schema=\'' . $schema . '\''; } for ( split /\n/, SSP::Util::timed_run( 0, 'mysql', '-NBe', 'SELECT table_schema,table_name,engine,row_format,create_time FROM information_schema.tables WHERE ( ' . $sql_schemas . ' ) AND engine IS NULL' ) ) { my @line = split /\t/; next unless scalar @line >= 2; $broken{ $line[0] }{ $line[1] } = 1; } for ( sort( keys(%broken) ) ) { SSP::Util::print_warn('mysql: broken tables - '); my $tables = scalar keys( %{ $broken{$_} } ) > 4 ? 'More than 4!' : join( ' ', sort( keys( %{ $broken{$_} } ) ) ); SSP::Util::print_warning( $_ . ' [ ' . $tables . ' ]' ); } } sub check_for_clock_skew { return unless my $clock_skew = SSP::Util::get_clock_skew(); my $max_skew = 120; if ( defined $CPCONF{'SecurityPolicy::TwoFactorAuth'} && $CPCONF{'SecurityPolicy::TwoFactorAuth'} == 1 ) { $max_skew = 25; } return if ( $clock_skew < $max_skew ); if ( $clock_skew >= 31536000 ) { $clock_skew = sprintf '%d', ( $clock_skew / 31536000 ); $clock_skew .= ' year(s)'; } elsif ( $clock_skew >= 86400 ) { $clock_skew = sprintf '%d', ( $clock_skew / 86400 ); $clock_skew .= ' day(s)'; } elsif ( $clock_skew >= 3600 ) { $clock_skew = sprintf '%d', ( $clock_skew / 3600 ); $clock_skew .= ' hour(s)'; } elsif ( $clock_skew >= 60 ) { $clock_skew = sprintf '%d', ( $clock_skew / 60 ); $clock_skew .= ' minute(s)'; } else { $clock_skew = sprintf '%d', ($clock_skew); $clock_skew .= ' seconds'; } SSP::Util::print_warn('Clock skew: '); SSP::Util::print_warning("server time may be off by ${clock_skew}. A very large difference may cause SSL/TLS connection errors, and more than about 30 seconds can cause 2FA failure."); } sub check_for_duplicate_rpms { return unless my $rpms = SSP::Util::get_rpm_href(); my %SEEN_RPMS; my %DUP_RPMS; foreach my $name ( keys %{$rpms} ) { foreach my $rpm_ref ( @{ $rpms->{$name} } ) { push @{ $SEEN_RPMS{ $name . '-' . $rpm_ref->{'arch'} } }, $rpm_ref->{'version'} . '-' . $rpm_ref->{'release'}; if ( scalar @{ $SEEN_RPMS{ $name . '-' . $rpm_ref->{'arch'} } } > 1 ) { $DUP_RPMS{ $name . '-' . $rpm_ref->{'arch'} } = 1; } } } foreach my $dup_rpm ( sort keys %DUP_RPMS ) { next if ( $dup_rpm =~ m{^(?:gpg-pubkey|kernel)} ); SSP::Util::print_warn('DUPLICATE PACKAGE: '); SSP::Util::print_warning( "$dup_rpm has multiple versions: " . join( " ", @{ $SEEN_RPMS{$dup_rpm} } ) ); } } sub check_for_wordpress_manager_rpms { return unless my $rpms = SSP::Util::get_rpm_href(); for my $rpm ( keys %{$rpms} ) { if ( $rpm =~ /^(?:wordpress-cpaddon|cpanel-wordpress-instance-manager-plugin)/i ) { SSP::Util::print_warn("WP Manager (deprecated) is installed.\n"); SSP::Util::print_magenta("\t \\_ Send Customer The PREDEFS::Deprecated WP Manager Installed Macro"); last; } } } sub check_imunify_config { return unless( -e '/usr/bin/imunify360-agent' ); my %i360Conf; my $i360_config_file = "/etc/sysconfig/imunify360/imunify360.config"; return unless( open( my $conf_fh, '<', $i360_config_file )); while (<$conf_fh>) { next unless ( m/\s(\S+):\s(\w+)/ ); $i360Conf{$1} = $2; } close($conf_fh); my $raw = SSP::Util::timed_run( 0, 'imunify360-agent', 'version', '--json' ); my $i360versioninfo = SSP::Util::get_json_href($raw); my $imunify_licenseType = $i360versioninfo->{license}->{license_type}; return unless($imunify_licenseType); $imunify_licenseType =~ s/Plus/\+/; SSP::Util::print_info("$imunify_licenseType is running: "); SSP::Util::print_normal("Default Action: $i360Conf{'default_action'}" ); my @trimFiles; my @quarantineFiles; @trimFiles = glob('/var/imunify360/cleanup_storage/*') unless( !-d '/var/imunify360/cleanup_storage'); @quarantineFiles = glob('/home/.imunify.quarantined/*') unless( !-d '/home/.imunify.quarantined'); SSP::Util::print_normal("\t\\_ cleanup (trim) files found within /var/imunify360/cleanup_storage [ will be automatically removed within $i360Conf{'keep_original_files_days'} days ]") unless( scalar @trimFiles == 0 ); SSP::Util::print_normal("\t\\_ quarantined files found within /home/.imunify.quarantined") unless( scalar @quarantineFiles == 0 ); if ( $imunify_licenseType eq "imunify360" or $imunify_licenseType eq "imunify360Trial") { SSP::Util::print_normal("\t\\_ Issues with installing PECL extensions or running PHP's composer? Try setting Imunify360's Proactive Defense to log mode"); } } sub check_for_etc_ubic_dir { if ( -e '/etc/ubic' ) { SSP::Util::print_warn('/etc/ubic: '); SSP::Util::print_warning('exists! This directory will cause ea-tomcat85 to fail when adding new users, see TECH-707'); } } sub check_for_acls_cpconf { return unless SSP::Util::i_am('cpanel'); return unless defined $CPCONF{'acls'}; if ( $CPCONF{'acls'} ) { SSP::Util::print_warn('Permissions: '); SSP::Util::print_warning('"acls" Tweak Setting enabled. This affects homedir, docroot, and htpasswd, see TECH-751'); } } sub check_for_port_53_dnsmasq { return unless my $ports = SSP::Util::get_lsof_port_href(); return unless exists $ports->{'53'}; for my $ref ( @{ $ports->{'53'} } ) { if ( $ref->{'CMD'} =~ m{ \A dnsmasq }xms ) { SSP::Util::print_warn('Dnsmasq: '); SSP::Util::print_warning('listening on port 53, known to cause issues with AutoSSL, see TECH-718'); last; } } } sub check_for_port_21_ftp { return unless my $ports = SSP::Util::get_lsof_port_href(); return unless exists $ports->{'21'}; my ( $pid, $comm ) = @{ $ports->{'21'}->[0] }{qw( PID CMD )}; return unless my $exe = readlink "/proc/${pid}/exe"; return unless my $cwd = readlink "/proc/${pid}/cwd"; return if ( $exe eq '/usr/sbin/pure-ftpd' || $exe eq '/usr/sbin/proftpd' ); SSP::Util::print_warn('Port 21: '); SSP::Util::print_warning("something other than Pure-FTPd or ProFTPd is running:"); SSP::Util::print_magenta("\t \\_ cmd [$comm]"); SSP::Util::print_magenta("\t \\_ exe [$exe]"); SSP::Util::print_magenta("\t \\_ cwd [$cwd]"); } sub check_ftpusers_file { my $ftpusers_file = '/etc/ftpusers'; return unless ( -s $ftpusers_file ); SSP::Util::print_warn('/etc/ftpusers exists: '); SSP::Util::print_warning('users in this file cannot access FTP'); } # TODO: Once 84 is largely distrubuted, update this to use one of our modules sub check_root_dns_resolvers { my $limit = 3; my @resolvers = ( 'a' .. 'm' ); my @failed; foreach my $dns (@resolvers) { last if scalar @failed >= $limit; chomp( my $ip = SSP::Util::timed_run( 5, 'dig', '-4', '+short', "$dns.root-servers.net", "\@$dns.root-servers.net" ) ); push @failed, $dns unless ( $ip =~ /\d+\.\d+\.\d+\.\d+/ ); } return unless @failed; SSP::Util::print_warn('DNS: '); SSP::Util::print_warning('Root DNS servers are unreachable [ ' . join(' ', @failed) . " ]. This can break AutoSSL. We stop after $limit failures, verify with this command:"); SSP::Util::print_warning("\t \\_ " . 'for i in {a..m}; do echo -n "$i.root-servers.net: "; dig -4 "$i".root-servers.net @"$i".root-servers.net +short;done'); } sub check_cloudlinux_phphandler_file { return unless SSP::Util::i_am('cloudlinux'); my $phphandler_file = '/etc/container/php.handler'; return unless not -z $phphandler_file; if (open my $fh, '<', $phphandler_file) { while(<$fh>) { next unless (m/x-httpd-ea-.*\/opt\/cloudlinux\/.*lsphp/); SSP::Util::print_warn('CloudLinux: '); SSP::Util::print_warning("/etc/container/php.handler exists. Having issues with downloading the PHP file? See TECH-792"); last; } close($fh); } } sub check_if_httpdconf_ipaddrs_exist { return unless SSP::Util::i_am( 'ea4' ); my $httpdconf = '/etc/apache2/conf/httpd.conf'; return unless -f $httpdconf; my @local_ipaddrs = @{ SSP::Util::get_local_ipaddrs_aref() }; my @vhost_ipaddrs; my @unbound_ipaddrs; if ( open my $httpdconf_fh, '<', $httpdconf ) { local $/ = undef; my $httpdconf_txt = readline($httpdconf_fh); close $httpdconf_fh; while ( $httpdconf_txt =~ m//sig ) { push @vhost_ipaddrs, $1; } } # uniq IP addrs only @vhost_ipaddrs = do { my %seen; grep { !$seen{$_}++ } @vhost_ipaddrs; }; for my $vhost_ipaddr (@vhost_ipaddrs) { my $is_bound = 0; for my $local_ipaddr (@local_ipaddrs) { if ( $vhost_ipaddr eq $local_ipaddr ) { $is_bound = 1; last; } } if ( $is_bound == 0 ) { push @unbound_ipaddrs, $vhost_ipaddr; } } return unless @unbound_ipaddrs; SSP::Util::print_warn('Apache: '); SSP::Util::print_warning('httpd.conf has VirtualHosts for these IP addrs, which aren\'t bound to the server:'); for my $unbound_ipaddr (@unbound_ipaddrs) { SSP::Util::print_magenta("\t \\_ $unbound_ipaddr"); } } sub check_for_custom_repos { my $yum_repos_dir; if ( SSP::Util::i_am('ubuntu')) { $yum_repos_dir = '/etc/apt/sources.list.d'; } else { $yum_repos_dir = '/etc/yum.repos.d'; } my $hit = 0; my @dir_contents; my %custom_repos = ( 'PostgreSQL' => { msg => '', regex => qr{ \A pgdg-(\d+)-centos\.repo }x, }, 'City-Fan' => { msg => ' see TECH-716', regex => qr{ \A city-fan.org\.repo }x, }, 'Jperkster' => { msg => ' see UPS-136', regex => qr {\A EA4-freetds.repo }x, }, 'FleetSSL 3rdParty Plugin' => { msg => ' abandoned! See: https://support.cpanel.net/hc/en-us/articles/360051909773', regex => qr {\A letsencrypt\.repo|fleetssl\.list }x, }, ); return if !-d $yum_repos_dir; opendir( my $dir_fh, $yum_repos_dir ); @dir_contents = grep { !/^\.\.?$/ } readdir $dir_fh; closedir $dir_fh; foreach my $repo (@dir_contents) { foreach my $key ( keys %custom_repos ) { if ( $repo =~ m/ ( $custom_repos{$key}{regex} ) /xms ) { $custom_repos{$key}{file} = $1; $hit++; SSP::Util::print_warn( 'Custom package(s) found in ' . $yum_repos_dir . ":\n" ) if ( $hit eq 1 ); SSP::Util::print_warning( "\t \\_ $key: " . $custom_repos{$key}{file} . $custom_repos{$key}{msg} ); } } } } sub check_for_rpm_overrides { return if ( SSP::Util::i_am_one_of( 'ubuntu', 'dnsonly' ) ); my $rpm_override_dir = '/var/cpanel/rpm.versions.d/'; return unless -d $rpm_override_dir; my $expected_file_version = '2'; my $local_versions = $rpm_override_dir . 'local.versions'; my $easy_versions = $rpm_override_dir . 'easy.versions'; my $cloud_versions = $rpm_override_dir . 'cloudlinux.versions'; my $md5_easy; my $md5_cloud; my $easy_is_default = 0; my $cloud_is_default = 0; if ( -f $easy_versions ) { $md5_easy = SSP::Util::timed_run( 0, 'md5sum', $easy_versions ); } if ( -f $cloud_versions ) { $md5_cloud = SSP::Util::timed_run( 0, 'md5sum', $cloud_versions ); } ## these are checksums for default files. we ignore them to prevent needless output from SSP if ( defined $md5_easy and $md5_easy =~ m{ \A ( 350e47b97efd4b75563837b6b3502d71 | 436a6863b1a1ec3bb7607066beca7356 | 5818611cb4c0bf4086806aced5669f25 | 600ff436e5939656a5645c6139cc0228 | 657d59cc9627d30a95d0b84ef4245185 | 89d631ef7c1d43475c20d7be7b7290ff | d56abe76c47853eceb706f0855e642a7 | eed54b4202d0b2655f37a5c1edfa0853 ) \s }xms ) { $easy_is_default = 1; } if ( defined $md5_cloud and $md5_cloud =~ m{ \A ( 23b40252dc77a775d00c87b89a64621a | 956e6d177a790389572c7fcc8b33f8c5 | a33f5a902bbadebd7b3029e0337fde35 | cf40c6ac1543464a937b8c39c4ad1da6 ) \s }xms ) { $cloud_is_default = 1; } opendir( my $dir_fh, $rpm_override_dir ); my @dir_contents = grep { !/^(?:\.\.?|local.versions)$/ } readdir $dir_fh; closedir $dir_fh; if ( $easy_is_default == 1 ) { @dir_contents = grep { $_ ne 'easy.versions' } @dir_contents; } if ( $cloud_is_default == 1 ) { @dir_contents = grep { $_ ne 'cloudlinux.versions' } @dir_contents; } if (@dir_contents) { SSP::Util::print_warn( $rpm_override_dir . ': ' ); SSP::Util::print_warning( '[ ' . join( ' ', @dir_contents ) . ' ] may contain custom entries, manually verify. More info: http://go.cpanel.net/rpmversions' ); } return unless -s $local_versions; return if !SSP::Util::load_module_with_fallbacks( 'needed_subs' => [qw{LoadFile}], 'modules' => [qw{YAML::Syck}], 'fail_warning' => 'can\'t check rpm.versions.d/local.versions for customization', ); my $ignore = { 'target_settings' => { 'analog' => '(?:un)?installed', 'awstats' => '(?:un)?installed', 'clamav' => '(?:un)?installed', 'easy-icu' => '(?:un)?installed', 'easy-tomcat7' => '(?:un)?installed', 'munin' => '(?:un)?installed', 'webalizer' => '(?:un)?installed', }, }; if ( SSP::Util::i_am('dnsonly') ) { $ignore->{'target_settings'}->{'composer'} = 'uninstalled'; $ignore->{'target_settings'}->{'cpanel-squirrelmail'} = 'uninstalled'; $ignore->{'target_settings'}->{'mailman'} = 'uninstalled'; $ignore->{'target_settings'}->{'proftpd'} = 'uninstalled'; $ignore->{'target_settings'}->{'pure-ftpd'} = 'uninstalled'; $ignore->{'target_settings'}->{'roundcube'} = 'uninstalled'; } if ( SSP::Util::cpanel_version_is(qw( >= 11.61.0.0 )) ) { $ignore->{'target_settings'}->{'perl522'} = 'uninstalled'; # Normal for WHM 62 upgrade process } if ( SSP::Util::cpanel_version_is(qw( >= 11.69.0.0 )) ) { $ignore->{'target_settings'}->{'perl524'} = 'uninstalled'; # Normal for WHM 70 upgrade process } if ( SSP::Util::cpanel_version_is(qw( >= 11.77.0.0 )) ) { $ignore->{'target_settings'}->{'perl526'} = 'uninstalled'; # Normal for WHM 78 upgrade process } if ( SSP::Util::cpanel_version_is(qw( >= 11.86.0.0 )) ) { $ignore->{'target_settings'}->{'perl530'} = 'uninstalled'; # Normal for WHM 86 upgrade process } if ( SSP::Util::cpanel_version_is(qw( >= 11.94.0.0 )) ) { $ignore->{'target_settings'}->{'perl532'} = 'uninstalled'; # Normal for WHM 94 upgrade process } my $ref; eval { $ref = YAML::Syck::LoadFile($local_versions); }; if ( defined $ref ) { if ( not defined $ref->{'file_format'} or not defined $ref->{'file_format'}->{'version'} ) { SSP::Util::print_warn( $local_versions . ': ' ); SSP::Util::print_warning('file version is missing!'); } else { if ( $ref->{'file_format'}->{'version'} ne $expected_file_version ) { SSP::Util::print_warn( $local_versions . ': ' ); SSP::Util::print_warning( 'file version is ' . $ref->{'file_format'}->{'version'} . ', expected version ' . $expected_file_version . '!' ); } } if ( exists $ref->{'target_settings'} ) { foreach my $package ( sort keys %{ $ref->{'target_settings'} } ) { next if exists $ignore->{'target_settings'}->{$package} and $ref->{'target_settings'}->{$package} =~ m{ \A $ignore->{'target_settings'}->{$package} \Z }xms; SSP::Util::print_warn( $local_versions . ': ' ); if ( $ref->{'target_settings'}->{$package} =~ m/installed|uninstalled|unmanaged/ ) { SSP::Util::print_warning( $package . ' is configured as ' . $ref->{'target_settings'}->{$package} ); } else { SSP::Util::print_warning( $package . ' is configured as ' . $ref->{'target_settings'}->{$package} . YELLOW ' [ not a valid target state ]' ); } } } for my $section (qw( install_targets location_keys rpm_groups rpm_locations srpm_sub_packages srpm_versions url_templates )) { if ( exists $ref->{$section} and scalar keys %{ $ref->{$section} } ) { SSP::Util::print_warn( $local_versions . ': ' ); SSP::Util::print_warning( 'Custom ' . $section . ' exists for: [ ' . join( ' ', sort keys %{ $ref->{$section} } ) . ' ]' ); } } } else { SSP::Util::print_warn( $local_versions . ': ' ); SSP::Util::print_warning('YAML not successfully imported, corrupted file?'); } } sub check_var_cpanel_immutable_files { my $immutable_files = '/var/cpanel/immutable_files'; if ( -e $immutable_files and not -z $immutable_files ) { SSP::Util::print_warn('immutable files: '); SSP::Util::print_warning("$immutable_files is not empty!"); } } sub check_mylogincnf { my $logincnf = '/root/.mylogin.cnf'; return unless ( -e $logincnf ); SSP::Util::print_warn('/root/.mylogin.cnf exists: '); SSP::Util::print_warning('This file takes precedence over "/root/.my.cnf", see TECH-637'); } sub check_for_noxsave_in_grub_conf { my $grub_conf = '/boot/grub/grub.conf'; return if !-f $grub_conf; if ( open my $grub_fh, '<', $grub_conf ) { while (<$grub_fh>) { if (/noxsave/) { SSP::Util::print_warn('noxsave: '); SSP::Util::print_warning("found in ${grub_conf}. kernel panics? segfaults?"); last; } } close $grub_fh; } } sub check_for_rpm_dist_ver_unknown { return unless( SSP::Util::cpanel_version_is(qw(< 11.100.0.0)) ); my $sysinfo_config = '/var/cpanel/sysinfo.config'; return unless -f $sysinfo_config; if ( open my $file_fh, '<', $sysinfo_config ) { while (<$file_fh>) { if (/^rpm_dist_ver=unknown$/) { SSP::Util::print_warn("${sysinfo_config}: "); SSP::Util::print_warning("contains 'rpm_dist_ver=unknown'. Try running '/scripts/gensysinfo' to fix"); last; } } close $file_fh; } } sub check_for_networkmanager { return unless ( SSP::Util::os_version_is(qw( >= 9 ) ) ); ## CX-918 return unless -s '/etc/ips'; return unless SSP::Util::exists_process_cmd( qr{ NetworkManager }xms, 'root' ); SSP::Util::print_warn('NetworkManager: '); SSP::Util::print_warning('is running, could disrupt ipaliases service - see https://go.cpanel.net/disablenm'); } sub check_for_prefix_in_network { return unless -s '/etc/ips'; if ( SSP::Util::i_am( 'ubuntu' ) ) { my @netplanfiles = glob(q{/etc/netplan/*.yaml}); foreach my $yamlfile(@netplanfiles) { open( my $fh, '<', $yamlfile ); while( <$fh> ) { chomp; next unless( $_ =~ m{/32} ); SSP::Util::print_crit( 'Networking: ' ); SSP::Util::print_critical( "Possible /32 found within the " . YELLOW $yamlfile . MAGENTA " file and multiple IPs in the /etc/ips file. Can cause network issues with Hetzner/OVH servers" ); last; } close( $fh ); } } else { my @sysconfigfiles = glob(q{/etc/sysconfig/network-scripts/ifcfg-*}); foreach my $ifcfgfile(@sysconfigfiles) { open( my $fh, '<', $ifcfgfile ); while( <$fh> ) { chomp; next unless( $_ =~ m{PREFIX=32} ); SSP::Util::print_crit( 'Networking: ' ); SSP::Util::print_critical( "PREFIX=32 found within the " . YELLOW $ifcfgfile . MAGENTA " file and multiple IPs in the /etc/ips file. Can cause network issues with Hetzner/OVH servers" ); last; } close( $fh ); } } } sub check_for_dhclient { return unless SSP::Util::exists_process_cmd( qr{ dhclient }xms, 'root' ); SSP::Util::print_warn('dhclient: '); SSP::Util::print_warning('found in the process list'); } sub check_for_var_cpanel_roundcube_install { my $install = '/var/cpanel/roundcube/install'; return unless ( -f $install and -x $install ); SSP::Util::print_warn('RoundCube: '); SSP::Util::print_warning("$install exists. /u/l/c/b/update-roundcube won't fully run (by design - see the docs)"); } sub check_for_missing_etc_localtime { stat('/etc/localtime'); if ( not -f _ or not -s _ ) { SSP::Util::print_warn('/etc/localtime: '); SSP::Util::print_warning('Missing or empty! Can break many things including upcp and stats.'); } } sub check_for_disabled_services { return if( SSP::Util::i_am('dnsonly')); my %disabled_services; my %touchfiles = ( '/etc/antirelayddisable' => 'antirelayd', '/var/cpanel/ssl/disable_auto_hostname_certificate' => '/var/cpanel/ssl/disable_auto_hostname_certificate', '/etc/rrdtooldisable' => 'rrdtool', '/var/run/chkservd.suspend' => 'chksrvd', '/etc/checkyumdisable' => 'checkyum', '/etc/clamddisable' => 'clamd', '/etc/cppopdisable' => 'courier', '/etc/pop3disable' => 'courier', '/etc/popddisable' => 'courier', '/etc/popdisable' => 'courier', '/etc/cpanel-dovecot-solrdisable' => 'cpanel-dovecot-solr', '/etc/cpanellogddisable' => 'cpanellogd', '/etc/cpdavddisable' => 'cpdavd', '/etc/cpsrvdddisable' => 'cpsrvd', '/etc/binddisable' => 'dnsserver', '/etc/dnsdisable' => 'dnsserver', '/etc/nameddisable' => 'dnsserver', '/etc/eximdisable' => 'exim', '/etc/exiscandisable' => 'exiscan', '/etc/disablehackcheck' => 'hackcheck', '/etc/ftpddisable' => 'ftpd', '/etc/ftpserverdisable' => 'ftpd', '/var/cpanel/ssl/disable_hostname_mismatch_check' => '/var/cpanel/ssl/disable_hostname_mismatch_check', '/etc/apachedisable' => 'httpd', '/etc/httpddisable' => 'httpd', '/etc/httpdisable' => 'httpd', '/etc/httpdisevil' => 'httpd', '/etc/cpimapdisable' => 'imapd', '/etc/imapddisable' => 'imapd', '/etc/imapdisable' => 'imapd', '/etc/ipaliasesdisable' => 'ipaliases', '/etc/mailmandisable' => 'mailman', '/etc/mydnsdisable' => 'mydns', '/etc/mysqldisable' => 'mysql', '/etc/nsddisable' => 'nsd', '/etc/postgresdisable' => 'postgresql', '/etc/postgresqldisable' => 'postgresql', '/etc/postmasterdisable' => 'postgresql', '/etc/proftpddisable' => 'proftpd', '/etc/pureftpddisable' => 'pureftpd', '/etc/pure-ftpddisable' => 'pureftpd', '/etc/queueprocddisable' => 'queueprocd', '/etc/rsyslogddisable' => 'rsyslogd', '/etc/rsyslogdisable' => 'rsyslogd', '/etc/securemysqldisable' => 'securemysql', '/var/cpanel/version/securetmp_disabled' => 'securetmp', '/var/cpanel/ssl/disable_service_certificate_management' => '/var/cpanel/ssl/disable_service_certificate_management', '/etc/spamddisable' => 'spamd', '/etc/spamdisable' => 'spamd', '/etc/sshddisable' => 'sshd', '/etc/syslogddisable' => 'syslogd', '/etc/syslogdisable' => 'syslogd', '/etc/tailwatchddisable' => 'tailwatchd', '/etc/tailwatchdisable' => 'tailwatchd', '/etc/tomcatdisable' => 'tomcat', '/var/cpanel/disabled/secureit' => 'secureit', '/var/cpanel/disabled/securetmp' => 'securetmp', '/var/cpanel/disabled/auto-restart-services' => '/var/cpanel/disabled/auto-restart-services', ); my %cpconfcheck = ( 'skipapnspush' => 'apnspush', 'skipchkservd' => 'chkservd', 'skipcpbandwd' => 'cpbandwd', 'skipeximstats' => 'eximstats', 'skipmailhealth' => 'mailhealth', 'skipmailman' => 'mailman', 'skipmodseclog' => 'modseclog', 'skiprecentauthedmailiptracker' => 'recentauthedmailiptracker', 'skiptailwatchd' => 'tailwatchd', ); while ( my ( $touchfile, $service ) = each(%touchfiles) ) { if ( -e $touchfile ) { $disabled_services{$service} = 1; } } while ( my ( $skip, $service ) = each(%cpconfcheck) ) { if ( $CPCONF{$skip} ) { $disabled_services{$service} = 1; } } return if !%disabled_services; SSP::Util::print_warn('Disabled services: '); SSP::Util::print_warning( '[ ' . join( ' ', sort( keys(%disabled_services) ) ) . ' ]' ); } sub check_for_license_status_json() { return if ! -e "/var/cpanel/license.status.json"; my $licstatus; if ( open my $file_fh, '<', "/var/cpanel/license.status.json" ) { while (<$file_fh>) { $licstatus = SSP::Util::get_json_href($_); } close($file_fh); } return unless( defined $licstatus->{code} && $licstatus->{code} != 200 ); SSP::Util::print_warn('License: '); SSP::Util::print_warning("Last cpkeyclt check failed: " . BOLD CYAN $licstatus->{message} ); return; } # Yes, this actually happened... sub check_for_cpbackup_exclude_everything { my $conf = '/etc/cpbackup-exclude.conf'; return unless -f $conf; open my $conf_fh, '<', $conf or return; while (<$conf_fh>) { chomp; if (/^\*$/) { SSP::Util::print_warn('Backups: '); SSP::Util::print_warning("'*' exists by itself in $conf . This can cause 0 byte backups"); last; } elsif (/\x00/) { SSP::Util::print_warn('Backups: '); SSP::Util::print_warning('/etc/cpbackup-exclude.conf contains NUL bytes - can result in homedir being skipped. See CPANEL-17065.'); last; } } close $conf_fh; } sub check_for_bw_module_and_more_than_1024_vhosts { return unless SSP::Util::i_am( 'ea4' ); my $modules = SSP::Util::get_apache_modules_href(); return unless defined $modules->{'bw_module'}; my $httpdconf = '/etc/apache2/conf/httpd.conf'; return if !-f $httpdconf; my $num_vhosts = 0; open my $httpdconf_fh, '<', $httpdconf or return; while (<$httpdconf_fh>) { if (m{ \A (?:\s+)? 1024 ) { SSP::Util::print_warn('bw_module: '); SSP::Util::print_warning("loaded, and httpd.conf has >1024 VirtualHosts ($num_vhosts). Apache failing to start? See EAL-2347"); } } sub check_for_uppercase_chars_in_hostname { if ( SSP::Util::get_hostname() =~ /[A-Z]/ ) { SSP::Util::print_warn('Hostname: '); SSP::Util::print_warning('contains UPPERCASE characters. Seeing incorrect info at cPanel >> Configure Email Client?'); } } sub check_for_harmful_php_mode_600_cron { return unless SSP::Util::i_am('cpanel'); return unless -d '/etc/cron.daily'; my @dir_contents; opendir( my $dir_fh, '/etc/cron.daily' ) or return; @dir_contents = grep { !/^\.\.?$/ } readdir $dir_fh; closedir $dir_fh; for my $file (@dir_contents) { $file = '/etc/cron.daily/' . $file; open my $file_fh, '<', $file or next; while (<$file_fh>) { if (/^mytmpfile=\/tmp\/php-mode-/) { SSP::Util::print_warn('harmful cron: '); SSP::Util::print_warning("${file}! Breaks webmail, phpMyAdmin, and more! Vendor: http://whmscripts.net/misc/2013/apache-symlink-security-issue-fixpatch/"); last; } } close $file_fh; } } sub check_for_bad_permissions_on_named_ca { return if ( SSP::Util::i_am( 'ubuntu' ) ); return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); my $namedca = '/var/named/named.ca'; if ( !-e $namedca ) { SSP::Util::print_warn("${namedca}: "); SSP::Util::print_warning('missing. named may not start without it'); return; } my ( $mode, $uid, $gid ) = ( stat('/var/named/named.ca') )[ 2, 4, 5 ]; my $world_readable_bit = $mode & oct(4); my $user = ( getpwuid($uid) ) ? getpwuid($uid) : ""; my $group = ( getgrgid($gid) ) ? getpwuid($gid) : ""; if ( not( $user eq 'named' or $group eq 'named' ) and ( $world_readable_bit == 0 ) ) { SSP::Util::print_warn("${namedca}: "); SSP::Util::print_warning('may not be readable to the \'named\' user, causing named to not restart'); } } sub check_var_db_nscd_directory { return if ( SSP::Util::i_am( 'ubuntu' ) ); return if ( ! SSP::Util::i_am( 'cpanel' ) ); return if ( -e '/etc/nscddisable' ); my @cached_services = qw( group hosts netgroup passwd services ); my $db_dir = '/var/db/nscd'; foreach my $cache_service(@cached_services) { next if -f "$db_dir/$cache_service"; SSP::Util::print_warn( 'NSCD: ' ); SSP::Util::print_warning( $db_dir . '/' . $cache_service . ' is not a file - nscd failing to start? - See TECH-533' ); } } sub check_for_jailshell_additional_mounts_trailing_slash { return unless SSP::Util::i_am('cpanel'); my $mounts_file = '/var/cpanel/jailshell-additional-mounts'; return if ( !-f $mounts_file ); if ( open my $file_fh, '<', $mounts_file ) { while (<$file_fh>) { chomp; if (m#/(?:[\s\t]+)?\z#) { SSP::Util::print_warn("$mounts_file: "); SSP::Util::print_warning('contains trailing slashes! Server may become unstable. See FB-71613'); last; } } close $file_fh; } } sub check_for_allow_query_localhost { my $named_conf = '/etc/named.conf'; return if !-f $named_conf; my $namedconf_contents; if ( open my $named_conf_fh, '<', $named_conf ) { local $/; $namedconf_contents = <$named_conf_fh>; close $named_conf_fh; } return unless $namedconf_contents; if ( $namedconf_contents =~ m#allow-query ([\s\t\r\n]+)? { ([\s\t]+)? ( localhost | 127\. )#xms ) { SSP::Util::print_warn('named.conf: '); SSP::Util::print_warning('allow-query is restricted to localhost. Remote DNS queries may not work'); } } sub check_for_nocloudlinux_touchfile { return if ( SSP::Util::i_am( 'ubuntu' ) ); if ( -e '/var/cpanel/nocloudlinux' && -e '/var/cpanel/disabled/cloudlinux' ) { SSP::Util::print_warn('nocloudlinux: '); SSP::Util::print_warning('\'/var/cpanel/nocloudlinux\' and \'/var/cpanel/disabled/cloudlinux\' exist! These block cPanel from installing/updating CloudLinux via:'); SSP::Util::print_warning( "\t \\_ " . '/usr/local/cpanel/scripts/cpanel_initial_install' ); SSP::Util::print_warning( "\t \\_ " . '/usr/local/cpanel/bin/cloudlinux_update' ); SSP::Util::print_warning( "\t \\_ " . '/usr/local/cpanel/bin/cloudlinux_system_install' ); return; } elsif ( -e '/var/cpanel/nocloudlinux' ) { SSP::Util::print_warn('nocloudlinux: '); SSP::Util::print_warning('\'/var/cpanel/nocloudlinux\' exists! This blocks cPanel from installing/updating CloudLinux via:'); SSP::Util::print_warning( "\t \\_ " . '/usr/local/cpanel/scripts/cpanel_initial_install' ); SSP::Util::print_warning( "\t \\_ " . '/usr/local/cpanel/bin/cloudlinux_update' ); SSP::Util::print_warning( "\t \\_ " . '/usr/local/cpanel/bin/cloudlinux_system_install' ); return; } elsif ( -e '/var/cpanel/disabled/cloudlinux' ) { SSP::Util::print_warn('nocloudlinux: '); SSP::Util::print_warning('\'/var/cpanel/disabled/cloudlinux\' exists! This blocks cPanel from installing/updating CloudLinux via:'); SSP::Util::print_warning( "\t \\_ " . '/usr/local/cpanel/scripts/cpanel_initial_install' ); SSP::Util::print_warning( "\t \\_ " . '/usr/local/cpanel/bin/cloudlinux_update' ); } } sub check_for_stupid_touchfile { return if !-e '/etc/allowstupidstuff'; SSP::Util::print_warn('/etc/allowstupidstuff: '); SSP::Util::print_warning('exists! Can allow usernames to be created that begin with digits.'); } sub check_for_noverify_SSL_touchfile { return if !-e '/var/cpanel/no_verify_SSL'; SSP::Util::print_crit('Found /var/cpanel/no_verify_SSL touchfile: '); SSP::Util::print_critical("Doesn't exist by default! Can lead to MITM attack!"); } sub check_for_team_manager_touchfile { # EXPERIMENTAL!!! - Through 110. Starting in 112 this is no longer present as this is no longer experimental. # Requires an active Premier Cloud or Premier Metal license. return unless( -f '/var/cpanel/experimental/p683' ); SSP::Util::print_warn('/var/cpanel/experimental/p683: '); SSP::Util::print_warning('exists! With a Premier license, the Experimental Team Manager option is enabled.'); } sub check_team_user_count { # CX-660 my $teamdir = '/var/cpanel/team'; return unless -d $teamdir; my @exceeded_team_users; opendir( my $dh, $teamdir ); my @contents = grep { !/^\.\.?$/ } readdir $dh; close $dh; foreach my $team(@contents) { chomp($team); open( my $fh, '<', "/var/cpanel/team/$team" ); my $totlines=0; while ( <$fh> ) { $totlines++; } close( $fh ); next unless ( $totlines > 8 ); ## first line is team name, rest is users. If line count is less than 8, all good push @exceeded_team_users, $team . " [$totlines] "; } if ( scalar( @exceeded_team_users ) > 0 ) { SSP::Util::print_warn('Team User Count Exceeded: '); SSP::Util::print_warning( CYAN join( ' ', @exceeded_team_users ) ); SSP::Util::print_normal( MAGENTA "\t\\_ Analysts: please see: https://go.cpanel.net/team-users-exceeded" ); } } sub check_for_dev_sandbox { my @touchfiles = qw{ /var/cpanel/align_memory_to_arch /var/cpanel/dev_sandbox }; my @found = grep { -e $_ } @touchfiles; for my $touchfile (@found) { SSP::Util::print_warn( $touchfile . ': ' ); SSP::Util::print_warning('Should NEVER exist on production servers.'); } } sub check_for_jail_owner { return unless -e '/jail_owner'; SSP::Util::print_warn('/jail_owner: '); SSP::Util::print_warning('exists! Should NEVER exist outside of jailshell. Will cause Exim mail delivery issues.'); } sub check_for_invalid_HOMEDIR { my $wwwacctconf = '/etc/wwwacct.conf'; return unless -f $wwwacctconf; my $homedir; if ( open my $file_fh, '<', $wwwacctconf ) { while (<$file_fh>) { if (/\AHOMEDIR[\s\t]+([^\s]+)/) { $homedir = $1; last; } } close $file_fh; } if ( !$homedir ) { SSP::Util::print_warn("$wwwacctconf: "); SSP::Util::print_warning('HOMEDIR value not found!'); return; } if ( !-d $homedir ) { SSP::Util::print_warn("$wwwacctconf: "); SSP::Util::print_warning("the directory that is specified as the HOMEDIR does not exist! ($homedir)"); } } sub check_if_hostname_missing_from_localdomains { return unless SSP::Util::i_am('cpanel'); my $hostname = SSP::Util::get_hostname(); if ( open my $localdomains_fh, '<', '/etc/localdomains' ) { while (<$localdomains_fh>) { return if (/^${hostname}$/); } close $localdomains_fh; } SSP::Util::print_warn('Hostname: '); SSP::Util::print_warning('not found in /etc/localdomains. This can cause "lowest numbered MX record points to local host"'); } sub check_for_eximstats_newline { return unless SSP::Util::i_am('cpanel'); return unless SSP::Util::cpanel_version_is(qw( < 11.63.0.0 )); my $eximstatspass = '/var/cpanel/eximstatspass'; if ( !-e $eximstatspass ) { SSP::Util::print_warn("$eximstatspass: "); SSP::Util::print_warning('missing!'); return; } if ( open my $eximstatspass_fh, '<', $eximstatspass ) { while (<$eximstatspass_fh>) { if (/\n/) { SSP::Util::print_warn("$eximstatspass: "); SSP::Util::print_warning('contains a newline. Breaks Mail Delivery Reports / eximstats'); last; } } close $eximstatspass_fh; } } sub check_for_processes_killed_by_lfd { my $log = '/var/log/lfd.log'; return if !-e $log; my $size = ( stat($log) )[7]; return if !$size; my $bytes_to_check = 20_971_520 / 2; # 10M my $seek_position = 0; my $log_data; my $count = 0; my @killed_by_lfd; if ( $size > $bytes_to_check ) { $seek_position = ( $size - $bytes_to_check ); } open my $file_fh, '<', $log or return; seek $file_fh, $seek_position, 0; read $file_fh, $log_data, $bytes_to_check; close $file_fh; if ( $log_data =~ /\sKill:1\s/ ) { my @logs = split /\n/, $log_data; undef $log_data; @logs = reverse @logs; for my $line (@logs) { if ( $line =~ /(.*?)[ \t]+\*User Processing\*[ \t]+(.*?)[ \t]+CMD:(.*)/ ) { my $header = $1; my %keypairs = map { ( split( m{:}, $_, 2 ) )[ 0, 1 ] } split( m{[ \t]+}, $2 ); my $cmd = $3; next if $keypairs{"Kill"} == "0"; push @killed_by_lfd, "[$header] " . join( " ", ( map { "[$_: $keypairs{$_}]" } sort keys %keypairs ) ) . " [cmd: $cmd]\n"; $count++; } last if $count >= 20; } } if (@killed_by_lfd) { chomp @killed_by_lfd; SSP::Util::print_warn("Last 20 processes killed by 3rd party software \"LFD\" (\"grep Kill:1 /var/log/lfd.log\"):\n"); for my $killed_process (@killed_by_lfd) { SSP::Util::print_magenta("\t \\_ $killed_process"); } } } sub check_for_processes_killed_by_oom { my @logs_to_check = qw( /var/log/messages /var/log/syslog /var/log/apache2/stderr.log /var/log/kern.log ); foreach my $log (@logs_to_check) { chomp($log); next unless -e $log; my $size = ( stat($log) )[7]; next unless defined $size; my $seek_position = 0; my $bytes_to_check = 10485760; if ( $size > $bytes_to_check ) { $seek_position = ( $size - $bytes_to_check ); } my $log_data; my $count = 0; my @killed_by_oom = (); open my $file_fh, '<', $log or return; seek $file_fh, $seek_position, 0; read $file_fh, $log_data, $bytes_to_check; close $file_fh; my $prefix_pattern = '^([a-zA-Z]{3} +\d{1,2} +\d{2}:\d{2}:\d{2}) (?:.*?) kernel:(?: \[ *\d*\.\d{6}\])?'; if ( $log_data =~ /[Kk]ill process|Out of memory|[Kk]illed process|Cannot allocate memory|lve PMEM limit|PHP memory_limit hit/ ) { my @logs = split /\n/, $log_data; undef $log_data; @logs = reverse @logs; for my $line (@logs) { # CloudLinux 6 LVE OOM, Virtuozzo + CentOS 6 OOM if ( $line =~ /$prefix_pattern Out of memory in UB (\d*): OOM killed process (\d*) \(([^)]+)\) score \d* vm:(\d*)kB, rss:(\d*)kB, swap:(\d*)kB/o ) { push @killed_by_oom, "[$1] [pid: $3] [cmd: $4] [LVE ID: $2] [vm: $5 kB] [rss: $6 kB] [swap: $7 kB]"; $count++; last if $count >= 10; } if ( $line =~ /$prefix_pattern Out of memory: OOM killed process (\d*?) \(([^)]+)\) score \d* vm:(\d*)kB, rss:(\d*)kB, swap:(\d*)kB/o ) { push @killed_by_oom, "[$1] [pid: $2] [cmd: $3] [vm: $4 kB] [rss: $5 kB] [swap: $6 kB]"; $count++; last if $count >= 10; } # CloudLinux 7 # Feb 6 08:52:43 lin02 kernel: Killed process 22541 (php) in VE "0" total-vm:357304kB, anon-rss:77080kB, file-rss:11116kB if ( $line =~ /$prefix_pattern Killed process (\d*) \(([^)]+)\) in VE "(\d*)" total-vm:(\d*)kB, anon-rss:(\d*)kB, file-rss:(\d*)kB/o ) { push @killed_by_oom, "[$1] [pid: $2] [cmd: $3] [LVE ID: $4] [total-vm: $5 kB] [anon-rss: $6 kB] [file-rss: $7 kB]" unless( $4 == 0); $count++; last if $count >= 10; } # CentOS 6/7, Ubuntu # Oct 8 12:51:37 whm-76-c7 kernel: Killed process 3172 (a.out) total-vm:571097852kB, anon-rss:547696kB, file-rss:4kB, shmem-rss:0kB # Oct 3 18:33:48 petersdevbox kernel: OOM killed process 14862 (spamd child) total-vm:246308kB, anon-rss:120088kB, file-rss:1168kB if ( $line =~ /$prefix_pattern (?:OOM\s)?[Kk]illed process (\d+)(?:, UID \d+,)? \(([^)]+)\) total-vm:(\d+)kB, anon-rss:(\d+)kB, file-rss:(\d+)kB/o ) { push @killed_by_oom, "[$1] [pid: $2] [cmd: $3] [total-vm: $4 kB] [anon-rss: $5 kB] [file-rss: $6 kB]"; $count++; last if $count >= 10; } if ( $line =~ /$prefix_pattern Out of memory: Kill process (\d+) \(([^)]+)\) score (\d+) or sacrifice child/o ) { push @killed_by_oom, "$1 Out of memory: Kill process [pid: $2] ($3) score $4 or sacrifice child"; $count++; last if $count >= 10; } # LiteSpeed if ( $line =~ /Cannot allocate memory|lve PMEM limit|PHP memory_limit hit/o ) { push @killed_by_oom, "$line"; $count++; last if $count >= 10; } } if (@killed_by_oom) { SSP::Util::print_warn("Last $count processes killed by Linux Out of memory killer (egrep -i \"Out of memory|OOM|out_of_memory|kill process|killed by\" $log):\n"); for my $killed_process (@killed_by_oom) { SSP::Util::print_magenta("\t \\_ $killed_process"); } } } } } sub check_for_processes_killed_by_prm { my @logfiles = qw( /usr/local/prm/prm_log /usr/local/prm/log_prm /usr/local/prm/logs/prm.log ); for my $log (@logfiles) { next if -l $log; next if !-f $log; my $size = ( stat($log) )[7]; next if !$size; my $bytes_to_check = 10485760; # 10M my $seek_position = 0; my $log_data; my $count = 0; my @killed_by_prm = (); if ( $size > $bytes_to_check ) { $seek_position = ( $size - $bytes_to_check ); } open my $file_fh, '<', $log or return; seek $file_fh, $seek_position, 0; read $file_fh, $log_data, $bytes_to_check; close $file_fh; if ( $log_data =~ /\sKILLED\s/ ) { my @logs = split /\n/, $log_data; undef $log_data; @logs = reverse @logs; for my $line (@logs) { if ( $line =~ /(\S+\s\d+\s\S+)\s(?:\S+)\s(?:\S+) proc pid:(?:\d+) \{user:(\S+) cmd:(\S+)\}.+(MAX_.+) KILLED/ ) { push @killed_by_prm, "[$1] [user: $2] [cmd: $3] [$4]"; $count++; } last if $count >= 10; } } if (@killed_by_prm) { SSP::Util::print_warn( "Last 10 processes killed by 3rd party software \"PRM\" (\"grep KILLED " . $log . "\"):\n" ); for my $killed_process (@killed_by_prm) { SSP::Util::print_magenta("\"\t \\_ $killed_process\""); } } } } sub check_for_broken_userdatadomains { return unless SSP::Util::i_am('cpanel'); return unless -f '/etc/userdatadomains'; open my $userdatadomains_fh, '<', '/etc/userdatadomains' or return; while (<$userdatadomains_fh>) { if (/^:/) { SSP::Util::print_warn('/etc/userdatadomains: '); SSP::Util::print_warning('contains a line that begins with ":". Check the following for accuracy (see 4416539 for examples):'); SSP::Util::print_magenta("\t \\_ /etc/userdatadomains"); SSP::Util::print_magenta("\t \\_ /var/cpanel/users/USER (check the ^DNS= lines)"); SSP::Util::print_magenta("\t \\_ /var/cpanel/userdata/USER/main (check for things like '')"); SSP::Util::print_magenta("\t \\_ /var/cpanel/userdata/USER/DOMAIN (check serveralias line)"); SSP::Util::print_magenta("\t \\_ /var/cpanel/userdata/USER/cache (userdatadomains uses this)"); SSP::Util::print_magenta("\t \\_ /usr/local/apache/conf/httpd.conf (may need rebuilding after fixing userdata)"); last; } } close $userdatadomains_fh; } sub check_ssl_db_perms { return unless SSP::Util::i_am('cpanel'); my $ssldb = '/var/cpanel/ssl/installed/ssl.db'; return unless -e $ssldb; my ( $uid, $gid ) = ( stat($ssldb) )[ 4, 5 ]; if ( $uid != 0 or $gid != 0 ) { SSP::Util::print_warn("$ssldb: "); SSP::Util::print_warning('not owned by the root user and/or group. This can prevent pkgacct from completing.'); } } sub check_for_stray_index_php { return unless SSP::Util::i_am('cpanel'); my $indexphp = '/usr/local/cpanel/base/index.php'; if ( -e $indexphp ) { SSP::Util::print_warn("$indexphp: "); SSP::Util::print_warning("exists! Errors when logging into cPanel?"); } } sub check_for_port_80_not_apache { return unless SSP::Util::i_am( 'ea4' ); return unless my $ports = SSP::Util::get_lsof_port_href(); return unless exists $ports->{'80'}; my ( $pid, $comm ) = @{ $ports->{'80'}->[0] }{qw( PID CMD )}; my $exe = readlink "/proc/${pid}/exe" or return; my $cwd = readlink "/proc/${pid}/cwd" or return; if ( $exe ne '/usr/local/apache/bin/httpd' && $exe ne '/usr/sbin/httpd' ) { open my $file_fh, '<', "/proc/${pid}/cmdline" or return; my $cmdline = readline $file_fh; close $file_fh; return if !$cmdline; $cmdline =~ s/\0/ /g; $cmdline =~ s/(\s+)$//g; my $ipcs = SSP::Util::timed_run( 0, 'ipcs', '-m' ); my $supported_webserver = ""; $supported_webserver = YELLOW " [ Supported ea-nginx ]" if ( $exe =~ m{nginx} && -d '/etc/nginx/ea-nginx' ); $supported_webserver = YELLOW " [ Supported LiteSpeed ]" if ( $exe =~ m{lshttpd} ); $supported_webserver = RED " [ UNSUPPORTED Varnish ]" if ( $exe =~ m{varnish} ); $supported_webserver = MAGENTA " [ cpsrvd MAILNODE/DNSNODE? ]" if ( $exe =~ m{cpsrvd} ); SSP::Util::print_warn('Port 80: '); SSP::Util::print_warning("something other than Apache is running: $supported_webserver"); SSP::Util::print_magenta("\t \\_ cmd [$comm]"); SSP::Util::print_magenta("\t \\_ exe [$exe]"); SSP::Util::print_magenta("\t \\_ cmdline [$cmdline]"); SSP::Util::print_magenta("\t \\_ cwd [$cwd]"); SSP::Util::print_magenta($ipcs) if ( $ipcs =~ /nobody/ ); } } sub check_for_missing_groups { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); my @groups = qw( cpanel cpaneleximfilter cpaneleximscanner cpanelanalytics cpanelcabcache cpanelconnecttrack cpaneldemo cpanellogin cpanelphpmyadmin cpanelphppgadmin cpanelroundcube cpses mail mailman mailnull mailtrap mysql named nobody root ); if ( SSP::Util::i_am( 'dnsonly' )) { # CX-203 - mailman is not part of DNSOnly - remove it from array @groups = grep {! /mailman/} @groups; } if ( SSP::Util::i_am( 'ubuntu' ) ) { push @groups, "sudo"; push @groups, "ssh" if(SSP::Util::os_version_is(qw( < 22.04 ))); push @groups, "_ssh" if(SSP::Util::os_version_is(qw( > 20.04 ))); } else { push @groups, "wheel"; push @groups, "sshd"; } my $missing_groups; for my $group (@groups) { my $gid = getgrnam($group); next if ( defined $gid and $gid =~ /^\d+$/ ); $missing_groups .= "[$group] "; } if ($missing_groups) { SSP::Util::print_warn('Missing groups: '); SSP::Util::print_warning($missing_groups); } } sub check_for_noquotafs { my $noquotafs = '/var/cpanel/noquotafs'; return if not -f $noquotafs or -z $noquotafs; SSP::Util::print_warn("$noquotafs: "); SSP::Util::print_warning('exists. quota issues? See https://docs.cpanel.net/whm/account-functions/the-quota-file-systems-configuration-file/'); } sub check_for_roundcube_overlay { my $rcdir = '/var/cpanel/roundcube'; return unless -d $rcdir; opendir( my $dh, $rcdir ); my @contents = grep { !/^\.\.?$/ } readdir $dh; close $dh; if ( grep { /^overlay/ } @contents ) { SSP::Util::print_warn('Roundcube overlay: '); SSP::Util::print_warning("found an overlay file in $rcdir . Login issues?"); } } sub check_for_hostname_park_zoneexists { return unless SSP::Util::i_am('cpanel'); return if ( SSP::Util::license_file_is_solo() ); my $hostname = SSP::Util::get_hostname(); if ( -f "/var/named/$hostname.db" and ( not defined $CPCONF{'allowparkonothers'} or $CPCONF{'allowparkonothers'} != 1 ) ) { SSP::Util::print_warn('Parking: '); SSP::Util::print_warning('since zone of hostname exists, "Allow cPanel users to create subdomains across accounts" must be ON to park on the hostname'); } } sub check_user_beancounters_failcnt { my $proc_ubc = '/proc/user_beancounters'; return unless -e $proc_ubc; return if SSP::Util::i_am( 'cloudlinux' ); return unless open( my $fh, '<', $proc_ubc ); my $cnt=0; my $showheader=0; while( <$fh> ) { last if ($cnt > 2); next if $_ =~ m/uid|resource|held|maxheld|barrier|limit|failcnt|Version/; if ( $_ =~ /([a-zA-Z]{1,12}\s*\d*\s*\d*\s*\d*\s*\d*\s*\d*)/ ) { my ( $resource, $failcnt ) = (split( /\s+/, $1 ))[0,5]; next unless( $failcnt ); if ( $showheader == 0 ) { SSP::Util::print_warn("/proc/user_beancounters has some failcnts > 0:\n") unless( $failcnt == 0 ); $showheader=1 unless( $failcnt == 0 );; } SSP::Util::print_warning("\t\\_ $resource has a failcnt of $failcnt") unless( $failcnt == 0 ); $cnt++ if $failcnt > 0; } } close($fh); return; } sub check_for_pgpass_colon_in_password_field { my $pgpass = '/root/.pgpass'; return if !-f $pgpass; if ( open my $fh, '<', $pgpass ) { while (<$fh>) { if (/^\*:\*:\*:postgres:(.*)/) { if ( $1 =~ /:/ ) { SSP::Util::print_warn("$pgpass: "); SSP::Util::print_warning('password field contains a colon. Seeing Postgres auth issues? See FB-89093'); last; } } } close $fh; } } sub check_for_extra_uid_0_user { my @uid_0_users; open my $file_fh, '<', '/etc/passwd' or die $!; while (<$file_fh>) { if (/\A([^:]+):[^:]+:0:/) { next if $1 eq 'root'; push @uid_0_users, $1; last if scalar @uid_0_users >= 5; } } close $file_fh; if (@uid_0_users) { my $info; SSP::Util::print_warn('non-root UID 0 user(s) found:'); for my $user (@uid_0_users) { $info .= ' [' . $user . ']'; next unless ( scalar @uid_0_users < 6 ); } $info .= ' (limit of 5, there may be more!)' if scalar @uid_0_users >= 5; $info .= ' -- and nscd is running! This can break things. See CPANEL-2360.' if SSP::Util::exists_process_cmd(qr{bin/nscd (?:\s|$)}xms); # Don't specify root user here, it may not match. SSP::Util::print_warning($info); my @suspectusers = grep { /cpanellogin/ } @uid_0_users; if ( scalar( @suspectusers > 0 )) { SSP::Security::print_generic_hack_predef('Potential Root Compromise'); SSP::Util::print_critical('User with UID 0 found known to be associated with root compromises!' ); for my $suspectuser (@suspectusers) { SSP::Util::print_critical( "\t\\_ $suspectuser" ); } } } } sub check_sudoers_files { my @sudoersfiles = glob(q{/etc/sudoers.d/*}); push @sudoersfiles, "/etc/sudoers" unless( !-e "/etc/sudoers" ); my $showHeader=0; foreach my $sudoerfile (@sudoersfiles) { chomp($sudoerfile); next if ( $sudoerfile =~ m{/etc/sudoers.d/ticket[0-9]} ); open( my $fh, '<', $sudoerfile ); my @sudoers = <$fh>; close( $fh ); foreach my $sudoerline (@sudoers) { chomp($sudoerline); next if ( $sudoerline =~ m/^(#|$|root|Defaults|%wheel|%sudo|%admin)/ ); next if ( $sudoerline =~ m/centos|ubuntu|wp-toolkit|cloud-user|rocky/ ); next unless ( $sudoerline =~ m/ALL$/ ); SSP::Util::print_warn( "Found non-root users with insecure privileges in a sudoer file.\n" ) unless($showHeader == 1); $showHeader = 1; if ( $sudoerline =~ m/ALL, !root/ ) { SSP::Util::print_warning( "\t\\_ $sudoerfile: $sudoerline has !root - might be susceptible to CVE-2019-14287" ); } else { SSP::Util::print_warning( "\t\\_ $sudoerfile: $sudoerline" ); } if ( $sudoerline =~ m/cpanellogin/ ) { SSP::Security::check_cpanellogin_in_sudoers() unless( $sudoerline =~ m/!cpanellogin/ ); return; } } } } sub check_for_allow_update_in_named_conf { my $namedconf = '/etc/named.conf'; return if !-e $namedconf; if ( open my $fh, '<', $namedconf ) { while (<$fh>) { if (/allow-update/i) { SSP::Util::print_warn('named.conf: '); SSP::Util::print_warning('allow-update found. This can possibly prevent rndc from reloading.'); last; } last if /^\s*view /i; } close $fh; } } sub check_for_modsec2_stage_files { return unless SSP::Util::i_am_one_of( 'ea4' ); my $file = '/etc/apache2/conf/modsec2.user.conf.STAGE'; return unless -e $file; SSP::Util::print_warn('Mod Security: '); SSP::Util::print_warning('/usr/local/apache/conf/modsec2.user.conf.STAGE exists -- This may prevent editing of rules in WHM ModSecurity Tools.'); } sub check_for_cron_allow { return unless SSP::Util::i_am('cpanel'); my $cron_allow = "/etc/cron.allow"; my $cron_deny = "/etc/cron.deny"; if ( -s $cron_deny ) { # By default /etc/cron.deny can contain the "nobody" user. Checking the contents can be expensive, size will do. my $cron_deny_size = ( stat($cron_deny) )[7]; my $cron_deny_max_size = 8; if ( $cron_deny_size > $cron_deny_max_size ) { SSP::Util::print_warn('crontab: '); SSP::Util::print_warning( $cron_deny . ' is > ' . $cron_deny_max_size . ' bytes, may contain users other than "nobody". A user listed here cannot see or edit cron jobs in the cPanel UI.' ); } } if ( -e $cron_allow ) { SSP::Util::print_warn('crontab: '); SSP::Util::print_warning('/etc/cron.allow exists. Any user NOT listed cannot see or edit cron jobs in the cPanel UI.'); } } sub check_for_broken_mysqldump { return if SSP::Util::i_am('dnsonly'); return unless -f '/usr/bin/mysql'; my $md = '/usr/bin/mysqldump'; if ( !-x $md ) { SSP::Util::print_warn("$md: "); SSP::Util::print_warning('not found or not executable!'); return; } local $?; SSP::Util::timed_run( 0, $md ); my $exit_status = $? >> 8; if ( $exit_status != 1 ) { # Running with no options is error 1 SSP::Util::print_warn("$md: "); SSP::Util::print_warning('may be broken (exit status != 1).'); } } sub check_exim_log_sanity { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); my @logs = qw( /var/log/exim_mainlog /var/log/exim_paniclog /var/log/exim_rejectlog ); for my $log (@logs) { if ( !-f $log ) { SSP::Util::print_warn("$log: "); SSP::Util::print_warning('is missing!'); } else { my $uid = ( stat($log) )[4]; my $user = getpwuid($uid); if ( $user ne 'mailnull' ) { SSP::Util::print_warn("$log: "); SSP::Util::print_warning('is not owned by "mailnull"'); } } } } sub check_exim_localopts { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); return unless my $exim_localopts = SSP::Util::get_exim_localopts_href(); # Example: 'exact_option_name' => { default => 'defaultvalue', check_missing => 1, help => '- Help text' }, # default, check_missing, and help are optional # If check_missing exists, a warning is generated if the item is NOT in exim.conf.localopts and does not match the default (if given) or is empty my %checks = ( 'allowweakciphers' => { default => '0', help => '"Allow weak SSL/TLS ciphers" enabled, this removes tls_require_ciphers from exim.conf. It is better to disable this and customize "SSL/TLS Cipher Suite List" instead.' }, ); for my $check ( sort( keys(%checks) ) ) { $checks{$check}->{check_missing} = 0 unless defined( $checks{$check}->{check_missing} ); $checks{$check}->{help} = '' unless defined( $checks{$check}->{help} ); my $help = $checks{$check}->{help} ? ' - ' . $checks{$check}->{help} : ''; if ( defined( $exim_localopts->{$check} ) && defined( $checks{$check}->{default} ) && !( $exim_localopts->{$check} eq $checks{$check}->{default} ) ) { SSP::Util::print_warn('/etc/exim.conf.localopts: '); SSP::Util::print_warning("[ $check = $exim_localopts->{$check} ] $help"); } elsif ( $checks{$check}->{check_missing} && ( !defined( $exim_localopts->{$check} ) || ( defined( $exim_localopts->{$check} ) && $exim_localopts->{$check} eq '' ) ) ) { SSP::Util::print_warn('/etc/exim.conf.localopts: '); SSP::Util::print_warning("[ $check ] not found or has empty value. $help"); } } } sub check_for_readonly_filesystems { open my $fh, '<', '/proc/mounts' or return; my @read_only_fs = (); while (<$fh>) { if ( my @fs = split(' ') ) { next if grep { m{ / (virtfs|cagefs-skeleton|machine-id|snap|credentials) / }x } $fs[1]; next if "/sys/fs/cgroup" eq $fs[1] and SSP::Util::os_version_is(qw( >= 7 )); if ( grep { m{ (^|,) ro (,|$) }x } $fs[3] ) { push( @read_only_fs, $fs[1] ); } } } if ( scalar @read_only_fs ) { SSP::Util::print_warn('Read-only filesystems: '); SSP::Util::print_warning( join( " ", @read_only_fs ) ); } close($fh); } sub check_for_cl_unsupported_memory_limits { return unless SSP::Util::i_am('cloudlinux'); return unless SSP::Util::os_version_is(qw( < 6 )); return unless SSP::Util::i_am( 'ea4' ); my ( $lsws_full_version, $lsws_numeric_version ) = @{ SSP::Util::get_lsws_version_aref() }; # Several memory limits are not imposed: URL: https://helpdesk.cloudlinux.com/index.php?/Knowledgebase/Article/View/69/3/memory-limits-are-not-working # Per Igor: # CL 5 - doesn't support memory limits for: mod_php, mod_ruid2, MPM ITK & LiteSpeed. # CL 6 - doesn't support memory limits for: mod_php & mod_ruid2 if ($lsws_full_version) { SSP::Util::print_warn('LiteSpeed: '); SSP::Util::print_warning('in use on CL5 or earlier. CloudLinux memory limits not imposed on Apache processes.'); } } sub check_memory_limit_in_bytes_value { open my $fh, '<', '/sys/fs/cgroup/memory/memory.limit_in_bytes' or return; my $memory_in_bytes = <$fh>; close($fh); chomp($memory_in_bytes); return unless( $memory_in_bytes > '9223372036854775807' ); SSP::Util::print_warn('Memory Limit In Bytes: '); SSP::Util::print_warning( "/sys/fs/cgroup/memory/memory.limit_in_bytes value too large [$memory_in_bytes] - Seeing Java/Dovecot-Solr issues?" ); } sub check_for_eblockers { return unless -e '/var/cpanel/update_blocks.config'; open my $blocker_fh, '<', '/var/cpanel/update_blocks.config' or return; SSP::Util::print_warn("WHM Update Blocker found:\n"); while (<$blocker_fh>) { chomp; next if /^$/; $_ =~ s|<.+?>||g; SSP::Util::print_magenta("\t \\_ $_"); } close $blocker_fh; } sub check_cloudlinux_sanity { return unless SSP::Util::i_am('cloudlinux'); if ( !-x "/usr/sbin/lvectl" ) { SSP::Util::print_warn('CloudLinux: '); SSP::Util::print_warning('missing or non-executable /usr/sbin/lvectl - check/repair lve-utils PACKAGE'); } if ( !-x '/usr/bin/selectorctl' ) { SSP::Util::print_warn('CloudLinux: '); SSP::Util::print_warning('missing or non-executable /usr/bin/selectorctl - check/repair lvemanager PACKAGE - This will incorrectly cause cPanel EA4 to be used.'); } if ( !-x '/usr/sbin/lveinfo' ) { SSP::Util::print_warn('CloudLinux: '); SSP::Util::print_warning('missing or non-executable /usr/sbin/lveinfo - check/repair lve-stats PACKAGE - This will incorrectly cause cPanel EA4 to be used.'); } if ( !-x '/usr/sbin/spacewalk-channel' ) { SSP::Util::print_warn('CloudLinux: '); SSP::Util::print_warning('missing or non-executable /usr/sbin/spacewalk-channel - check/repair rhn-setup PACKAGE'); } if ( !-x '/usr/bin/python' && SSP::Util::os_version_is(qw ( < 8 )) ) { SSP::Util::print_warn('CloudLinux: '); SSP::Util::print_warning('missing or non-executable /usr/bin/python - check/repair python PACKAGE'); } if ( -e '/var/.cagefs' ) { SSP::Util::print_warn('CloudLinux: '); SSP::Util::print_warning('/var/.cagefs - EXISTS! Should never exist outside of CageFS, will break many things.'); } if ( -x '/usr/bin/cldiag' ) { local $?; my @cldiag_switches = qw( symlinksifowner check-suexec check-suphp check-usepam check-phpselector check-multi-php check-cagefs check-symlinkowngid check-cpanel-packages check-php-conf check-defaults-cfg check-hidepid ); my $cldiag_output; foreach my $cldiag_switch (@cldiag_switches) { $cldiag_output = SSP::Util::timed_run( 0, '/usr/bin/cldiag', "--$cldiag_switch" ); my $cldiag_output_status = $? >> 8; if ( $cldiag_output_status ) { SSP::Util::print_warn('CloudLinux: '); SSP::Util::print_warning("[ /usr/bin/cldiag --$cldiag_switch ] returned non-zero exit status, run it for more information."); } } } my $cldetect = '/usr/bin/cldetect'; my $check_license_opt = '--check-license'; if ( -x $cldetect ) { my $cldetect_help = SSP::Util::timed_run_trap_stderr( 0, $cldetect, '--help' ); if ( defined $cldetect_help and $cldetect_help =~ m{ $check_license_opt }xms ) { local $?; SSP::Util::timed_run( 0, $cldetect, $check_license_opt, '-q' ); # The -q is necessary for it to use exit status my $cldetect_check_license_status = $? >> 8; if ($cldetect_check_license_status) { SSP::Util::print_warn('CloudLinux: '); SSP::Util::print_warning("[ $cldetect $check_license_opt ] returned an error, run it for more information. An invalid license can prevent CL repos appearing in Yum."); } } } } sub check_updatelog { my $log = '/var/cpanel/updatelogs/last'; return unless -e $log; my $size = ( stat($log) )[7]; return if !$size; my $bytes_to_read = 10485760; # 10M $bytes_to_read = $size if $bytes_to_read > $size; my $log_data; open my $file_fh, '<', $log or return; read $file_fh, $log_data, $bytes_to_read; close $file_fh; foreach ( split( "\n", $log_data ) ) { if (m{ installing .+ needs .+ on .+ filesystem }x) { SSP::Util::print_warn("WHM update: "); SSP::Util::print_warning( $log . ' contains errors which indicate that PACKAGE installation had insufficient free disk space available' ); return; } } } sub check_for_saltstack { return unless SSP::Util::exists_process_cmd( qr{ salt-minion }xms, 'root' ); SSP::Util::print_warn('SaltStack Minion: '); SSP::Util::print_warning('the salt-minion process is running. Seeing files being reverted, this may be why'); } sub check_for_puppet_agent { my $puppet_rpm_exists = 0; my $puppet_running = 0; my $puppet_logs_updated = 0; my $rpms = SSP::Util::get_rpm_href(); if ( exists $rpms->{'puppet'} ) { $puppet_rpm_exists = 1; } if ( SSP::Util::exists_process_cmd( qr{ puppet }xms, 'root' ) ) { $puppet_running = 1; } if ( -s '/var/log/puppet/puppet.log' && -s '/var/log/pe-puppetserver/puppetserver.log' ) { if ( -M '/var/log/puppet/puppet.log' < 1 || -M '/var/log/pe-puppetserver/puppetserver.log' < 1 ) { $puppet_logs_updated = 1; } } if ( $puppet_rpm_exists ) { SSP::Util::print_warn('Puppet Agent: '); SSP::Util::print_warning('the puppet package is installed. Seeing files being reverted, this may be why'); return; } if ( $puppet_running ) { SSP::Util::print_warn('Puppet Agent: '); SSP::Util::print_warning('the puppet process is running. Seeing files being reverted, this may be why'); return; } if ( $puppet_logs_updated ) { SSP::Util::print_warn('Puppet Agent: '); SSP::Util::print_warning('the puppet log were files recently updated. Seeing files being reverted, this may be why'); return; } } sub check_use_apache_md5_for_htaccess { return unless ( defined $CPCONF{'use_apache_md5_for_htaccess'} ); return if ( $CPCONF{'use_apache_md5_for_htaccess'} == 1 ); SSP::Util::print_warn('Tweak Setting: '); SSP::Util::print_warning('use_apache_md5_for_htaccess is disabled, htpasswd limits passwords to a length of 8 characters'); } sub check_pure_ftpd_conf_for_upload_script_and_dead { return unless SSP::Util::i_am('cpanel'); return unless defined( $CPCONF{'ftpserver'} ) && $CPCONF{'ftpserver'} eq 'pure-ftpd'; return unless my $pureftpd_conf = SSP::Util::get_pureftpd_conf_href(); if ( defined( $pureftpd_conf->{'calluploadscript'} ) && $pureftpd_conf->{'calluploadscript'}->{value} eq 'yes' ) { if ( !-e '/var/run/pure-ftpd.upload.pipe' ) { SSP::Util::print_warn("$PURE_FTPD_CONF_FILE: "); SSP::Util::print_warning("CallUploadScript set to yes, /var/run/pure-ftpd.upload.pipe is missing [might be broken ConfigServer's cxs ( http://configserver.com/cp/cxs.html ) or Imunify360]"); } else { unless ( SSP::Util::grep_process_cmd( 'pure-uploadscri', 'root' ) ) { SSP::Util::print_warn("$PURE_FTPD_CONF_FILE: "); SSP::Util::print_warning("CallUploadScript set to yes, but pure-uploadscript does not appear to be running. [might be broken ConfigServer's cxs ( http://configserver.com/cp/cxs.html ) or Imunify360]"); } } } } sub check_perl_version_less_than_588 { my $perl_version = $^V; if ( $perl_version =~ /^v(.+)$/ ) { $perl_version = $1; } return if !$perl_version; if ( SSP::Util::version_compare( $perl_version, qw( < 5.8.8 ) ) ) { SSP::Util::print_warn('Perl Version: '); SSP::Util::print_warning("less than 5.8.8: [ $perl_version ]"); } if ( SSP::Util::version_compare( $perl_version, qw( < 5.14.0 ) ) ) { SSP::Util::print_warn('Perl Version: '); SSP::Util::print_warning('better resolver results can be obtained when running SSP with Perl 5.14 or later'); } } sub check_sshd_config { return unless SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ); my $sshd_config = '/etc/ssh/sshd_config'; return unless my $conf = SSP::Util::get_flat_conf_href( $sshd_config, 1 ); # Example: 'lowercaseoption' => { default => 'lowercasedefaultvalue', check_missing => 1, orig_name => 'MixedCaseName', help => 'Help text' }, # default, check_missing, and help are optional, but orig_name should be defined if check_missing is used so that the mixed-case name can be printed # If check_missing exists, a warning is generated if the item is NOT in sshd_conf and does not match the default (if given) my %checks; my $add = sub { my ( $name, $new_ref ) = @_; $checks{$name}{'help'} = [] unless defined $checks{$name}{'help'}; for my $key ( keys %{$new_ref} ) { if ( $key eq 'help' ) { push @{ $checks{$name}{'help'} }, @{ $new_ref->{'help'} }; next; } $checks{$name}{$key} = $new_ref->{$key}; } }; if ( not -e '/etc/cphulkddisable' ) { $add->( 'usedns', { default => 'no', check_missing => 1, orig_name => 'UseDNS', help => ['Must be explicitly set to "no" for cPHulk to work correctly for SSH logins'] } ); $add->( 'usepam', { default => 'yes', check_missing => 1, orig_name => 'UsePAM', help => ['Must be explicitly set to "yes" for cPHulk to work with SSH logins'] } ); } if ( SSP::Util::i_am('cloudlinux') ) { $add->( 'usepam', { default => 'yes', check_missing => 1, orig_name => 'UsePAM', help => ['Must be explicitly set to "yes" for CloudLinux LVE limits or CageFS to apply to SSH sessions'] } ); } my $indent = 34; for my $lc_key ( sort( keys(%checks) ) ) { my $check = $checks{$lc_key}; my $lc_default = defined $check->{default} ? $check->{default} : ''; my $check_missing = defined $check->{check_missing} ? 1 : 0; my $missing_name = defined $check->{orig_name} ? $check->{orig_name} : $lc_key; my $help = scalar @{ $check->{help} } ? '- ' : ''; my ( $conf_key, $conf_value ) = defined $conf->{$lc_key} ? @{ $conf->{$lc_key} } : undef; my $lc_conf_value = defined $conf_value ? lc($conf_value) : undef; if ( defined $lc_conf_value and $lc_conf_value ne $lc_default ) { $help .= join( "\n" . ' ' x ( $indent + length($conf_key) + length($conf_value) ) . '\_ - ', @{ $check->{help} } ); SSP::Util::print_warn( $sshd_config . ': ' ); SSP::Util::print_warning("[ $conf_key $conf_value ] $help"); } elsif ( $check_missing and not defined $conf_value ) { $help .= join( "\n" . ' ' x ( $indent + length($missing_name) + 9 ) . '\_ - ', @{ $check->{help} } ); SSP::Util::print_warn( $sshd_config . ': ' ); SSP::Util::print_warning("[ $missing_name ] not found $help"); } } } sub count_undefined_packages { my $undefined_cnt=0; my @dir_contents; my $file_fh; opendir( my $dir_fh, '/var/cpanel/users' ) or return; @dir_contents = grep { !/^\.\.?$|system|nobody/ } readdir $dir_fh; closedir $dir_fh; for my $file (@dir_contents) { $file = '/var/cpanel/users/' . $file; open $file_fh, '<', $file or next; ## no critic (InputOutput::RequireBriefOpen) while (<$file_fh>) { next unless( $_ =~ m{PLAN=undefined} ); $undefined_cnt++; } close( $file_fh ); } return unless( $undefined_cnt >= 1 ); my $is_are = ( $undefined_cnt == 1) ? "is" : "are"; SSP::Util::print_warn( 'Undefined PLAN: ' ); SSP::Util::print_warning("There $is_are $undefined_cnt user accounts with the PLAN set to undefined. See CPANEL-39573"); } sub verify_ca_certificates { # TECH-1448 return unless( -s '/usr/local/cpanel/logs/license_log' ); my %rpms; my $rpms = SSP::Util::get_rpm_href(); return unless( $rpms ); return unless( exists $rpms->{'ca-certificates'} ); my $ca_verify = 0; open( my $fh, '<', '/usr/local/cpanel/logs/license_log' ); while( <$fh> ) { if ($_ =~ m{Transition to SSL failed on 2089|SSL connect attempt failed error:|tls_process_server_certificate:certificate verify failed} ) { $ca_verify = 1; last; } } close( $fh ); if ( $ca_verify ) { SSP::Util::print_warn( 'CA Certificates: ' ); SSP::Util::print_warning("The ca-certificates package contains errors. Seeing license issues? Trouble connecting to secure locations? Verify the ca-certificates package"); } } sub check_entropy_avail { return if( SSP::Util::exists_process_cmd( qr{ rngd }xms, 'root' ) ); return if( SSP::Util::exists_process_cmd( qr{ haveged }xms, 'root' ) ); my $entropy_avail = SSP::Util::timed_run( 3, 'cat', '/proc/sys/kernel/random/entropy_avail' ); if ( $entropy_avail < 256 ) { SSP::Util::print_warn( 'Entropy: ' ); SSP::Util::print_warning("available entropy is too low and neither rngd nor haveged are running."); } } sub check_if_splitlogs_running { return unless SSP::Util::i_am('cpanel'); return unless defined $CPCONF{'enable_piped_logs'}; return if( SSP::Util::exists_process_cmd( qr{ splitlogs }xms, 'root' ) ); if ( $CPCONF{'enable_piped_logs'} ) { SSP::Util::print_warn( 'Splitlogs: ' ); SSP::Util::print_warning("is enabled but splitlogs binary isn't running. May cause rebuildhttpdconf to fail!"); } } sub check_queueprocd_for_infinite_loops { my @taskqueue_json_files = qw( /var/cpanel/taskqueue/servers_sched.json /var/cpanel/taskqueue/servers_queue.json ); my $queueprocd_loop=0; foreach my $file(@taskqueue_json_files) { chomp($file); my $size = ( stat($file) )[7]; if ( $size > 2048 ) { $queueprocd_loop++; } } my $logfile = '/usr/local/cpanel/logs/queueprocd.log'; my $access_time = ( stat($logfile) )[9]; my $time_now = time(); my $timediff = $time_now - $access_time; if ( $timediff > 86400 ) { $queueprocd_loop++; } if ( my %procs = SSP::Util::grep_process_cmd( qr{ queueprocd }xms, 'root' ) ) { my $cpu=map { my ( $C ) = @{ $procs{$_} } { 'CPU' }; "$C" } keys %procs; if ( $cpu > 60 ) { $queueprocd_loop++; } } SSP::Util::print_warn( 'Queueprocd Service: ' ) if ( $queueprocd_loop >= 2 ); SSP::Util::print_warning("found indication this process may be stuck. May cause a myriad of problems.") if ( $queueprocd_loop > 2 ); } sub check_cp_major_version_yum_dnf { # CX-775 return if( SSP::Util::i_am('ubuntu')); return unless( SSP::Util::i_am('cpanel')); my @files=qw( /etc/yum/vars/cp_centos_major_version /etc/dnf/vars/cp_centos_major_version ); my ( $os, $dist, $maj, $min, $bld ) = SSP::Util::get_os_info(); foreach my $file(@files) { chomp($file); open( my $fh, '<', $file ) or next; while( <$fh> ) { chomp($_); next if( $_ eq ""); if ( $_ ne $maj ) { SSP::Util::print_warn( 'CP YUM/DNF: ' ); SSP::Util::print_warning( "The value in $file does not match the Major OS value ($maj). Can cause repo issues. Add ticket to CPANEL-42170"); SSP::Util::print_warning( "\t\\_ Before fixing, please try to find out why/how this happened and add your findings to the case."); } } close( $fh ); } } ############################## # END [WARN] CHECKS ############################## 1; SSP_WARN s/^ //mg for values %fatpacked; my $class = 'FatPacked::'.(0+\%fatpacked); no strict 'refs'; *{"${class}::files"} = sub { keys %{$_[0]} }; if ($] < 5.008) { *{"${class}::INC"} = sub { if (my $fat = $_[0]{$_[1]}) { my $pos = 0; my $last = length $fat; return (sub { return 0 if $pos == $last; my $next = (1 + index $fat, "\n", $pos) || $last; $_ .= substr $fat, $pos, $next - $pos; $pos = $next; return 1; }); } }; } else { *{"${class}::INC"} = sub { if (my $fat = $_[0]{$_[1]}) { open my $fh, '<', \$fat or die "FatPacker error loading $_[1] (could be a perl installation issue?)"; return $fh; } return; }; } unshift @INC, bless \%fatpacked, $class; } # END OF FATPACK CODE # SSP - System Status Probe (Main module) # Find and print useful troubleshooting info on cPanel servers =head1 COPYRIGHT This software is Copyright 2024 WebPros International, LLC. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THE SOFTWARE LICENSED HEREUNDER IS PROVIDED "AS IS" AND WEBPROS INTERNATIONAL, LLC D/B/A/ CPANEL (CPANEL) HEREBY DISCLAIMS ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, RELATING TO THE SOFTWARE, ITS THIRD PARTY COMPONENTS, AND ANY DATA ACCESSED THEREFROM, OR THE ACCURACY, TIMELINESS, COMPLETENESS, OR ADEQUACY OF THE SOFTWARE, ITS THIRD PARTY COMPONENTS, AND ANY DATA ACCESSED THEREFROM, INCLUDING THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. CPANEL DOES NOT WARRANT THAT THE SOFTWARE OR ITS THIRD PARTY COMPONENTS ARE ERROR-FREE OR WILL OPERATE WITHOUT INTERRUPTION. IF THE SOFTWARE, ITS THIRD PARTY COMPONENTS, OR ANY DATA ACCESSED THEREFROM IS DEFECTIVE, YOU ASSUME THE SOLE RESPONSIBILITY FOR THE ENTIRE COST OF ALL REPAIR OR INJURY OF ANY KIND, EVEN IF CPANEL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DEFECTS OR DAMAGES. NO ORAL OR WRITTEN INFORMATION OR ADVICE GIVEN BY CPANEL, ITS AFFILIATES, LICENSEES, DEALERS, SUB-LICENSORS, AGENTS OR EMPLOYEES SHALL CREATE A WARRANTY OR IN ANY WAY INCREASE THE SCOPE OF ANY WARRANTY. =cut package SSP; use lib 'lib'; use 5.006; use strict; use warnings; use Getopt::Long(); use Term::ANSIColor qw(:constants); $Term::ANSIColor::AUTORESET = 1; if ( $^O ne 'linux' ) { die "Unknown OS: $^O (only Linux is supported)"; } if ( $< != 0 ) { die "SSP must be run as root\n"; } use SSP::Util (); use SSP::Info (); use SSP::Warn (); use SSP::Crit (); use SSP::Security (); use SSP::Misc (); our $OPT_SKIP_NETWORKING; # Disable network calls our $OPT_TIMEOUT; # How long to wait for system commands to finish executing # Global variables initialized at application initialization our %CPCONF; # cpanel.config our $ORIGINAL_PATH; our %MEMOIZE_CACHE; run(@ARGV) unless caller; sub run { local @ARGV = @_; # Because GetOptionsFromArray available in Getopt::Long 2.36 and later only, Perl 5.8.8 on CentOS 5.11 includes 2.35 Getopt::Long::GetOptions( 'help' => sub { exit SSP::Util::print_help(); }, ## no critic (NoExitsFromSubroutines) 'bugreport' => sub { SSP::Util::init(); exit SSP::Util::print_bug_report(); }, 'csi' => sub { SSP::Util::init(); exit SSP::Security::csi_checks_only(); }, 'sshcheck' => sub { SSP::Util::init(); exit SSP::Misc::sshcheck(); }, 'docreport' => sub { SSP::Util::init(); exit SSP::Util::print_doc_bug_report(); }, 'no-network' => \$OPT_SKIP_NETWORKING, 'no-speed' => sub { $MEMOIZE_CACHE{'PRECACHE'} = { disabled => 1 }; }, 'profiling' => sub { SSP::Util::load_module_with_fallbacks( 'needed_subs' => [qw{tv_interval gettimeofday}], 'modules' => [qw{Time::HiRes}], 'fail_warning' => 'Profiling won\'t work without this', 'fail_fatal' => 1, ); $MEMOIZE_CACHE{'PROFILING'} = { enabled => 1 }; }, 'simulatestate=s@' => sub { SSP::Util::_simulate_run_state( $_[1] ); }, 'simulatevar=s%' => sub { SSP::Util::_simulate_run_var( $_[1], $_[2] ); }, 'timeout=i' => \$OPT_TIMEOUT, ); if ($OPT_TIMEOUT) { $OPT_TIMEOUT = int $OPT_TIMEOUT; if ( $OPT_TIMEOUT < 5 ) { $OPT_TIMEOUT = 5; } } SSP::Util::init(); print "\n"; for ( 1 .. 3 ) { print BOLD GREEN ON_RED "\tPlease DO NOT paste output from SSP into tickets unless it is relevant to an issue" . RESET . "\n"; } if ( SSP::Util::i_am('wp2') ) { SSP::Util::print_start("\n\t\tWP2: "); SSP::Util::print_warning("profile detected, assuming this is a WP Squared license\n"); } if ( SSP::Util::i_am('dnsonly') ) { SSP::Util::print_start("\n\t\tDNSONLY: "); SSP::Util::print_warning("/var/cpanel/dnsonly or DNSONLY license detected, assuming DNSONLY operation\n"); } if ( SSP::Util::i_am('mailnode') ) { SSP::Util::print_start("\n\t\tMAILNODE "); SSP::Util::print_warning("profile detected, assuming this is a MAILNODE server\n"); } if ( SSP::Util::i_am('databasenode') ) { SSP::Util::print_start("\n\t\tDATABASENODE "); SSP::Util::print_warning("profile detected, assuming this is a DATABASENODE server\n"); } if ( SSP::Util::i_am('dnsnode') ) { SSP::Util::print_start("\n\t\tDNSNODE "); SSP::Util::print_warning("profile detected, assuming this is a DNSNODE server\n"); } unless ( SSP::Util::i_am_one_of( 'cpanel', 'dnsonly' ) ) { SSP::Util::print_critical("\nCPANEL IS NOT INSTALLED ON THIS SERVER! SOME SSP OUTPUT MAY NOT BE RELEVANT!\n"); } print "\n"; SSP::Misc::print_tip(); SSP::Util::print_version(); SSP::Security::check_for_hacked_server_touchfile(); SSP::Crit::check_for_multiple_tech_logins(); SSP::Crit::check_for_lve_environment(); SSP::Crit::check_for_os_release_5(); SSP::Crit::check_for_centos_8(); SSP::Crit::check_for_ubuntu_less_than_102(); SSP::Crit::check_for_os_release_32bit(); SSP::Crit::check_for_ea3(); SSP::Crit::check_for_eula(); SSP::Crit::check_ubuntu_release_value(); SSP::Crit::check_for_failed_install(); SSP::Util::find_httpd_bin(); # Cache result now, it is used by get_apache_* below. The following must be memoized in init() first. SSP::Util::_memoize_parallel_populate_cache( qw( check_for_non_default_permissions get_apache_modules_href get_apache_version_href get_clock_skew get_external_license_ip get_installed_ea4_php_href get_license_info get_local_ipaddrs_aref get_lsof_port_href get_process_pid_href get_rpm_href get_tiers_file get_tiers_json_href ) ); print "\n"; SSP::Info->run(); SSP::Warn->run(); SSP::Misc->run(); SSP::Crit->run(); SSP::Security->run(); SSP::Warn::check_for_additional_rpms(); SSP::Warn::check_for_broken_rpm(); SSP::Warn::check_for_ea4_mismatch(); SSP::Util::print_info2('Done.'); SSP::Util::print_footer(); SSP::Util::print_profiling_data(); return 0; } 1;