%PDF- %PDF-
Direktori : /home/kfvehpdt/public_html/wp-content/plugins/sucuri-scanner/src/ |
Current File : /home/kfvehpdt/public_html/wp-content/plugins/sucuri-scanner/src/event.lib.php |
<?php /** * Code related to the event.lib.php interface. * * PHP version 5 * * @category Library * @package Sucuri * @subpackage SucuriScanner * @author Daniel Cid <dcid@sucuri.net> * @copyright 2010-2018 Sucuri Inc. * @license https://www.gnu.org/licenses/gpl-2.0.txt GPL2 * @link https://wordpress.org/plugins/sucuri-scanner */ if (!defined('SUCURISCAN_INIT') || SUCURISCAN_INIT !== true) { if (!headers_sent()) { /* Report invalid access if possible. */ header('HTTP/1.1 403 Forbidden'); } exit(1); } /** * System events, reports and actions. * * An event is an action or occurrence detected by the program that may be * handled by the program. Typically events are handled synchronously with the * program flow, that is, the program has one or more dedicated places where * events are handled, frequently an event loop. Typical sources of events * include the user; another source is a hardware device such as a timer. Any * program can trigger its own custom set of events as well, e.g. to communicate * the completion of a task. A computer program that changes its behavior in * response to events is said to be event-driven, often with the goal of being * interactive. * * @category Library * @package Sucuri * @subpackage SucuriScanner * @author Daniel Cid <dcid@sucuri.net> * @copyright 2010-2018 Sucuri Inc. * @license https://www.gnu.org/licenses/gpl-2.0.txt GPL2 * @link https://wordpress.org/plugins/sucuri-scanner */ class SucuriScanEvent extends SucuriScan { /** * Creates a cronjob to run the file system scanner. * * Right after a fresh installation of the plugin, it will create a cronjob * that will execute the first scan in the next five minutes. This scan will * set the base-line for the file monitor through the API service. When a new * scan is execute the API will compare the checksum from the previous file * list with the checksum of the new file list, if there are differences we * will assume that someone or something modified one or more files and send * an email alsert about the incident. * * @return void */ public static function installScheduledTask() { $task_name = 'sucuriscan_scheduled_scan'; if (!wp_next_scheduled($task_name)) { wp_schedule_event(time() + 10, 'daily', $task_name); } } /** * Returns a list of available cronjob frequencies. * * This method will return not only the default WordPress cronjob frequencies * but also the custom ones defined by 3rd-party plugins or themes. It will * also add an additional option to allow the website owners to disable the * schedule tasks from the settings page. * * @see https://developer.wordpress.org/reference/functions/wp_get_schedules/ * * @return array List of available cronjob frequencies. */ public static function availableSchedules() { $schedules = array(); $jobs = wp_get_schedules(); foreach ($jobs as $unique => $info) { $schedules[$unique] = sprintf( __('%s (every %d seconds)', 'sucuri-scanner'), $info['display'], $info['interval'] ); } $schedules['_oneoff'] = __('Never (no execution)', 'sucuri-scanner'); return $schedules; } /** * Returns a list of active cronjobs. * * This method will return not only the default WordPress cronjobs but also * the custom ones defined by 3rd-party plugins or themes. * * @see https://developer.wordpress.org/reference/functions/_get_cron_array/ * * @return array List of available cronjobs. */ public static function activeSchedules() { $activeCrons = array(); foreach ((array) _get_cron_array() as $timestamp => $cronhooks) { foreach ((array) $cronhooks as $hook => $events) { foreach ((array) $events as $key => $event) { if (empty($event['args'])) { $event['args'] = array('[]'); } $activeCrons[$hook] = array( 'schedule' => $event['schedule'], 'nextTime' => SucuriScan::datetime($timestamp), 'nextTimeHuman' => SucuriScan::humanTime($timestamp), 'arguments' => json_encode($event['args']), ); } } } return $activeCrons; } /** * Creates the cronjob weekly, monthly and quarterly frequencies. * * A few Sucuri services require additional cronjob frequencies that are not * available on WordPress by default. This function will add these schedules * frequency if they were not yet register by any a 3rd party extension. * * @return void */ public static function additionalSchedulesFrequencies($schedules) { if (!defined('MONTH_IN_SECONDS')) { define('MONTH_IN_SECONDS', 30 * DAY_IN_SECONDS); } if (!isset($schedules['weekly'])) { $schedules['weekly'] = array( 'display' => __('Weekly', 'sucuriscan'), 'interval' => WEEK_IN_SECONDS, ); } if (!isset($schedules['monthly'])) { $schedules['monthly'] = array( 'display' => __('Monthly', 'sucuriscan'), 'interval' => MONTH_IN_SECONDS, ); } if (!isset($schedules['quarterly'])) { $schedules['quarterly'] = array( 'display' => __('Quarterly', 'sucuriscan'), 'interval' => 3 * MONTH_IN_SECONDS, ); } return $schedules; } /** * Creates a cronjob. * * @return bool True if the cronjob is correctly created. */ public static function addScheduledTask($hookName, $frequency) { // Return false if schedule frequency does not exist. if (!in_array($frequency, array_keys(self::availableSchedules()))) { return false; } // Remove cron first if already exists. if (wp_next_scheduled($hookName)) { self::deleteScheduledTask($hookName); } // Add cron job hook. wp_schedule_event(time() + 10, $frequency, $hookName); return true; } /** * Deletes a cronjob. * * @return bool True if the cronjob is correctly removed. */ public static function deleteScheduledTask($hookName) { // Return false if task does not exist. if (!wp_next_scheduled($hookName)) { return false; } // Remove cron job hook. wp_clear_scheduled_hook($hookName); return true; } /** * Reports the WordPress version number to the API. * * @return bool True if the version number was reported, false otherwise. */ public static function reportSiteVersion() { if (!SucuriScanAPI::getPluginKey()) { return self::throwException(__('API key is not available', 'sucuri-scanner')); } $wp_version = self::siteVersion(); $reported_version = SucuriScanOption::getOption(':site_version'); /* use simple comparison to leverage casting */ if ($reported_version == $wp_version) { return self::throwException(__('WordPress version was already reported', 'sucuri-scanner')); } SucuriScanEvent::reportInfoEvent(sprintf(__('WordPress version detected %s', 'sucuri-scanner'), $wp_version)); return SucuriScanOption::updateOption(':site_version', $wp_version); } /** * Decides if the file system scanner can run or not. * * @param bool $force_scan Force the execution of the scanner. * @return bool True if the scanner can run, false otherwise. */ private static function runFileScanner($force_scan = false) { if ($force_scan) { return SucuriScanOption::updateOption(':runtime', time()); } $current_time = time(); $runtime = (int) SucuriScanOption::getOption(':runtime'); $diff = abs($current_time - $runtime); if ($diff < SUCURISCAN_SCANNER_FREQUENCY) { return false; } return SucuriScanOption::updateOption(':runtime', $current_time); } /** * Gather all the checksums (aka. file hashes) of this site, send them, and * analyze them using the Sucuri Monitoring service, this will generate the * audit logs for this site and be part of the integrity checks. * * @param bool $force_scan Whether the filesystem scan was forced by an administrator user or not. * @return bool True if the filesystem scan was successful, false otherwise. */ public static function filesystemScan($force_scan = false) { if (!SucuriScanAPI::getPluginKey()) { return self::throwException(__('API key is not available', 'sucuri-scanner')); } if (!self::runFileScanner($force_scan)) { return self::throwException(__('Scanner ran a couple of minutes ago', 'sucuri-scanner')); } $fifo = new SucuriScanFileInfo(); $signatures = $fifo->getDirectoryTreeMd5(ABSPATH); SucuriScanOption::updateOption(':runtime', time()); return SucuriScanAPI::sendHashes($signatures); } /** * Sends information of a security event to the API. * * If the website owner has enabled the security log exporter, this method * will also write the information about the security event to that file. * This allows to integrate with different monitoring systems like OSSEC or * OpenVAS. * * If the communication with the API is enabled, it will also send all the * security logs collected on previous executions of the method that resulted * in a failure. However, this procedure depends on the ability of the plugin * to write the log into the queue when the previous request failed. * * @param string $message Information about the event. * @param string|int $timestamp Time when the event was triggered. * @param int $timeout Maximum time in seconds to connect to the API. * @return bool True if the event was logged, false otherwise. */ private static function sendLogToAPI($message = '', $timestamp = '', $timeout = 1) { if (empty($message)) { return self::throwException(__('Event identifier cannot be empty', 'sucuri-scanner')); } $params = array(); $params['a'] = 'send_log'; $params['m'] = $message; $params['time'] = $timestamp; $args = array('timeout' => $timeout); $resp = SucuriScanAPI::apiCallWordpress('POST', $params, true, $args); return (bool) ( is_array($resp) && array_key_exists('status', $resp) && intval($resp['status']) === 1 ); } /** * Sends the event message to a local queue system. * * Note: The method is public to facilitate the execution of some unit-tests * but it could be private and be mocked by the test bootstrap script. Take * this in consideration during the static analysis of the code. * * @param string $message Information about the security event. * @return bool True if the operation succeeded, false otherwise. */ public static function sendLogToQueue($message = '') { /* create storage directory if necessary */ SucuriScanInterface::createStorageFolder(); /** * Self-hosted Monitor. * * Send a copy of the event log to a local file, this will allow the * administrator of the server to integrate the events monitored by the * plugin with a 3rd-party service like OSSEC or similar. More info in * the Self-Hosting panel located in the plugin' settings page. */ if (function_exists('sucuriscan_selfhosting_fpath')) { $monitor_fpath = sucuriscan_selfhosting_fpath(); if ($monitor_fpath !== false) { $local_event = sprintf( "%s WordPressAudit %s %s : %s\n", SucuriScan::datetime(null, 'Y-m-d H:i:s'), SucuriScan::getTopLevelDomain(), SucuriScanOption::getOption(':account'), $message ); @file_put_contents( $monitor_fpath, $local_event, FILE_APPEND ); } } $cache = new SucuriScanCache('auditqueue'); $key = str_replace('.', '_', microtime(true)); $written = $cache->add($key, $message); return true; } /** * Sends all the events from the queue to the API. * * @return array|bool Information about the dequeue process. */ public static function sendLogsFromQueue() { if (SucuriScanOption::isDisabled(':api_service') || !defined('SUCURISCAN_API_URL') || empty(SUCURISCAN_API_URL)) { return false; } $cache = new SucuriScanCache('auditqueue'); $finfo = $cache->getDatastoreInfo(); $events = $cache->getAll(); if (!$events) { return false; } $result = array( 'maxtime' => -1, 'ttllogs' => 0, 'success' => 0, 'failure' => 0, 'elapsed' => 0, ); /** * Send logs to the API with a limit. * * We will use the maximum execution time setting to limit the number of * logs that the plugin will try to send to the API service before the * server times out. In a regular installation, the limit is set to 30 * seconds, since the timeout for the HTTP request is 5 seconds we will * instruct the plugin to wait (30 secs - 5 secs) and an additional one * second to spare processing, so in a regular installation the plugin * will try to send as much logs as possible to the API service in less * than 25 seconds. */ $maxtime = (int) SucuriScan::iniGet('max_execution_time'); $timeout = ($maxtime > 1) ? ($maxtime - 6) : 30; /* record some statistics */ $startTime = microtime(true); $result['maxtime'] = $maxtime; $result['ttllogs'] = count($events); foreach ($events as $keyname => $message) { $offset = strpos($keyname, '_'); $timestamp = substr($keyname, 0, $offset); $status = self::sendLogToAPI($message, $timestamp); /* skip; API is busy */ if ($status !== true) { $result['failure']++; continue; } /* dequeue event message */ unset($events[$keyname]); $result['success']++; /* avoid gateway timeout; max execution time */ $elapsedTime = (microtime(true) - $startTime); if ($elapsedTime >= $timeout) { break; } } $result['elapsed'] = round(microtime(true) - $startTime, 4); $cache->override($events); return $result; } /** * Generates an audit event log (to be sent later). * * @param int $severity Importance of the event that will be reported. * @param string $message The explanation of the event. * @return bool True if the event was logged, false otherwise. */ private static function reportEvent($severity = 0, $message = '') { if (!function_exists('wp_get_current_user')) { return; } $user = wp_get_current_user(); $remote_ip = self::getRemoteAddr(); $username = false; // Identify current user in session. if ($user instanceof WP_User && isset($user->user_login) && !empty($user->user_login) ) { $username = sprintf("\x20%s,", $user->user_login); } $severity = intval($severity); $severity_name = __('Info', 'sucuri-scanner'); $severities = array( /* 0 */ __('Debug', 'sucuri-scanner'), /* 1 */ __('Notice', 'sucuri-scanner'), /* 2 */ __('Info', 'sucuri-scanner'), /* 3 */ __('Warning', 'sucuri-scanner'), /* 4 */ __('Error', 'sucuri-scanner'), /* 5 */ __('Critical', 'sucuri-scanner'), ); if (isset($severities[$severity])) { $severity_name = $severities[$severity]; } /* remove unnecessary characters */ $message = strip_tags($message); $message = str_replace("\r", '', $message); $message = str_replace("\n", '', $message); $message = str_replace("\t", '', $message); return self::sendLogToQueue( sprintf( '%s:%s %s; %s', $severity_name, $username, $remote_ip, $message ) ); } /** * Reports a debug event on the website. * * @param string $message Text witht the explanation of the event or action performed. * @return bool Either true or false depending on the success of the operation. */ public static function reportDebugEvent($message = '') { return self::reportEvent(0, $message); } /** * Reports a notice event on the website. * * @param string $message Text witht the explanation of the event or action performed. * @return bool Either true or false depending on the success of the operation. */ public static function reportNoticeEvent($message = '') { return self::reportEvent(1, $message); } /** * Reports a info event on the website. * * @param string $message Text witht the explanation of the event or action performed. * @return bool Either true or false depending on the success of the operation. */ public static function reportInfoEvent($message = '') { return self::reportEvent(2, $message); } /** * Reports a warning event on the website. * * @param string $message Text witht the explanation of the event or action performed. * @return bool Either true or false depending on the success of the operation. */ public static function reportWarningEvent($message = '') { return self::reportEvent(3, $message); } /** * Reports a error event on the website. * * @param string $message Text witht the explanation of the event or action performed. * @return bool Either true or false depending on the success of the operation. */ public static function reportErrorEvent($message = '') { return self::reportEvent(4, $message); } /** * Reports a critical event on the website. * * @param string $message Text witht the explanation of the event or action performed. * @return bool Either true or false depending on the success of the operation. */ public static function reportCriticalEvent($message = '') { return self::reportEvent(5, $message); } /** * Send a notification to the administrator of the specified events, only if * the administrator accepted to receive alerts for this type of events. * * @param string $event The name of the event that was triggered. * @param string $content Body of the email that will be sent to the administrator. * @return bool True if the email was apparently sent, false otherwise. */ public static function notifyEvent($event = '', $content = '') { $email_params = array(); $email = SucuriScanOption::getOption(':notify_to'); $notify = SucuriScanOption::getOption(':notify_' . $event); /** * Skip if the IP address is trusted. * * Ignore event if the website owner has the IP address of the current * user in session in the allowlist. This is useful if the administrator is * working in an office and they want to allow every person in the office * (aka. the same LAN) to execute any task without triggering a security * alert. */ if (self::isTrustedIP()) { $notify = 'disabled'; } /* skip if alerts for this event are disabled */ if ($notify !== 'enabled') { return false; } switch ($event) { case 'post_publication': $event = 'post_update'; break; case 'failed_login': $settings_url = SucuriScanTemplate::getUrl('settings'); $content .= "\n" . sprintf( __("<br><br>\n\n<em>Explanation: Someone failed to login to your site. If you are getting too many of these messages, it is likely your site is under a password guessing brute-force attack [1]. You can disable the failed login alerts from here [2]. Alternatively, you can consider to install a firewall between your website and your visitors to filter out these and other attacks, take a look at Sucuri Firewall [3].</em><br><br>\n\n[1] <a href='https://kb.sucuri.net/definitions/attacks/brute-force/password-guessing'>https://kb.sucuri.net/definitions/attacks/brute-force/password-guessing</a><br>\n[2] <a href='%s'>%s</a> <br>\n[3] <a href='https://sucuri.net/website-firewall/?wpalert'>https://sucuri.net/website-firewall/</a><br>\n", 'sucuri-scanner'), $settings_url, $settings_url ); break; case 'bruteforce_attack': $email_params['Force'] = true; break; case 'scan_checksums': $event = 'core_integrity_checks'; $email_params['Force'] = true; $email_params['ForceHTML'] = true; break; case 'available_updates': $email_params['Force'] = true; $email_params['ForceHTML'] = true; } $title = ucwords(str_replace('_', "\x20", $event)); return SucuriScanMail::sendMail( $email, $title, $content, $email_params ); } /** * Check whether an IP address is being trusted or not. * * @param string $addr The supposed ip address that will be checked. * @return bool True if the user IP is trusted, false otherwise. */ public static function isTrustedIP($addr = '') { if (!$addr) { $addr = SucuriScan::getRemoteAddr(); } $cache = new SucuriScanCache('trustip', false); $trusted_ips = $cache->getAll(); if (!is_array($trusted_ips) || empty($trusted_ips)) { return false; } /* check if exact IP address match is in the allowlist */ if (array_key_exists(md5($addr), $trusted_ips)) { return true; } $is_trusted_ip = false; /* check if the CIDR in range 32 of this IP is trusted. */ foreach ($trusted_ips as $cache_key => $ip_info) { $ip_parts = explode('.', $ip_info->remote_addr); $ip_pattern = false; // Generate the regular expression for a specific CIDR range. switch ($ip_info->cidr_range) { case 24: $ip_pattern = sprintf( '/^%d\.%d\.%d\.[0-9]{1,3}$/', intval($ip_parts[0]), intval($ip_parts[1]), intval($ip_parts[2]) ); break; case 16: $ip_pattern = sprintf( '/^%d\.%d\.[0-9]{1,3}\.[0-9]{1,3}$/', intval($ip_parts[0]), intval($ip_parts[1]) ); break; case 8: $ip_pattern = sprintf( '/^%d\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', intval($ip_parts[0]) ); break; } if ($ip_pattern && preg_match($ip_pattern, $addr)) { $is_trusted_ip = true; break; } } return $is_trusted_ip; } /** * Generate and set a new password for a specific user not in session. * * @param int $user_id User account identifier. * @return bool True if the process exit clean, false otherwise. */ public static function setNewPassword($user_id = 0) { $user = get_userdata($user_id); if (!($user instanceof WP_User)) { return false; } /* invalidates the password for the given user */ $new_password = wp_generate_password(15, true, false); wp_set_password($new_password, $user_id); $website = SucuriScan::getDomain(); $user_login = $user->user_login; $display_name = $user->display_name; $key = self::GetPasswordResetKey($user); if (is_wp_error($key)) { return false; } $reset_password_url = network_site_url("wp-login.php?action=rp&key=$key&login=" . rawurlencode($user_login), 'https' ); $message = SucuriScanTemplate::getSection( 'settings-posthack-reset-password-alert', array( 'ResetPassword.UserName' => $user_login, 'ResetPassword.DisplayName' => $display_name, 'ResetPassword.ResetURL' => $reset_password_url, 'ResetPassword.Website' => $website, ) ); /* Skip per hour alert limit and force text/html content-type */ $data_set = array('Force' => true, 'ForceHTML' => true); $sent = SucuriScanMail::sendMail( $user->user_email, __('Password Change', 'sucuri-scanner'), $message, $data_set ); return true; } /** * Gets a new password reset key. * * @since 1.8.25 * * @param WP_User $user WP_User object. * @return string|WP_Error Returns a password reset key as a string, WP_Error otherwise. */ private static function GetPasswordResetKey($user) { global $wp_hasher; $key_error = new WP_Error('no_password_reset'); if (!($user instanceof WP_User)) { return $key_error; } /** * As of version 1.8.25 of this plugin, we still support WordPress version 3.6 and up * and for that reason we can't take advantage of the native function get_password_reset_key * (https://developer.wordpress.org/reference/functions/get_password_reset_key/), introduced in * WordPress 4.4. * * When we drop support for versions prior to WordPress 4.4, we can use get_password_reset_key * instead of this function. */ if (version_compare(SucuriScan::siteVersion(), '4.4', '>=') && function_exists('get_password_reset_key') ) { $key = get_password_reset_key($user); return $key; } if (is_multisite() && is_user_spammy($user)) { return $key_error; } // Generate something random for a password reset key. $key = wp_generate_password(20, false); if (empty($wp_hasher)) { require_once ABSPATH . WPINC . '/class-phpass.php'; $wp_hasher = PasswordHash(8, true); } $hashed = time() . ':' . $wp_hasher->HashPassword($key); $key_saved = wp_update_user( array( 'ID' => $user->ID, 'user_activation_key' => $hashed, ) ); if (is_wp_error($key_saved)) { return $key_saved; } return $key; } /** * Changes the WordPress secret keys. * * Modify the WordPress configuration file to define new secret keys from a * new randomly generated list of strings from the official WordPress API. * The result of the operation will be either False in case of error, or an * array containing multiple indexes explaining the modification, among them * you will find the old and new keys. * * @return array|bool Array with the old and new keys, false otherwise. */ public static function setNewConfigKeys() { $new_wpconfig = ''; $config_path = self::getConfigPath(); if (!$config_path) { return false; } $pattern = self::secretKeyPattern(); $define_tpl = "define('%s',%s'%s');"; $content = SucuriScanFileInfo::fileContent($config_path); $config_lines = explode("\n", $content); /* maintain new lines */ $new_keys = SucuriScanAPI::getNewSecretKeys(); if (!$new_keys) { return false; } $new_keys_string = ''; $old_keys_string = ''; $old_keys = array(); if ($config_lines && $new_keys) { foreach ($config_lines as $config_line) { if (@preg_match($pattern, $config_line, $match)) { $key_name = $match[1]; if (array_key_exists($key_name, $new_keys)) { $white_spaces = $match[2]; $old_keys[$key_name] = $match[3]; $config_line = sprintf( $define_tpl, $key_name, $white_spaces, $new_keys[$key_name] ); $old_keys_string .= sprintf( $define_tpl . "\n", $key_name, $white_spaces, $old_keys[$key_name] ); $new_keys_string .= $config_line . "\n"; } } $new_wpconfig .= $config_line . "\n"; } } $resp = array( 'updated' => is_writable($config_path), 'old_keys' => $old_keys, 'old_keys_string' => $old_keys_string, 'new_keys' => $new_keys, 'new_keys_string' => $new_keys_string, 'new_wpconfig' => $new_wpconfig, ); if ($resp['updated']) { @file_put_contents($config_path, $new_wpconfig, LOCK_EX); } return $resp; } /** * Clear last logins or failed login logs. * * This can also be done via Sucuri Security -> Settings -> Data Storage, * however to improve the user experience, a button on Last Logins and on * Failed logins sections was added and it triggers the removal of * sucuri/sucuri-lastlogins.php and sucuri/sucuri-failedlogins.php. * * @param string $filename Name of the file to be deleted. * * @return HTML Message with the delete action outcome. */ public static function clearLastLogs($filename) { // Get the complete path of the file. $filepath = SucuriScan::dataStorePath($filename); // Do not proceed if not possible. if (!is_writable(dirname($filepath)) || is_dir($filepath)) { return SucuriScanInterface::error( sprintf( __('%s cannot be deleted.', 'sucuri-scanner'), $filename ) ); } // Delete $filepath. @unlink($filepath); // Register on audit logs and return result. SucuriScanEvent::reportInfoEvent( sprintf( __('%s was deleted.', 'sucuri-scanner'), $filename ) ); return SucuriScanInterface::info( sprintf( __('%s was deleted.', 'sucuri-scanner'), $filename ) ); } }