mirror of
https://gitlab.com/ArkHost/WP-Security-Pack.git
synced 2026-09-19 17:37:30 +02:00
first commit
This commit is contained in:
@@ -0,0 +1,766 @@
|
||||
<?php
|
||||
/**
|
||||
* Malware scanner for WP Security Pack.
|
||||
*
|
||||
* @package WP_Security_Pack
|
||||
*/
|
||||
|
||||
// Prevent direct access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Malware scanner with signature and hash-based detection.
|
||||
*
|
||||
* Uses two detection methods:
|
||||
* 1. Pattern-based: Regex signatures for suspicious code patterns
|
||||
* 2. Hash-based: Known malware file hashes (100% accurate, no false positives)
|
||||
*
|
||||
* Only scans plugins, themes, and uploads - NOT WordPress core.
|
||||
*/
|
||||
class WPSP_Malware_Scanner {
|
||||
|
||||
/**
|
||||
* Option key for scan results.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const RESULTS_OPTION = 'wpsp_malware_scan_results';
|
||||
|
||||
/**
|
||||
* Option key for last scan time.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const LAST_SCAN_OPTION = 'wpsp_malware_last_scan';
|
||||
|
||||
/**
|
||||
* Option key for malware hash database.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const HASH_DB_OPTION = 'wpsp_malware_hashes';
|
||||
|
||||
/**
|
||||
* Option key for hash database last update.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const HASH_DB_UPDATED_OPTION = 'wpsp_malware_hashes_updated';
|
||||
|
||||
/**
|
||||
* Malware signatures (patterns to detect).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $signatures = array();
|
||||
|
||||
/**
|
||||
* Known malware file hashes (MD5).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $malware_hashes = array();
|
||||
|
||||
/**
|
||||
* Paths/patterns to skip (legitimate libraries and tools).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $skip_paths = array(
|
||||
// This plugin's own files.
|
||||
'wp-security-pack',
|
||||
|
||||
// Common libraries that legitimately use shell/network functions.
|
||||
'phpseclib',
|
||||
'php-curl-class',
|
||||
|
||||
// Package managers.
|
||||
'/vendor/',
|
||||
'/node_modules/',
|
||||
|
||||
// Known backup/security plugins.
|
||||
'updraftplus',
|
||||
'backwpup',
|
||||
'duplicator',
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->load_signatures();
|
||||
$this->load_malware_hashes();
|
||||
|
||||
if ( ! WP_Security_Pack::get_setting( 'malware_scan_enabled', true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule weekly scan.
|
||||
add_action( 'wpsp_weekly_malware_scan', array( $this, 'run_scheduled_scan' ) );
|
||||
|
||||
if ( ! wp_next_scheduled( 'wpsp_weekly_malware_scan' ) ) {
|
||||
wp_schedule_event( time(), 'weekly', 'wpsp_weekly_malware_scan' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load known malware file hashes.
|
||||
*
|
||||
* These are MD5 hashes of known malicious files. When a file matches,
|
||||
* it's 100% confirmed malware - no false positives possible.
|
||||
*/
|
||||
private function load_malware_hashes() {
|
||||
// Try to load from database (updated hashes).
|
||||
$stored_hashes = get_option( self::HASH_DB_OPTION, array() );
|
||||
|
||||
if ( ! empty( $stored_hashes ) ) {
|
||||
$this->malware_hashes = $stored_hashes;
|
||||
return;
|
||||
}
|
||||
|
||||
// Hash database starts empty - relies on signature detection.
|
||||
// Users can add hashes via the 'wpsp_malware_hashes' filter or
|
||||
// by updating from a trusted source using update_hash_database().
|
||||
$this->malware_hashes = array();
|
||||
|
||||
// Allow adding custom hashes via filter.
|
||||
$this->malware_hashes = apply_filters( 'wpsp_malware_hashes', $this->malware_hashes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update malware hash database from remote source.
|
||||
*
|
||||
* @param string $source_url URL to fetch hashes from (JSON format).
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function update_hash_database( $source_url = '' ) {
|
||||
if ( empty( $source_url ) ) {
|
||||
// Default: Could be a GitHub raw URL or your own endpoint.
|
||||
// For now, just return - users can provide their own source.
|
||||
return new WP_Error( 'no_source', __( 'No hash database source URL provided.', 'wp-security-pack' ) );
|
||||
}
|
||||
|
||||
$response = wp_remote_get( $source_url, array( 'timeout' => 30 ) );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( $body, true );
|
||||
|
||||
if ( ! is_array( $data ) ) {
|
||||
return new WP_Error( 'invalid_data', __( 'Invalid hash database format.', 'wp-security-pack' ) );
|
||||
}
|
||||
|
||||
// Merge with existing hashes.
|
||||
$current_hashes = $this->malware_hashes;
|
||||
$new_hashes = array_merge( $current_hashes, $data );
|
||||
|
||||
update_option( self::HASH_DB_OPTION, $new_hashes );
|
||||
update_option( self::HASH_DB_UPDATED_OPTION, time() );
|
||||
|
||||
$this->malware_hashes = $new_hashes;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file matches a known malware hash.
|
||||
*
|
||||
* @param string $file_path Path to file.
|
||||
* @return array|false Malware info if matched, false otherwise.
|
||||
*/
|
||||
public function check_file_hash( $file_path ) {
|
||||
if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$md5_hash = md5_file( $file_path );
|
||||
|
||||
if ( isset( $this->malware_hashes[ $md5_hash ] ) ) {
|
||||
return array(
|
||||
'hash' => $md5_hash,
|
||||
'name' => $this->malware_hashes[ $md5_hash ],
|
||||
'method' => 'hash',
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hash database info.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_hash_database_info() {
|
||||
return array(
|
||||
'count' => count( $this->malware_hashes ),
|
||||
'last_updated' => get_option( self::HASH_DB_UPDATED_OPTION, 0 ),
|
||||
'source' => empty( get_option( self::HASH_DB_OPTION ) ) ? 'built-in' : 'updated',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load malware signatures.
|
||||
*
|
||||
* Patterns are organized by category and designed to minimize false positives
|
||||
* while catching real threats. We do NOT scan WordPress core files.
|
||||
*/
|
||||
private function load_signatures() {
|
||||
$this->signatures = array(
|
||||
|
||||
// =====================================================================
|
||||
// CRITICAL: Code Execution with Obfuscation
|
||||
// These patterns are almost always malicious.
|
||||
// =====================================================================
|
||||
array(
|
||||
'name' => 'Base64 Decode Execution',
|
||||
'pattern' => '/\beval\s*\(\s*base64_decode\s*\(/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Executing base64-encoded PHP code',
|
||||
),
|
||||
array(
|
||||
'name' => 'Gzinflate Execution',
|
||||
'pattern' => '/\beval\s*\(\s*gzinflate\s*\(/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Executing gzip-compressed PHP code',
|
||||
),
|
||||
array(
|
||||
'name' => 'Gzuncompress Execution',
|
||||
'pattern' => '/\beval\s*\(\s*gzuncompress\s*\(/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Executing compressed PHP code',
|
||||
),
|
||||
array(
|
||||
'name' => 'Str_rot13 Execution',
|
||||
'pattern' => '/\beval\s*\(\s*str_rot13\s*\(/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Executing ROT13-obfuscated PHP code',
|
||||
),
|
||||
array(
|
||||
'name' => 'Multiple Decode Layers',
|
||||
'pattern' => '/base64_decode\s*\([^)]*base64_decode/is',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Multiple layers of encoding (heavy obfuscation)',
|
||||
),
|
||||
array(
|
||||
'name' => 'Preg_replace /e Modifier',
|
||||
'pattern' => '/preg_replace\s*\(\s*["\'][^"\']*\/[a-z]*e[a-z]*["\']/',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Code execution via deprecated preg_replace /e modifier',
|
||||
),
|
||||
|
||||
// =====================================================================
|
||||
// CRITICAL: User Input to Code Execution
|
||||
// Direct path from user input to code execution.
|
||||
// =====================================================================
|
||||
array(
|
||||
'name' => 'Eval with User Input',
|
||||
'pattern' => '/\beval\s*\(\s*[\$\.\s]*\$_(POST|GET|REQUEST|COOKIE|SERVER|FILES)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Direct code execution from user input',
|
||||
),
|
||||
array(
|
||||
'name' => 'Assert with User Input',
|
||||
'pattern' => '/\bassert\s*\(\s*[\$\.\s]*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Code execution via assert() from user input',
|
||||
),
|
||||
array(
|
||||
'name' => 'Create_function with User Input',
|
||||
'pattern' => '/\bcreate_function\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Dynamic function creation with user input',
|
||||
),
|
||||
array(
|
||||
'name' => 'Call_user_func with User Input',
|
||||
'pattern' => '/\bcall_user_func(_array)?\s*\(\s*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Calling arbitrary function from user input',
|
||||
),
|
||||
array(
|
||||
'name' => 'Variable Function with User Input',
|
||||
'pattern' => '/\$_(POST|GET|REQUEST|COOKIE)\s*\[[^\]]+\]\s*\(/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Calling function name from user input',
|
||||
),
|
||||
|
||||
// =====================================================================
|
||||
// CRITICAL: Shell Command Execution with User Input
|
||||
// =====================================================================
|
||||
array(
|
||||
'name' => 'Shell_exec with User Input',
|
||||
'pattern' => '/\bshell_exec\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Shell command execution with user input',
|
||||
),
|
||||
array(
|
||||
'name' => 'System with User Input',
|
||||
'pattern' => '/\bsystem\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'System command execution with user input',
|
||||
),
|
||||
array(
|
||||
'name' => 'Passthru with User Input',
|
||||
'pattern' => '/\bpassthru\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Passthru command execution with user input',
|
||||
),
|
||||
array(
|
||||
'name' => 'Exec with User Input',
|
||||
'pattern' => '/\bexec\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Exec command execution with user input',
|
||||
),
|
||||
array(
|
||||
'name' => 'Popen with User Input',
|
||||
'pattern' => '/\bpopen\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Process opened with user-controlled command',
|
||||
),
|
||||
array(
|
||||
'name' => 'Proc_open with User Input',
|
||||
'pattern' => '/\bproc_open\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Process opened with user-controlled command',
|
||||
),
|
||||
|
||||
// =====================================================================
|
||||
// CRITICAL: File Inclusion Vulnerabilities
|
||||
// =====================================================================
|
||||
array(
|
||||
'name' => 'Include with User Input',
|
||||
'pattern' => '/\b(include|require|include_once|require_once)\s*\(?\s*[\$\.\s]*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Local/Remote File Inclusion vulnerability',
|
||||
),
|
||||
|
||||
// =====================================================================
|
||||
// CRITICAL: File Write Vulnerabilities
|
||||
// =====================================================================
|
||||
array(
|
||||
'name' => 'File_put_contents with User Input',
|
||||
'pattern' => '/\bfile_put_contents\s*\([^,]*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Writing to user-controlled file path',
|
||||
),
|
||||
array(
|
||||
'name' => 'Fwrite with User Content',
|
||||
'pattern' => '/\bfwrite\s*\([^,]+,\s*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'high',
|
||||
'description' => 'Writing user content to file',
|
||||
),
|
||||
array(
|
||||
'name' => 'Fopen with User Path',
|
||||
'pattern' => '/\bfopen\s*\(\s*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Opening user-controlled file path',
|
||||
),
|
||||
array(
|
||||
'name' => 'Unrestricted File Upload Path',
|
||||
'pattern' => '/move_uploaded_file\s*\([^,]+,\s*[^)]*\$_(POST|GET|REQUEST)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Uploading file to user-controlled path',
|
||||
),
|
||||
|
||||
// =====================================================================
|
||||
// HIGH: Known Backdoor Patterns
|
||||
// Specific patterns that indicate known malware structures.
|
||||
// =====================================================================
|
||||
array(
|
||||
'name' => 'Web Shell Upload Form',
|
||||
'pattern' => '/<form[^>]*enctype=["\']multipart\/form-data["\'][^>]*>.*<input[^>]*type=["\']file["\'].*\$_FILES/is',
|
||||
'severity' => 'high',
|
||||
'description' => 'File upload form with immediate processing (potential backdoor)',
|
||||
),
|
||||
array(
|
||||
'name' => 'Eval POST/GET Pattern',
|
||||
'pattern' => '/\beval\s*\(\s*\$_(POST|GET)\s*\[\s*["\'][a-z0-9_]+["\']\s*\]\s*\)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Classic backdoor pattern: eval($_POST[key])',
|
||||
),
|
||||
array(
|
||||
'name' => 'Base64 POST Execution',
|
||||
'pattern' => '/\beval\s*\(\s*base64_decode\s*\(\s*\$_(POST|GET|REQUEST)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Executing base64-encoded user input',
|
||||
),
|
||||
array(
|
||||
'name' => 'Gunzip Eval Chain',
|
||||
'pattern' => '/\beval\s*\(\s*gzuncompress\s*\(\s*base64_decode/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Multi-layer deobfuscation chain',
|
||||
),
|
||||
|
||||
// =====================================================================
|
||||
// HIGH: Obfuscation Indicators
|
||||
// =====================================================================
|
||||
array(
|
||||
'name' => 'Very Long Encoded String',
|
||||
'pattern' => '/["\'][A-Za-z0-9+\/=]{1500,}["\']/s',
|
||||
'severity' => 'high',
|
||||
'description' => 'Extremely long encoded string (likely obfuscated malware)',
|
||||
),
|
||||
array(
|
||||
'name' => 'Hex Escape Sequence',
|
||||
'pattern' => '/(\\\\x[0-9a-fA-F]{2}){20,}/i',
|
||||
'severity' => 'high',
|
||||
'description' => 'Long hex-encoded string (obfuscation technique)',
|
||||
),
|
||||
array(
|
||||
'name' => 'Chr() Obfuscation',
|
||||
'pattern' => '/(\bchr\s*\(\s*\d+\s*\)\s*\.?\s*){10,}/i',
|
||||
'severity' => 'high',
|
||||
'description' => 'Building string from chr() calls (obfuscation)',
|
||||
),
|
||||
array(
|
||||
'name' => 'Array Character Building',
|
||||
'pattern' => '/\$\w+\s*=\s*["\'][A-Za-z]+["\'];\s*\$\w+\s*=\s*\$\w+\[\d+\]\s*\.\s*\$\w+\[\d+\]/i',
|
||||
'severity' => 'medium',
|
||||
'description' => 'Building function names from array indices (obfuscation)',
|
||||
),
|
||||
|
||||
// =====================================================================
|
||||
// HIGH: WordPress-Specific Attacks
|
||||
// =====================================================================
|
||||
array(
|
||||
'name' => 'WP User Creation Backdoor',
|
||||
'pattern' => '/wp_create_user\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Creating WordPress user from user input',
|
||||
),
|
||||
array(
|
||||
'name' => 'WP Insert User Backdoor',
|
||||
'pattern' => '/wp_insert_user\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Inserting WordPress user from user input',
|
||||
),
|
||||
array(
|
||||
'name' => 'WP Option Injection',
|
||||
'pattern' => '/update_option\s*\(\s*\$_(POST|GET|REQUEST)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Updating arbitrary WordPress option from user input',
|
||||
),
|
||||
array(
|
||||
'name' => 'WP Auth Cookie Manipulation',
|
||||
'pattern' => '/wp_set_auth_cookie\s*\([^)]*\$_(POST|GET|REQUEST)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Setting auth cookie from user input (authentication bypass)',
|
||||
),
|
||||
array(
|
||||
'name' => 'WP Role Escalation',
|
||||
'pattern' => '/->set_role\s*\(\s*["\']administrator["\']\s*\)|->add_cap\s*\([^)]*\$_(POST|GET|REQUEST)/i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Privilege escalation attempt',
|
||||
),
|
||||
|
||||
// =====================================================================
|
||||
// HIGH: Suspicious Patterns with Context
|
||||
// These require specific dangerous context to trigger.
|
||||
// =====================================================================
|
||||
array(
|
||||
'name' => 'Error Suppression with Eval',
|
||||
'pattern' => '/@\s*eval\s*\(/i',
|
||||
'severity' => 'high',
|
||||
'description' => 'Eval with error suppression (hiding malicious activity)',
|
||||
),
|
||||
array(
|
||||
'name' => 'Remote Code Fetch and Execute',
|
||||
'pattern' => '/eval\s*\(\s*file_get_contents\s*\(\s*["\']https?:\/\//i',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Fetching and executing remote code',
|
||||
),
|
||||
array(
|
||||
'name' => 'CURL Fetch and Execute',
|
||||
'pattern' => '/eval\s*\([^)]*curl_exec/is',
|
||||
'severity' => 'critical',
|
||||
'description' => 'Executing code fetched via CURL',
|
||||
),
|
||||
array(
|
||||
'name' => 'Dynamic URL Fetch with User Input',
|
||||
'pattern' => '/file_get_contents\s*\(\s*\$_(POST|GET|REQUEST|COOKIE)/i',
|
||||
'severity' => 'high',
|
||||
'description' => 'Fetching content from user-controlled URL',
|
||||
),
|
||||
);
|
||||
|
||||
// Allow adding custom signatures via filter.
|
||||
$this->signatures = apply_filters( 'wpsp_malware_signatures', $this->signatures );
|
||||
}
|
||||
|
||||
/**
|
||||
* Run scheduled scan.
|
||||
*/
|
||||
public function run_scheduled_scan() {
|
||||
$results = $this->scan_files();
|
||||
|
||||
if ( ! empty( $results ) ) {
|
||||
update_option( self::RESULTS_OPTION, $results );
|
||||
$this->send_alert( $results );
|
||||
}
|
||||
|
||||
update_option( self::LAST_SCAN_OPTION, time() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan files for malware signatures.
|
||||
*
|
||||
* Only scans plugins, themes, and uploads directories.
|
||||
* Does NOT scan WordPress core to avoid false positives.
|
||||
*
|
||||
* @param array $paths Paths to scan (default: wp-content directories only).
|
||||
* @return array
|
||||
*/
|
||||
public function scan_files( $paths = array() ) {
|
||||
if ( empty( $paths ) ) {
|
||||
// Only scan wp-content directories, NOT WordPress core.
|
||||
$paths = array(
|
||||
WP_CONTENT_DIR . '/plugins/',
|
||||
WP_CONTENT_DIR . '/themes/',
|
||||
WP_CONTENT_DIR . '/uploads/',
|
||||
WP_CONTENT_DIR . '/mu-plugins/',
|
||||
);
|
||||
}
|
||||
|
||||
$results = array();
|
||||
$file_count = 0;
|
||||
$max_files = 10000; // Limit to prevent timeout.
|
||||
|
||||
foreach ( $paths as $path ) {
|
||||
if ( ! file_exists( $path ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( is_file( $path ) ) {
|
||||
if ( ! $this->should_skip_file( $path ) ) {
|
||||
$file_results = $this->scan_file( $path );
|
||||
if ( ! empty( $file_results ) ) {
|
||||
$results[ $path ] = $file_results;
|
||||
}
|
||||
}
|
||||
$file_count++;
|
||||
} else {
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator( $path, RecursiveDirectoryIterator::SKIP_DOTS ),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ( $iterator as $file ) {
|
||||
if ( $file_count >= $max_files ) {
|
||||
break 2;
|
||||
}
|
||||
|
||||
if ( ! $file->isFile() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$file_path = $file->getPathname();
|
||||
$ext = strtolower( $file->getExtension() );
|
||||
|
||||
// Only scan PHP files.
|
||||
if ( 'php' !== $ext ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip large files (> 2MB).
|
||||
if ( $file->getSize() > 2 * 1024 * 1024 ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip known safe paths.
|
||||
if ( $this->should_skip_file( $file_path ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$file_results = $this->scan_file( $file_path );
|
||||
if ( ! empty( $file_results ) ) {
|
||||
$results[ $file_path ] = $file_results;
|
||||
}
|
||||
|
||||
$file_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_option( self::LAST_SCAN_OPTION, time() );
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file should be skipped (known legitimate libraries).
|
||||
*
|
||||
* @param string $file_path File path to check.
|
||||
* @return bool True if file should be skipped.
|
||||
*/
|
||||
private function should_skip_file( $file_path ) {
|
||||
foreach ( $this->skip_paths as $skip ) {
|
||||
if ( stripos( $file_path, $skip ) !== false ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a single file.
|
||||
*
|
||||
* @param string $file_path File path.
|
||||
* @return array
|
||||
*/
|
||||
public function scan_file( $file_path ) {
|
||||
if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$findings = array();
|
||||
|
||||
// First: Check against known malware hashes (100% accurate).
|
||||
$hash_match = $this->check_file_hash( $file_path );
|
||||
if ( $hash_match ) {
|
||||
$findings[] = array(
|
||||
'name' => 'Known Malware: ' . $hash_match['name'],
|
||||
'severity' => 'critical',
|
||||
'description' => 'File matches known malware hash (MD5: ' . $hash_match['hash'] . ')',
|
||||
'match' => 'Hash match - confirmed malware',
|
||||
'confirmed' => true,
|
||||
);
|
||||
// Hash match is definitive - still scan for patterns but mark as confirmed.
|
||||
}
|
||||
|
||||
// Second: Pattern-based detection.
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$content = file_get_contents( $file_path );
|
||||
|
||||
if ( ! empty( $content ) ) {
|
||||
foreach ( $this->signatures as $signature ) {
|
||||
if ( preg_match( $signature['pattern'], $content, $matches ) ) {
|
||||
$findings[] = array(
|
||||
'name' => $signature['name'],
|
||||
'severity' => $signature['severity'],
|
||||
'description' => $signature['description'],
|
||||
'match' => substr( $matches[0], 0, 100 ),
|
||||
'confirmed' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $findings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email alert about scan results.
|
||||
*
|
||||
* @param array $results Scan results.
|
||||
*/
|
||||
private function send_alert( $results ) {
|
||||
if ( ! WP_Security_Pack::get_setting( 'email_alerts_enabled', false ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = WP_Security_Pack::get_setting( 'email_alerts_address', get_option( 'admin_email' ) );
|
||||
$site_name = get_bloginfo( 'name' );
|
||||
$site_url = home_url();
|
||||
|
||||
// Count by severity.
|
||||
$counts = array(
|
||||
'critical' => 0,
|
||||
'high' => 0,
|
||||
'medium' => 0,
|
||||
'low' => 0,
|
||||
);
|
||||
|
||||
foreach ( $results as $file => $findings ) {
|
||||
foreach ( $findings as $finding ) {
|
||||
$counts[ $finding['severity'] ]++;
|
||||
}
|
||||
}
|
||||
|
||||
$subject = sprintf(
|
||||
/* translators: %s: Site name */
|
||||
__( '[%s] Malware Scan Alert - Suspicious Files Found', 'wp-security-pack' ),
|
||||
$site_name
|
||||
);
|
||||
|
||||
$message = sprintf(
|
||||
/* translators: %s: Site URL */
|
||||
__( "WP Security Pack malware scan has detected suspicious files on %s.\n\n", 'wp-security-pack' ),
|
||||
$site_url
|
||||
);
|
||||
|
||||
$message .= __( "Summary:\n", 'wp-security-pack' );
|
||||
$message .= sprintf( __( "- Critical: %d\n", 'wp-security-pack' ), $counts['critical'] );
|
||||
$message .= sprintf( __( "- High: %d\n", 'wp-security-pack' ), $counts['high'] );
|
||||
$message .= sprintf( __( "- Medium: %d\n", 'wp-security-pack' ), $counts['medium'] );
|
||||
$message .= sprintf( __( "- Low: %d\n\n", 'wp-security-pack' ), $counts['low'] );
|
||||
|
||||
$message .= __( "Files with issues:\n", 'wp-security-pack' );
|
||||
|
||||
$count = 0;
|
||||
foreach ( $results as $file => $findings ) {
|
||||
if ( $count >= 20 ) {
|
||||
$message .= sprintf(
|
||||
/* translators: %d: Number of additional files */
|
||||
__( "... and %d more files\n", 'wp-security-pack' ),
|
||||
count( $results ) - 20
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
$relative_path = str_replace( ABSPATH, '', $file );
|
||||
$message .= "\n" . $relative_path . ":\n";
|
||||
|
||||
foreach ( $findings as $finding ) {
|
||||
$message .= sprintf( " - [%s] %s\n", strtoupper( $finding['severity'] ), $finding['name'] );
|
||||
}
|
||||
|
||||
$count++;
|
||||
}
|
||||
|
||||
$message .= "\n" . __( "Please review these files in your WordPress admin panel under Settings > WP Security Pack.", 'wp-security-pack' );
|
||||
$message .= "\n\n" . __( "Note: Some detections may be false positives. Review each file carefully before taking action.", 'wp-security-pack' );
|
||||
|
||||
wp_mail( $email, $subject, $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last scan results.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_last_scan_results() {
|
||||
return array(
|
||||
'time' => get_option( self::LAST_SCAN_OPTION, 0 ),
|
||||
'results' => get_option( self::RESULTS_OPTION, array() ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear scan results.
|
||||
*/
|
||||
public function clear_results() {
|
||||
delete_option( self::RESULTS_OPTION );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get severity color.
|
||||
*
|
||||
* @param string $severity Severity level.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_severity_color( $severity ) {
|
||||
$colors = array(
|
||||
'critical' => '#dc3545',
|
||||
'high' => '#fd7e14',
|
||||
'medium' => '#ffc107',
|
||||
'low' => '#17a2b8',
|
||||
);
|
||||
|
||||
return isset( $colors[ $severity ] ) ? $colors[ $severity ] : '#6c757d';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user