File: /home/zwafgyp/dodolodge-net/wp-content/plugins/wp-malware-removal/traits/wpmr_scanner.php
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
trait WPMR_Scanner {
function set_args( $args = array() ) {
$cli = $this->wpmr_iscli();
$GLOBALS['WPMR'] = array();
$GLOBALS['WPMR'] = wp_parse_args(
$GLOBALS['WPMR'],
array(
'do_vuln_scan' => $cli ? $args['do_vuln_scan'] : ( ! empty( $_REQUEST['do_vuln_scan'] ) ? $this->mc_get_bool( sanitize_text_field( wp_unslash( $_REQUEST['do_vuln_scan'] ) ) ) : false ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Scan parameters processed by AJAX handlers with nonce verification
'do_db_scan' => $cli ? $args['do_db_scan'] : ( ! empty( $_REQUEST['do_db_scan'] ) ? $this->mc_get_bool( sanitize_text_field( wp_unslash( $_REQUEST['do_db_scan'] ) ) ) : false ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Scan parameters processed by AJAX handlers with nonce verification
'do_file_scan' => $cli ? $args['do_file_scan'] : ( ! empty( $_REQUEST['do_file_scan'] ) ? $this->mc_get_bool( sanitize_text_field( wp_unslash( $_REQUEST['do_file_scan'] ) ) ) : false ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Scan parameters processed by AJAX handlers with nonce verification
'do_redirect_scan' => $cli ? $args['do_redirect_scan'] : ( ! empty( $_REQUEST['do_redirect_scan'] ) ? $this->mc_get_bool( sanitize_text_field( wp_unslash( $_REQUEST['do_redirect_scan'] ) ) ) : false ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Scan parameters processed by AJAX handlers with nonce verification
'suspicious' => $cli ? $args['mcsuspicious'] : ( ! empty( $_REQUEST['suspicious'] ) ? $this->mc_get_bool( sanitize_text_field( wp_unslash( $_REQUEST['suspicious'] ) ) ) : false ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Scan parameters processed by AJAX handlers with nonce verification
'skipdirs' => $cli ? ( ! empty( $args['mcskipdirs'] ) ? $args['mcskipdirs'] : null ) : ( ! empty( $_REQUEST['skipdirs'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['skipdirs'] ) ) : null ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Scan parameters processed by AJAX handlers with nonce verification
'regex' => $cli ? ( ! empty( $args['mcregex'] ) ? $this->encode( $args['mcregex'] ) : null ) : ( ! empty( $_REQUEST['regex'] ) ? $this->encode( base64_decode( sanitize_text_field( wp_unslash( $_REQUEST['regex'] ) ) ) ) : false ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Scan parameters processed by AJAX handlers with nonce verification
'wpmr_extra_db_query' => $cli ? ( ! empty( $args['mcdbquery'] ) ? $this->encode( $args['mcdbquery'] ) : null ) : ( ! empty( $_REQUEST['wpmr_extra_db_query'] ) ? $this->encode( base64_decode( sanitize_text_field( wp_unslash( $_REQUEST['wpmr_extra_db_query'] ) ) ) ) : false ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Scan parameters processed by AJAX handlers with nonce verification
'wpmr_extra_db_regex' => $cli ? ( ! empty( $args['mcdbregex'] ) ? $this->encode( $args['mcdbregex'] ) : null ) : ( ! empty( $_REQUEST['wpmr_extra_db_regex'] ) ? $this->encode( base64_decode( sanitize_text_field( wp_unslash( $_REQUEST['wpmr_extra_db_regex'] ) ) ) ) : false ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Scan parameters processed by AJAX handlers with nonce verification
// scan single file
'files' => $cli ?
( ! empty( $args['mcfiles'] ) ? $args['mcfiles'] : null )
: ( ! empty( $_REQUEST['files'] ) ? array_map( // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Scan parameters processed by AJAX handlers with nonce verification
array( $this, 'decode_filename' ),
array_map( 'sanitize_text_field', wp_unslash( $_REQUEST['files'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified elsewhere, Sanitized via array_map
) : null ),
'only_scan_dirs' => $cli ? ( ! empty( $args['mcscanonlydirs'] ) ? $args['mcscanonlydirs'] : null ) : ( ! empty( $_REQUEST['wpmr_scan_only_dirs'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['wpmr_scan_only_dirs'] ) ) : null ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Scan parameters processed by AJAX handlers with nonce verification
'timestamp' => $cli ? $args['timestamp'] : ( ! empty( $_REQUEST['timestamp'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['timestamp'] ) ) : false ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Scan parameters processed by AJAX handlers with nonce verification
'debug' => $cli ? ( ! empty( $args['mcdebug'] ) && ( $args['mcdebug'] == 'true' ) ? true : false ) : ( isset( $_REQUEST['debug'] ) ? true : false ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Scan parameters processed by AJAX handlers with nonce verification
)
);
if ( ! $this->is_registered() ) {
$GLOBALS['WPMR']['suspicious'] = true;
}
$GLOBALS['WPMR']['home_dir'] = $this->get_home_dir();
return $GLOBALS['WPMR'];
}
function bootstrap( $args = array() ) {
$this->raise_limits_conditionally();
$this->set_args( $args );
if ( ! empty( $GLOBALS['WPMR']['skipdirs'] ) ) {
$skipdirs = array_map( 'trim', explode( ',', $GLOBALS['WPMR']['skipdirs'] ) );
$GLOBALS['WPMR']['skipdirs'] = $skipdirs;
} else {
$GLOBALS['WPMR']['skipdirs'] = array();
}
if ( ! empty( $GLOBALS['WPMR']['only_scan_dirs'] ) ) {
$only_scan_dirs = array_map( 'trim', explode( ',', $GLOBALS['WPMR']['only_scan_dirs'] ) );
$only_scan_dirs = array_map( 'untrailingslashit', $only_scan_dirs );
$only_scan_dirs = array_map( array( $this, 'unleadingslashit' ), $only_scan_dirs );
$only_scan_dirs = array_map(
function ( $k ) {
return $this->normalise_path( untrailingslashit( ABSPATH ) . DIRECTORY_SEPARATOR . $k );
},
$only_scan_dirs
);
$GLOBALS['WPMR']['only_scan_dirs'] = $only_scan_dirs;
} else {
$GLOBALS['WPMR']['only_scan_dirs'] = array();
}
$all_files = $this->return_all_files();
// Check if there's a custom WP_CONTENT_DIR
if ( ! file_exists( trailingslashit( $this->normalise_path( ABSPATH ) ) . 'wp-content' ) ) {
$wp_content = $this->return_all_files( WP_CONTENT_DIR );
if ( ! is_array( $wp_content ) ) { // in case WP_CONTENT_DIR is .ignored
$wp_content = array();
}
$all_files = array_merge( $all_files, $wp_content );
}
// Check if there's a custom plugin directory
if ( ! file_exists( trailingslashit( $this->normalise_path( WP_CONTENT_DIR ) ) . 'plugins' ) ) {
$wp_content_plugins = $this->return_all_files( WP_PLUGIN_DIR );
if ( ! is_array( $wp_content_plugins ) ) { // in case WP_PLUGIN_DIR is .ignored
$wp_content_plugins = array();
}
$all_files = array_merge( $all_files, $wp_content_plugins );
}
// check if there's a custom themes directory
if ( ! file_exists( trailingslashit( $this->normalise_path( WP_CONTENT_DIR ) ) . 'themes' ) ) {
$wp_content_themes = $this->return_all_files( get_theme_root() );
if ( ! is_array( $wp_content_themes ) ) {
$wp_content_themes = array();
}
$all_files = array_merge( $all_files, $wp_content_themes );
}
// Check if there's a custom upload directory
if ( ! file_exists( $this->default_uploads_path() ) ) {
$uploads = wp_get_upload_dir();
if ( ! empty( $uploads['basedir'] ) ) {
$wp_content_uploads = $this->return_all_files( $uploads['basedir'] );
if ( ! is_array( $wp_content_uploads ) ) { // in case $uploads['basedir'] is .ignored
$wp_content_uploads = array();
}
$all_files = array_merge( $all_files, $wp_content_uploads );
}
}
$all_files = $this->prioritise_core_files( $all_files );
$files = $this->process_only_scan_dirs( $all_files['files'] );
$files = array_values( $files );
$response = array(
'files' => $files,
'timestamp' => $GLOBALS['WPMR']['timestamp'],
'count' => count( $files ),
'debug' => $_REQUEST, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Debug information, not processing form data
'checksums' => count( $this->get_db_checksums() ) . '+' . count( $this->get_checksums() ),
'db_stats' => $this->db_stats(),
);
if ( ! empty( $_REQUEST['wpmr_init_nonce'] ) || $this->wpmr_iscli() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification handled by calling function
if ( ! $GLOBALS['WPMR']['do_file_scan'] ) {
$response['files'] = array();
}
if ( $GLOBALS['WPMR']['do_db_scan'] ) {
$db_stats = @$this->db_stats();
if ( $this->wpmr_iscli() ) {
unset( $response['db_stats'] ); // force remove for now else this triggers batch scan
$response['db_scan'] = @$this->db_scan_cli();
} else {
$response['db_stats'] = @$this->db_stats();
}
} else {
unset( $response['db_stats'] );
}
$response['title_hack'] = (bool) $this->title_hack();
if ( $GLOBALS['WPMR']['do_redirect_scan'] ) {
$response['redirect_hijack'] = (bool) @$this->redirect_hijack();
} else {
$response['redirect_hijack'] = false;
}
if ( $GLOBALS['WPMR']['do_vuln_scan'] ) {
$response['vulnerabilities_scan'] = @$this->vulnerability_scan();
} else {
$response['vulnerabilities_scan'] = false;
}
$response['definition_count'] = $this->get_definition_count();
$response['definition_version'] = $this->get_definition_version();
$response['last_updated'] = $this->get_last_updated_ago();
do_action( 'wpmr_scan_init', $GLOBALS['WPMR'] );
}
return $response;
}
function wpmr_scan_files( $args = array() ) {
if ( ! $this->wpmr_iscli() ) {
// Check for nonce if not using WP CLI
check_ajax_referer( 'wpmr_scan_files', 'wpmr_scan_files_nonce' );
// Check if the user has the required capability
if ( ! current_user_can( $this->cap ) ) {
wp_send_json_error( 'Unauthorized access' );
return;
}
}
$start = microtime( true );
$this->raise_limits_conditionally();
$this->set_args( $args );
$files = $GLOBALS['WPMR']['files'];
if ( defined( 'WP_CLI' ) && WP_CLI ) {
} else {
@ini_set( 'max_execution_time', min( 90, count( $files ) + 1 ) ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Necessary for file scanning execution time management
}
// @ini_set( 'max_execution_time', max( (int) ini_get( 'max_execution_time' ), min( 90, count( $files ) ) ) );
$affected_files = array();
$registered = $this->is_registered();
$definitions = $this->get_definitions()['definitions']['files'];
if ( ! empty( $GLOBALS['WPMR']['regex'] ) ) {
$definitions = array_merge(
array(
'DWPMR' => array(
'severity' => 'severe',
'signature' => $GLOBALS['WPMR']['regex'],
'class' => 'scripting',
),
),
$definitions
);
}
$GLOBALS['WPMR']['definitions'] = $definitions;
$GLOBALS['WPMR']['checksums'] = $this->get_checksums_values();
$GLOBALS['WPMR']['core_checksums'] = get_option( 'WPMR_checksums' );
foreach ( $files as $file ) {
// $this->flog( 'Scanning file: ' . $file );
if ( is_link( $file ) || ! $this->is_scannable_file( $file ) ) { // skip if this is a directory symlink
continue;
}
$checksum_failure = $this->fails_checksum( $file );
// $this->flog( 'Checksum failure status for file ' . $file . ': ' . print_r( $checksum_failure, true ) );
if ( $checksum_failure ) {
$threat = 0;
$threat = $this->wpmr_scan_file_threats( $file );
if ( $threat ) {
$affected_files[ $file ] = $threat;
} elseif ( ( $checksum_failure == 'missing' ) && ( ! $registered || $GLOBALS['WPMR']['suspicious'] ) ) {
// THIS SHOULD NEVER BE THE CASE. PLUGIN REQUIRES REGISTRATION ELSE IT CAN'T BE USED
// If the scan is running without API registration
$affected_files[ $file ] = array(
'id' => 'mismatch',
'severity' => 'suspicious',
'info' => 'Mismatch',
);
} elseif ( $checksum_failure == 'missing' && $this->is_core_wp_file( $file ) ) {
// unknown file in core directory
$affected_files[ $file ] = array(
'id' => 'unknown',
'severity' => 'suspicious',
'info' => 'Suspicious',
);
} elseif ( $GLOBALS['WPMR']['debug'] ) {
}
} elseif ( $GLOBALS['WPMR']['debug'] ) {
}
}
$this->update_cached_checksums( array_diff( array_values( $files ), array_keys( $affected_files ) ) ); // This will not save checksum if the file is suspicious either
$affected_files = $this->may_be_filter_suspicious( $affected_files );
if ( $affected_files ) {
if ( ! empty( $GLOBALS['WPMR']['timestamp'] ) ) {
$this->update_saved_records( $GLOBALS['WPMR']['timestamp'], array( 'files' => $affected_files ) );
}
}
foreach ( $affected_files as $f => $report ) {
$affected_files[ $f ] = $this->set_status( $report['severity'], $report['info'], $report['id'] );
}
if ( empty( $GLOBALS['WPMR']['response_debug'] ) ) {
$GLOBALS['WPMR']['response_debug'] = array();
}
$cpu = $this->get_server_load();
if ( empty( $cpu ) ) {
$cpu = array( 0, 0, 0 );
}
$cpu = array_map(
function ( $num ) {
return number_format( $num, 2, '.', '' );
},
$cpu
);
$memory_limit = @ini_get( 'memory_limit' );
$GLOBALS['WPMR']['response_debug']['time_taken'] = ( microtime( true ) - $start );
$result = array(
'report' => $affected_files,
'memory' => ( memory_get_peak_usage( true ) / 1024 / 1024 ),
'memory_limit' => $memory_limit,
'cpu' => $cpu,
'debug' => $GLOBALS['WPMR']['response_debug'],
);
if ( $this->wpmr_iscli() ) {
return $result;
} else {
wp_send_json( $result );
}
}
function wpmr_scan_file_threats( $file ) {
if ( $this->is_invalid_file( $file ) ) {
if ( ! is_readable( $file ) ) { // return early so that error is displayed
return array(
'id' => 'unreadable',
'severity' => 'skipped',
'info' => 'Error Reading File',
);
}
return;
}
$ext = $this->get_fileext( $file );
$tests = array();
if ( $this->is_invalid_file( $file ) ) {
return;
}
$GLOBALS['WPMR']['tmp']['file_contents'] = @file_get_contents( $file );
// failure to read file contents
if ( $GLOBALS['WPMR']['tmp']['file_contents'] === false ) {
return array(
'id' => 'unreadable',
'severity' => 'skipped',
'info' => 'Error Reading File',
);
}
// File contents read but file is empty
if ( empty( $GLOBALS['WPMR']['tmp']['file_contents'] ) ) {
// Flag empty files in core directories as suspicious
if ( $this->is_core_wp_file( $file ) ) {
return array(
'id' => 'unknown',
'severity' => 'suspicious',
'info' => 'Suspicious',
);
}
// not core file, skip scanning
return;
}
$definitions = $GLOBALS['WPMR']['definitions'];
if ( strpos( @ini_get( 'disable_functions' ), 'ini_set' ) === false ) {
@ini_set( 'pcre.backtrack_limit', 1000000 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Required for malware pattern matching with large files
}
// Set execution time limit per pattern to prevent hangs
$max_pattern_time = 5; // seconds per pattern
foreach ( $definitions as $definition => $signature ) {
if ( $signature['class'] == 'htaccess' && $ext != 'htaccess' ) {
continue;
}
try {
// $this->flog( 'Testing signature ' . $definition . ' ' . $this->decode( $signature['signature'] ) . ' on file ' . $file );
if ( @preg_match( $this->decode( $signature['signature'] ), '' ) === false ) {
throw new Exception( 'Invalid regular expression ' . $definition . ' in ' . __FUNCTION__ );
}
// $this->flog( 'Pattern ' . $this->decode( $signature['signature'] ) . ' is valid.' );
// FOR DEBUGGING
if ( false && preg_match( '/footer/isS', $file ) ) {
$this->flog( 'Scanning file ' . $file . ' for ' . $this->decode( $signature['signature'] ) );
}
// Execute preg_match with timeout protection
$start_time = microtime( true );
set_error_handler(
function () use ( $start_time, $max_pattern_time ) {
if ( ( microtime( true ) - $start_time ) > $max_pattern_time ) {
$this->flog( 'Pattern execution timeout reached.' );
throw new Exception( 'Pattern execution timeout' );
}
}
);
$matches = @preg_match( $this->decode( $signature['signature'] ), $GLOBALS['WPMR']['tmp']['file_contents'], $found );
restore_error_handler();
$execution_time = microtime( true ) - $start_time;
if ( $execution_time > 2 ) {
$this->flog( 'WARNING: Slow pattern execution (' . number_format( $execution_time, 2 ) . 's) for ' . $definition . ' on file ' . $file, false, false, true );
}
// $this->flog( 'Scanning : ' . $definition . ' ' . $this->decode( $signature['signature'] ) . ' complete on file ' . $file . print_r( $found, true ) . ' Matches: ' . $matches );
} catch ( Exception $e ) {
restore_error_handler();
$this->flog( 'Faulty Signature: ' . $definition, false, false, true );
$this->flog( 'Faulty Pattern: ' . $this->decode( $signature['signature'] ), false, false, true );
$this->flog( 'File: ' . $file, false, false, true );
$this->flog( $e->getMessage(), false, false, true );
continue;
}
if ( $matches >= 1 ) {
if ( in_array( $signature['severity'], array( 'severe', 'high' ) ) ) {
$this->update_setting( 'infected', true );
}
return array(
'id' => $definition,
'severity' => $signature['severity'],
'info' => $signature['severity'],
);
}
}
}
function vulnerability_scan() {
// Build the components array with WordPress, plugins, and themes information
$components = array();
$home_path = untrailingslashit( realpath( get_home_path() ) ) . DIRECTORY_SEPARATOR;
$issues = array();
// Add WordPress core version
$components['core'] = array(
'version' => get_bloginfo( 'version' ),
// 'version' => '6.0.2,'
);
// Add plugins information
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
$components['plugins'] = array();
foreach ( $all_plugins as $plugin_file => $plugin_data ) {
if ( ! empty( $plugin_data['Name'] ) && ! empty( $plugin_data['Version'] ) ) {
$plugin_name = $plugin_data['Name'];
$plugin_dir = dirname( $plugin_file );
$readme_path = WP_PLUGIN_DIR . "/{$plugin_dir}/readme.txt";
if ( is_readable( $readme_path ) ) {
$contents = file_get_contents( $readme_path );
if ( preg_match( '/^===\s*(.+?)\s*===\s*$/m', $contents, $m ) ) {
// override if we actually found a readme header
$plugin_name = trim( $m[1] );
}
}
// Use the absolute path to the plugin file as the key
$plugin_path = wp_normalize_path( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin_file );
$components['plugins'][ $plugin_path ] = array(
'name' => $plugin_name,
'version' => $plugin_data['Version'],
);
}
}
ksort( $components['plugins'] );
// Add themes information
$all_themes = wp_get_themes();
$components['themes'] = array();
foreach ( $all_themes as $theme_key => $theme_obj ) {
$theme_name = $theme_obj->get( 'Name' );
$readme = $theme_obj->get_stylesheet_directory() . '/readme.txt';
if ( is_readable( $readme ) ) {
$contents = file_get_contents( $readme );
if ( preg_match( '/^===\s*(.+?)\s*===/m', $contents, $matches ) ) {
$theme_name = trim( $matches[1] );
}
}
// Use the absolute path to the theme directory as the key
$theme_path = $theme_obj->get_stylesheet_directory();
$components['themes'][ wp_normalize_path( $theme_path . DIRECTORY_SEPARATOR . 'style.css' ) ] = array(
'name' => $theme_name,
'version' => $theme_obj->get( 'Version' ),
);
}
ksort( $components['themes'] );
$response = $this->saas_request(
'saas_check_vulnerabilities',
array(
'method' => 'POST',
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded',
),
'body' => array(
'components' => wp_json_encode( $components ),
),
)
);
if ( is_wp_error( $response ) ) {
$this->flog( 'ERROR: ' . $response->get_error_message() );
return array(
'success' => false,
'error' => $response->get_error_message(),
);
}
$data = isset( $response['response'] ) && is_array( $response['response'] ) ? $response['response'] : array();
if ( empty( $data ) || ! isset( $data['success'] ) || ! $data['success'] ) {
$this->flog( 'Contract Violation: Unsuccessful response in vulnerability_scan' );
return array();
}
if ( ! isset( $response['payload']['vulnerabilities'] ) ) {
$this->flog( 'Contract Violation: Missing vulnerabilities payload' );
return array();
}
$vulnerability_sets = $response['payload']['vulnerabilities'];
if ( empty( $vulnerability_sets ) ) {
$this->flog( 'INFO: No vulnerabilities found.' );
return array();
}
// Process core findings (if any)
if ( ! empty( $vulnerability_sets['core'] ) ) {
$core_entries = $vulnerability_sets['core'];
if ( isset( $core_entries['name'] ) ) {
$core_entries = array( $core_entries );
}
foreach ( $core_entries as $core_details ) {
if ( ! is_array( $core_details ) ) {
continue;
}
$name = isset( $core_details['name'] ) ? sanitize_text_field( $core_details['name'] ) : 'WordPress';
$signature = isset( $core_details['id'] ) ? sanitize_text_field( $core_details['id'] ) : 'core';
$issues['core'][ $name ] = $this->set_status( 'vulnerable', $name, $signature );
}
}
// Process plugin findings
if ( ! empty( $vulnerability_sets['plugins'] ) && is_array( $vulnerability_sets['plugins'] ) ) {
foreach ( $vulnerability_sets['plugins'] as $plugin => $vulnerable ) {
if ( ! is_array( $vulnerable ) ) {
continue;
}
$plugin_slug = basename( dirname( $plugin ) );
if ( empty( $plugin_slug ) ) {
$plugin_slug = basename( $plugin );
}
$plugin_slug = sanitize_text_field( $plugin_slug );
$name = isset( $vulnerable['name'] ) ? sanitize_text_field( $vulnerable['name'] ) : $plugin_slug;
$signature = isset( $vulnerable['id'] ) ? sanitize_text_field( $vulnerable['id'] ) : $plugin_slug;
$issues['plugins'][ $plugin_slug ] = $this->set_status( 'vulnerable', $name, $signature );
}
}
// Process theme findings
if ( ! empty( $vulnerability_sets['themes'] ) && is_array( $vulnerability_sets['themes'] ) ) {
foreach ( $vulnerability_sets['themes'] as $theme => $vulnerable ) {
if ( ! is_array( $vulnerable ) ) {
continue;
}
$theme_slug = basename( dirname( $theme ) );
if ( empty( $theme_slug ) ) {
$theme_slug = basename( $theme );
}
$theme_slug = sanitize_text_field( $theme_slug );
$name = isset( $vulnerable['name'] ) ? sanitize_text_field( $vulnerable['name'] ) : $theme_slug;
$signature = isset( $vulnerable['id'] ) ? sanitize_text_field( $vulnerable['id'] ) : $theme_slug;
$issues['themes'][ $theme_slug ] = $this->set_status( 'vulnerable', $name, $signature );
}
}
if ( empty( $issues ) ) {
$this->flog( 'INFO: No vulnerabilities found.' );
return array();
}
if ( ! empty( $GLOBALS['WPMR']['timestamp'] ) ) {
// set_transient( 'WPMR_log_' . $GLOBALS['WPMR']['timestamp'], json_encode( array( 'vulnerabilities' => $issues ) ), 30 * DAY_IN_SECONDS );
$this->update_saved_records( $GLOBALS['WPMR']['timestamp'], array( 'vulnerabilities' => $issues ) );
}
return $issues;
}
function wpmr_init_scan() {
check_ajax_referer( 'wpmr_init_scan', 'wpmr_init_nonce' );
if ( ! current_user_can( $this->cap ) ) {
wp_send_json_error( 'Unauthorized access' );
return;
}
wp_send_json( $this->bootstrap() );
}
function wpmr_get_stats() {
check_ajax_referer( 'wpmr_stats', 'wpmr_stats_nonce' );
if ( ! current_user_can( $this->cap ) ) {
wp_send_json_error( 'Unauthorized access' );
return;
}
if ( ! empty( $_REQUEST['stat_type'] ) ) {
$request_type = sanitize_text_field( wp_unslash( $_REQUEST['stat_type'] ) );
switch ( $request_type ) {
case 'bootstrap':
$data = $this->bootstrap();
unset( $data['files'] );
wp_send_json_success( $data );
case 'hidden_files':
$data = $this->bootstrap();
$hidden = array_filter(
$data['files'],
function ( $v ) {
return ( ! strlen( explode( '.', basename( $v ) )[0] ) || ! strlen( explode( '.', basename( dirname( $v ) ) )[0] ) ) ? true : false;
}
);
if ( ! empty( $hidden ) ) {
$hidden = array_values( $hidden );
$newlist = array();
foreach ( $hidden as $k => $v ) {
$parts = explode( '.', basename( dirname( $v ) ) );
if ( ! strlen( $parts[0] ) ) {
$newlist[ dirname( $v ) ] = '<strong>[*DIR] ' . dirname( $v ) . '</strong>';
}
$newlist[ $v ] = '[FILE] ' . $v;
}
$newlist = implode( '<br />', $newlist );
wp_send_json_success( $newlist );
} else {
wp_send_json_error( array() );
}
case 'definition_count':
wp_send_json_success( $this->get_definition_count() );
case 'definition_version':
wp_send_json_success( $this->get_definition_version() );
case 'last_updated':
wp_send_json_success( $this->get_last_updated_ago() );
case 'memory_limit':
wp_send_json_success( (int) @ini_get( 'memory_limit' ) );
}
}
wp_send_json_error( $_REQUEST );
}
function wpmr_clear_infection_stats() {
check_ajax_referer( 'wpmr_clear_infection_stats', 'wpmr_clear_infection_stats_nonce' );
if ( ! current_user_can( $this->cap ) ) {
return;
}
wp_send_json( ! $this->get_setting( 'infected' ) || ( $this->get_setting( 'infected' ) && $this->delete_setting( 'infected' ) ) );
}
function is_file_blacklisted( $checksum ) {
return in_array( $checksum, array( 'ada53f04e24f787f126d5e7d07f42425a780f3d76f12c6d11e9535ed5106b94a' ) );
}
function db_stats() {
global $wpdb;
$wpstats = array();
$tables = array(
'posts' => array(
'table' => $wpdb->posts,
'column' => 'ID',
),
'postmeta' => array(
'table' => $wpdb->postmeta,
'column' => 'meta_id',
),
'options' => array(
'table' => $wpdb->options,
'column' => 'option_id',
),
'comments' => array(
'table' => $wpdb->comments,
'column' => 'comment_ID',
),
);
foreach ( $tables as $name => $table_info ) {
$table = $table_info['table'];
$column = $table_info['column'];
// Construct the SQL query for row count, min and max ID in one go
$query = "SELECT COUNT(*) AS count, MIN($column) AS min, MAX($column) AS max FROM $table";
// Execute the query
$result = $wpdb->get_row( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for table statistics, no user input, caching not beneficial for one-time stats.
// Handle cases when no rows are found (like 0 comments)
if ( $result['count'] == 0 ) { // If no rows found, set min and max to 0
$result['count'] = 0;
$result['min'] = 0;
$result['max'] = 0;
}
// Add the results to the table stats
$wpstats[ $name ] = array(
'count' => $result['count'],
'min' => $result['min'],
'max' => $result['max'],
);
}
return $wpstats;
}
function wpmr_scan_db( $args = array() ) {
check_ajax_referer( 'wpmr_scan_db', 'wpmr_scan_db_nonce' );
// Check user_cap if the request is not made via WP CLI
if ( ! current_user_can( $this->cap ) ) {
wp_send_json_error( 'Unauthorized access' );
return;
}
$start = microtime( true );
$this->raise_limits_conditionally();
$this->set_args( $args );
if ( ! empty( $_REQUEST['table'] ) && ! empty( $_REQUEST['batchsize'] ) ) {
$table = sanitize_text_field( wp_unslash( $_REQUEST['table'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized on same line
$batchsize = sanitize_text_field( wp_unslash( $_REQUEST['batchsize'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized on same line
$pointer = isset( $_REQUEST['pointer'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['pointer'] ) ) : 0; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized on same line
if ( empty( $GLOBALS['WPMR']['response_debug'] ) ) {
$GLOBALS['WPMR']['response_debug'] = array();
}
$cpu = $this->get_server_load();
if ( empty( $cpu ) ) {
$cpu = array( 0, 0, 0 );
}
$cpu = array_map(
function ( $num ) {
return number_format( $num, 2, '.', '' );
},
$cpu
);
$memory_limit = @ini_get( 'memory_limit' );
$GLOBALS['WPMR']['response_debug']['time_taken'] = ( microtime( true ) - $start );
$result = array(
'report' => $this->db_scan_batch( $table, $batchsize, $pointer ),
'memory' => ( memory_get_peak_usage( true ) / 1024 / 1024 ),
'memory_limit' => $memory_limit,
'cpu' => $cpu,
'debug' => $GLOBALS['WPMR']['response_debug'],
);
if ( $this->wpmr_iscli() ) {
return $result;
} else {
wp_send_json( $result );
}
}
}
function db_scan_batch( $table, $batchsize, $pointer ) {
global $wpdb;
$batchsize = intval( $batchsize );
$pointer = intval( $pointer );
$sql = '';
$table_name = '';
switch ( $table ) {
case 'posts':
$sql = "SELECT `ID` AS id, `post_content` AS content, `post_type` as post_type FROM ( SELECT `ID`, `post_content`, `post_type` FROM `{$wpdb->posts}` WHERE `ID` > %d AND `ID` <= %d ORDER BY `ID` ) AS limited_rows WHERE `post_content` LIKE %s";
$table_name = $wpdb->posts;
break;
case 'postmeta':
$sql = "SELECT `meta_id` AS id, `meta_value` AS content FROM ( SELECT `meta_id`, `meta_value` FROM $wpdb->postmeta WHERE `meta_id` > %d AND `meta_id` <= %d ORDER BY `meta_id` ) AS limited_rows WHERE `meta_value` LIKE %s";
$table_name = $wpdb->postmeta;
break;
case 'options':
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
$all_plugins = get_option( 'active_plugins' );
$exclusions = array();
if ( isset( $all_plugins['gotmls/index.php'] ) ) {
$exclusions[] = 'GOTMLS_definitions_array';
}
if ( in_array( 'malcare-security/malcare.php', $all_plugins ) || in_array( 'blogvault-real-time-backup/blogvault.php', $all_plugins ) ) {
$exclusions[] = 'bvruleset';
}
if ( ! empty( $exclusions ) ) {// Prepare the string for SQL query
$exclusions = array_map( 'esc_sql', $exclusions );
$exclusion_list = "'" . implode( "','", $exclusions ) . "'";
$sql = "SELECT `option_id` AS id, `option_value` AS content FROM ( SELECT `option_id`, `option_value` FROM $wpdb->options WHERE `option_id` > %d AND `option_id` <= %d AND `option_name` NOT IN (" . $exclusion_list . ') ORDER BY `option_id`) AS limited_rows WHERE `option_value` LIKE %s';
} else {
$sql = "SELECT `option_id` AS id, `option_value` AS content FROM ( SELECT `option_id`, `option_value` FROM $wpdb->options WHERE `option_id` > %d AND `option_id` <= %d ORDER BY `option_id` ) AS limited_rows WHERE `option_value` LIKE %s";
}
$table_name = $wpdb->options;
break;
case 'comments':
$sql = "SELECT `comment_ID` AS id, `comment_content` AS content FROM ( SELECT `comment_ID`, `comment_content` FROM $wpdb->comments WHERE `comment_approved` = '1' AND `comment_ID` > %d AND `comment_ID` <= %d ORDER BY comment_ID) AS limited_rows WHERE `comment_content` LIKE %s";
$table_name = $wpdb->comments;
break;
default:
return;
}
$definitions = $this->get_definitions()['definitions']['db'];
$db_results = array(
'results' => array(),
'pointer' => $pointer,
);
$db_scan_log = array();
if ( ! empty( $GLOBALS['WPMR']['wpmr_extra_db_query'] ) && ! empty( $GLOBALS['WPMR']['wpmr_extra_db_regex'] ) ) {
$definitions['DWPMR'] = array(
'severity' => 'severe',
'query' => $GLOBALS['WPMR']['wpmr_extra_db_query'],
'signature' => $GLOBALS['WPMR']['wpmr_extra_db_regex'],
);
}
$results = array();
$db_checksums = $this->get_db_checksums();
$maybe_infected = array();
$hash = false;
foreach ( $definitions as $ver => $details ) {
$time = microtime( true );
// $raw_query = ;
// Prepare the SQL query within the loop
$prepared_sql = $wpdb->prepare( $sql, $pointer, $pointer + $batchsize, $this->decode( $details['query'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared.
$results = $wpdb->get_results( $prepared_sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- SQL prepared already, direct query needed for malware scanning, caching not suitable for scan results.
if ( ! $results ) { // get_results also returns NULL which foreach can't handle
continue;
}
foreach ( $results as $result ) {
$content = $result['content'];
$hash = hash( 'sha256', $content );
if ( in_array( $hash, $db_checksums ) ) {
continue;
// This result was previously scanned and stored in the checksums.
// No need to scan it again for this definition.
// Skip this result and continue scanning the next result.
}
$id = sanitize_text_field( $result['id'] );
$post_status = empty( $result['post_type'] ) ? '' : 'post-type → ' . $result['post_type'] . '.';
try {
if ( @preg_match( $this->decode( $details['signature'] ), '' ) === false ) {
throw new Exception( 'Invalid regular expression ' . $ver . ' in ' . __FUNCTION__ );
}
$matches = preg_match( $this->decode( $details['signature'] ), $content, $found );
} catch ( Exception $e ) {
$this->flog( 'Faulty Signature: ' . $ver, false, false, true );
$this->flog( 'Faulty Pattern: ' . $this->decode( $details['signature'] ), false, false, true );
$this->flog( $e->getMessage(), false, false, true );
continue;
}
if ( $matches >= 1 ) {
if ( in_array( $details['severity'], array( 'severe', 'high' ) ) ) {
$this->update_setting( 'infected', true );
}
$db_results['results'][ $table_name . '_' . $id ] = $this->set_status(
$details['severity'],
preg_replace( '/[^A-Za-z0-9]/', ' ', $table_name ) . " ID $id. $post_status " . '',
$ver
);
$db_scan_log[ $id ] = array(
'severity' => $details['severity'],
'infection' => $ver,
'type' => ucwords( $table_name ),
'id' => $id,
);
$maybe_infected[ $hash ] = 1;
} else {
// $maybe_infected has should only be changed to 0 if it is not 1 already
if ( ! isset( $maybe_infected[ $hash ] ) ) {
$maybe_infected[ $hash ] = 0;
}
}
} // foreach $results
} // foreach $definitions
foreach ( $maybe_infected as $checksum => $infected ) {
if ( $infected == 0 ) {
$db_checksums[] = $checksum;
}
}
$db_checksums = array_unique( $db_checksums );
update_option( 'WPMR_db_checksums_cache', $db_checksums );
switch ( $table ) {
case 'posts':
$sql = $wpdb->prepare( "SELECT MIN(`ID`) AS next_id FROM $wpdb->posts WHERE `ID` > %d", ( $pointer + $batchsize ) );
break;
case 'postmeta':
$sql = $wpdb->prepare( "SELECT MIN(`meta_id`) AS next_id FROM $wpdb->postmeta WHERE `meta_id` > %d", ( $pointer + $batchsize ) );
break;
case 'options':
$sql = $wpdb->prepare( "SELECT MIN(`option_id`) AS next_id FROM $wpdb->options WHERE `option_id` > %d", ( $pointer + $batchsize ) );
break;
case 'comments':
$sql = $wpdb->prepare( "SELECT MIN(`comment_ID`) AS next_id FROM $wpdb->comments WHERE `comment_approved` = '1' AND `comment_ID` > %d", ( $pointer + $batchsize ) );
break;
default:
}
$new_pointer = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- SQL prepared already, direct query needed for batch processing pointer calculation, caching not suitable for dynamic pointers.
if ( ! empty( $new_pointer ) ) {
$new_pointer = intval( $new_pointer ) - 1;
$db_results['new_pointer'] = $new_pointer;
} else {
$db_results['new_pointer'] = $pointer + $batchsize;
}
if ( $db_results['results'] ) {
$db_results['results'] = array_values( $db_results['results'] );
if ( ! empty( $db_scan_log ) && ! empty( $GLOBALS['WPMR']['timestamp'] ) ) {
$record = $GLOBALS['WPMR']['timestamp'];
$db_scan_log = array( 'db' => $db_scan_log );
$this->update_saved_records( $record, $db_scan_log );
}
return $db_results;
}
return $db_results;
}
function get_saved_records( $record ) {
$defaults = array(
'vulnerabilities' => array(),
'db' => array(),
'files' => array(),
);
if ( ! $record ) {
return $defaults;
}
$saved = get_transient( 'WPMR_log_' . $record );
if ( $saved ) {
$saved = json_decode( $saved, true );
}
if ( is_array( $saved ) ) {
$saved = array_merge( $defaults, $saved );
} else {
$saved = $defaults;
}
return $saved;
}
function update_saved_records( $record, $data ) {
if ( is_array( $data ) ) {
$saved = $this->get_saved_records( $record );
$data = array_merge_recursive( $saved, $data );
return set_transient( 'WPMR_log_' . $record, json_encode( $data ), 30 * DAY_IN_SECONDS );
} else {
$this->flog( 'Invalid data sent to function ' . __FUNCTION__ );
}
}
function db_scan_cli() {
global $wpdb;
$scan_sqls = array(
'post' => "SELECT ID AS id, post_content AS content, post_type as post_type FROM $wpdb->posts where post_content LIKE '%s'",
'post_meta' => "SELECT meta_id AS id, meta_value AS content FROM $wpdb->postmeta where meta_value LIKE '%s'",
'option' => "SELECT option_id AS id, option_value AS content FROM $wpdb->options WHERE option_value LIKE '%s' AND option_name NOT IN ('GOTMLS_definitions_array', 'bvruleset')",
'comment' => "SELECT comment_ID AS id, comment_content AS content FROM $wpdb->comments WHERE comment_content LIKE '%s' AND comment_approved = '1'",
);
$definitions = $this->get_definitions()['definitions']['db'];
$db_results = array();
$db_scan_log = array();
if ( ! empty( $GLOBALS['WPMR']['wpmr_extra_db_query'] ) && ! empty( $GLOBALS['WPMR']['wpmr_extra_db_regex'] ) ) {
$definitions['DWPMR'] = array(
'severity' => 'severe',
'query' => $GLOBALS['WPMR']['wpmr_extra_db_query'],
'signature' => $GLOBALS['WPMR']['wpmr_extra_db_regex'],
);
}
foreach ( $definitions as $ver => $details ) {
foreach ( $scan_sqls as $key => $sql ) {
$results = $wpdb->get_results( $wpdb->prepare( $sql, $this->decode( $details['query'] ) ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- SQL is prepared, direct query required for malware signature scanning across multiple tables, caching not appropriate for security scan results.
foreach ( $results as $result ) {
$content = $result['content'];
$id = sanitize_text_field( $result['id'] );
$post_status = empty( $result['post_type'] ) ? '' : 'post-type → ' . $result['post_type'] . '.';
try {
$matches = preg_match( $this->decode( $details['signature'] ), $content, $found );
} catch ( Exception $e ) {
$this->flog( 'Faulty Signature: ' . $definition, false, false, true );
$this->flog( 'Faulty Pattern: ' . $this->decode( $signature['signature'] ), false, false, true );
$this->flog( 'Key: ' . $key, false, false, true );
$this->flog( $e->getMessage(), false, false, true );
continue;
}
if ( $matches >= 1 ) {
if ( in_array( $details['severity'], array( 'severe', 'high' ) ) ) {
$this->update_setting( 'infected', true );
}
$db_results[ $id ] = $this->set_status( $details['severity'], preg_replace( '/[^A-Za-z0-9]/', ' ', $key ) . " ID $id. ", $ver );
$db_scan_log[ $id ] = array(
'severity' => $details['severity'],
'infection' => $ver,
'type' => ucwords( $key ),
'id' => $id,
);
}
}
}
}
if ( $db_results ) {
if ( ! empty( $db_scan_log ) && ! empty( $GLOBALS['WPMR']['timestamp'] ) ) {
$record = $GLOBALS['WPMR']['timestamp'];
$db_scan_log = array( 'db' => $db_scan_log );
$this->update_saved_records( $record, $db_scan_log );
}
return $db_results;
}
return $db_results;
}
function get_whitelist() {
return $this->get_setting( 'whitelist' );
}
function render_whitelist() {
$whitelist = $this->get_whitelist( 'whitelist' );
if ( $whitelist ) {
foreach ( $whitelist as $file => $hash ) {
if ( @hash_file( 'sha256', $file ) == $hash ) {
echo '<p data-file-wrap="' . esc_attr( $file ) . '"><span data-file="' . esc_attr( $file ) . '" class="dashicons dashicons-dismiss remove-from-whitelist"></span>' . esc_html( $file ) . '</p>';
}
}
}
}
function check_page_hack( $url = '' ) {
if ( empty( $url ) || ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
$this->flog( 'Invalid URL ' . print_r( $url, 1 ) . ' in ' . __FUNCTION__ );
return false;
}
$url = add_query_arg( array( 'cachebust' => uniqid( '', 1 ) ), $url );
if ( ! $content = $this->get_remote_response( $url ) ) {
$this->flog( 'Failed to get remote response for URL: ' . $url );
return false;
}
if ( ! $content = wp_remote_retrieve_body( $content ) ) {
$this->flog( '$wp_remote_retrieve_body:' . $content );
return false;
}
if ( empty( $content ) ) {
$this->flog( 'empty content' );
return false;
}
libxml_use_internal_errors( true );
if ( ! class_exists( 'DOMDocument' ) ) {
$this->flog( 'DOMDocument class not found in ' . __FUNCTION__ );
return false;
}
$dom = new DOMDocument();
$dom->loadHTML( $content );
$content = $dom->getElementsByTagName( 'title' );
$strings = array();
foreach ( $content as $c ) {
$strings[] = $c->nodeValue;
}
libxml_use_internal_errors( false );
if ( empty( $strings ) ) {
return false;
}
return $this->check_string_hack( $strings );
}
function check_redirect_hijack( $url, $referer = 'https://www.google.com/' ) {
$url = trailingslashit( $url );
stream_context_set_default(
array(
'http' => array(
'method' => 'GET',
'header' => array( 'Referer: ' . $referer ),
'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : 'WordPress', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized on same line
'follow_location' => 0,
),
)
);
$headers = @get_headers( $url, 1 ); // error control because if allow_url_fopen is not available then this fails
if ( ! empty( $headers ) && is_array( $headers ) ) {
preg_match( '/\d\d\d/', $headers[0], $matches );
} else {
return;
}
$status_code = $matches[0];
if ( ( $status_code == 301 || $status_code == 302 ) && ! empty( $headers['Location'] ) ) {
$r_host = wp_parse_url( $headers['Location'], PHP_URL_HOST );
$wp_host = wp_parse_url( $url, PHP_URL_HOST );
if ( strpos( $this->normalize_host( $wp_host ), $this->normalize_host( $r_host ) ) === false ) {
return true; // redirecting to external domain
}
}
}
function check_string_hack( $strings = array() ) {
$strings[] = get_bloginfo( 'name' );
$strings[] = get_bloginfo( 'description' );
$regexes = array( '/h[\@a]ck[3e]d.*by/is', '/[^<]*hack[3e][rd]/i' );
$infected = false;
foreach ( $strings as $str ) {
$this->flog( 'Checking string: ' . $str );
foreach ( $regexes as $regex ) {
if ( preg_match( $regex, $str ) ) {
$infected = true;
} else {
}
}
}
return $infected;
}
function redirect_hijack() {
if ( 'page' == get_option( 'show_on_front' ) ) {
$page_on_front = get_option( 'page_on_front' );
$page_for_posts = get_option( 'page_for_posts' );
return ( ! empty( $page_on_front ) ? $this->check_redirect_hijack( get_permalink( $page_on_front ) ) : false ) || ( ! empty( $page_for_posts ) ? $this->check_redirect_hijack( get_permalink( $page_for_posts ) ) : false );
} else {
return $this->check_redirect_hijack( home_url() );
}
}
function title_hack() {
if ( 'page' == get_option( 'show_on_front' ) ) {
$page_for_posts = get_option( 'page_for_posts' );
$page_on_front = get_option( 'page_on_front' );
// there may be a chance that only one of these is set: page_on_front has not been set but page_for_posts has been set
return $this->check_page_hack( get_permalink( $page_for_posts ) ) || $this->check_page_hack( get_permalink( $page_on_front ) );
} else {
return $this->check_page_hack( home_url() );
}
}
}