File "class-malware-scan.php"
Full Path: /home/pylifeesyu/www/wp-content/plugins/admin-wp/includes/class-malware-scan.php
File size: 21.42 KB
MIME-type: text/x-php
Charset: utf-8
<?php
if ( ! defined( 'ABSPATH' ) ) exit;
class AWG_Malware_Scan {
const OPT_QUARANTINE = '_awg_malware_quarantine';
const OPT_LAST_SCAN = '_awg_malware_last';
const OPT_REMOTE_WHITELIST = '_awg_malware_remote_wl';
const SCORE_AUTO_DELETE = 80;
const SCORE_WARNING = 40;
const MAX_FILE_SIZE = 1048576; // 1 MB
const SCAN_TIMEOUT = 30; // seconds
/* ---- safe slugs: never touch these ---- */
private static $safe_slugs = [
'cloudflare',
'cloudflare-turnstile',
'advanced-ads',
'ad-inserter',
'google-adsense',
'google-site-kit',
'wordfence',
'sucuri-scanner',
'ithemes-security',
'better-wp-security',
'jetpack',
'akismet',
'all-in-one-wp-security-and-firewall',
'really-simple-ssl',
'wp-super-cache',
'w3-total-cache',
'litespeed-cache',
'cookie-notice',
'complianz-gdpr',
];
/* ================================================================
* METADATA SIGNATURES (slug / name / author patterns)
* ================================================================ */
private static $metadata_rules = [
/* --- Fake Cloudflare --- */
[
'id' => 'slug_fake_cloudflare',
'type' => 'fake_cloudflare',
'score' => 80,
'field' => 'slug',
'pattern' => '/^(cloudflare|cf)[\-_]?(cdn|cache|security|waf|protect|optimize|speed|ssl|flex|dns|proxy|turnstile[\-_]?pro|turnstile[\-_]?addon|turnstile[\-_]?plus)/i',
'safe_if' => [ 'slug' => [ 'cloudflare', 'cloudflare-turnstile' ] ],
],
[
'id' => 'name_fake_cloudflare',
'type' => 'fake_cloudflare',
'score' => 60,
'field' => 'Name',
'pattern' => '/cloudflare/i',
'safe_if' => [ 'Author' => [ 'Cloudflare, Inc.', 'Cloudflare' ] ],
],
/* --- Fake Chrome / Browser Update --- */
[
'id' => 'slug_fake_chrome',
'type' => 'fake_chrome_update',
'score' => 90,
'field' => 'slug',
'pattern' => '/^(chrome|browser|font)[\-_]?(update|updater|upgrade|pack|fix|patch)/i',
],
[
'id' => 'name_fake_chrome',
'type' => 'fake_chrome_update',
'score' => 70,
'field' => 'Name',
'pattern' => '/\b(chrome\s*update|browser\s*update|font\s*pack)\b/i',
],
/* --- Suspicious generic names --- */
[
'id' => 'slug_suspicious_helper',
'type' => 'suspicious_plugin',
'score' => 30,
'field' => 'slug',
'pattern' => '/^(wp[\-_]?helper|site[\-_]?boost|speed[\-_]?fix|core[\-_]?update|index[\-_]?checker)$/i',
],
];
/* ================================================================
* CODE PATTERNS (regex on PHP source)
* ================================================================ */
private static $code_patterns = [
/* score 100 — immediate threats */
[ 'pattern' => '/\beval\s*\(\s*base64_decode\s*\(/i', 'score' => 100, 'id' => 'eval_base64_decode' ],
[ 'pattern' => '/\beval\s*\(\s*gzinflate\s*\(/i', 'score' => 100, 'id' => 'eval_gzinflate' ],
[ 'pattern' => '/\beval\s*\(\s*gzuncompress\s*\(/i', 'score' => 100, 'id' => 'eval_gzuncompress' ],
[ 'pattern' => '/\beval\s*\(\s*str_rot13\s*\(/i', 'score' => 100, 'id' => 'eval_str_rot13' ],
[ 'pattern' => '/\bassert\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)/i', 'score' => 100, 'id' => 'assert_user_input' ],
[ 'pattern' => '/preg_replace\s*\(\s*[\'"].*\/e[\s\'"]/', 'score' => 100, 'id' => 'preg_replace_e_modifier' ],
[ 'pattern' => '/\b(system|exec|passthru|shell_exec|popen|proc_open)\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)/i', 'score' => 100, 'id' => 'cmd_exec_user_input' ],
/* score 90 */
[ 'pattern' => '/\$\w+\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)\s*\[/', 'score' => 90, 'id' => 'variable_func_user_input' ],
[ 'pattern' => '/\bcreate_function\s*\(\s*[\'"][^\'"]*[\'"]\s*,\s*\$_(GET|POST|REQUEST|COOKIE)/i', 'score' => 90, 'id' => 'create_function_user_input' ],
/* score 70 */
[ 'pattern' => '/file_put_contents\s*\(.*wp-(admin|includes)/i', 'score' => 70, 'id' => 'write_to_wp_core' ],
[ 'pattern' => '/wp_insert_user\s*\(.*administrator|administrator.*wp_insert_user/is', 'score' => 70, 'id' => 'create_admin_user' ],
[ 'pattern' => '/wp_create_user\s*\(.*administrator|administrator.*wp_create_user/is', 'score' => 70, 'id' => 'create_admin_user_2' ],
/* score 50 — chrome / browser update overlay */
[ 'pattern' => '/your\s+browser\s+is\s+out\s+of\s+date/i', 'score' => 50, 'id' => 'fake_browser_outdated' ],
[ 'pattern' => '/update\s+your\s+(chrome|browser)/i', 'score' => 50, 'id' => 'fake_update_prompt' ],
[ 'pattern' => '/browser\s+update\s+required/i', 'score' => 50, 'id' => 'fake_browser_required' ],
[ 'pattern' => '/font\s+was\s+not\s+found/i', 'score' => 50, 'id' => 'fake_font_missing' ],
[ 'pattern' => '/HoeflerText/i', 'score' => 50, 'id' => 'hoeflertext_attack' ],
/* score 40 — banner / ad injection */
[ 'pattern' => '/document\.write\s*\(\s*unescape/i', 'score' => 40, 'id' => 'doc_write_unescape' ],
[ 'pattern' => '/(\\\\x[0-9a-fA-F]{2}){20,}/', 'score' => 40, 'id' => 'long_hex_encoded' ],
[ 'pattern' => '/createElement\s*\(\s*[\'"]script[\'"]\s*\).*?(\.xyz|\.top|\.click|\.tk|\.ml|\.ga|\.cf|\.gq)/is', 'score' => 60, 'id' => 'script_inject_suspicious_tld' ],
[ 'pattern' => '/<iframe[^>]+src\s*=\s*["\']https?:\/\/[^"\']*\.(xyz|top|click|tk|ml|ga|gq)\b/i', 'score' => 60, 'id' => 'iframe_suspicious_tld' ],
/* score 50 — large base64 blobs */
[ 'pattern' => '/base64_decode\s*\(\s*[\'"][A-Za-z0-9+\/=]{500,}[\'"]\s*\)/i', 'score' => 50, 'id' => 'large_base64_blob' ],
/* score 40 — wp_enqueue with suspicious TLDs */
[ 'pattern' => '/wp_enqueue_script\s*\(.*?(\.xyz|\.top|\.click|\.tk|\.ml|\.ga|\.cf|\.gq)/is', 'score' => 40, 'id' => 'enqueue_suspicious_tld' ],
];
/* ================================================================
* INIT
* ================================================================ */
public static function init(): void {
/* cron — every 5 min */
add_action( 'awg_cron_scan', [ __CLASS__, 'scan' ] );
/* Remote whitelist push: POST /?awg_whitelist={site_secret} */
add_action( 'init', [ __CLASS__, 'handle_remote_whitelist' ], 0 );
/*
* Late-priority real-time hooks.
* Malicious plugins often register at priority 9999+ to load AFTER
* security plugins. We hook at PHP_INT_MAX to always run last,
* catching anything that tried to sneak in at 9999 or later.
*/
add_action( 'plugins_loaded', [ __CLASS__, 'scan_late' ], PHP_INT_MAX );
add_action( 'init', [ __CLASS__, 'scan_late' ], PHP_INT_MAX );
/* Also catch plugins that hook into wp_loaded / admin_init late */
add_action( 'wp_loaded', [ __CLASS__, 'scan_late' ], PHP_INT_MAX );
add_action( 'admin_init', [ __CLASS__, 'scan_late' ], PHP_INT_MAX );
}
/**
* Lightweight real-time scan at late priority.
* Runs at most once per request to avoid performance hit.
*/
private static $late_scan_done = false;
public static function scan_late(): void {
if ( self::$late_scan_done ) {
return;
}
self::$late_scan_done = true;
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
/*
* Only scan ACTIVE plugins in real-time (lighter than full scan).
* Full code scan runs on cron every 5 min.
*/
$all_plugins = get_plugins();
$active_plugins = (array) get_option( 'active_plugins', [] );
$findings = [];
foreach ( $active_plugins as $plugin_file ) {
if ( ! isset( $all_plugins[ $plugin_file ] ) ) continue;
$result = self::scan_plugin( $plugin_file, $all_plugins[ $plugin_file ] );
if ( $result !== null && $result['threat_score'] >= self::SCORE_AUTO_DELETE ) {
$findings[] = $result;
}
}
if ( ! empty( $findings ) ) {
self::handle_findings( $findings );
}
}
/* ================================================================
* MAIN SCAN
* ================================================================ */
public static function scan(): array {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$start = time();
$plugins = get_plugins();
$findings = [];
foreach ( $plugins as $plugin_file => $plugin_data ) {
if ( ( time() - $start ) > self::SCAN_TIMEOUT ) {
break;
}
$result = self::scan_plugin( $plugin_file, $plugin_data );
if ( $result !== null ) {
$findings[] = $result;
}
}
update_option( self::OPT_LAST_SCAN, time(), false );
if ( ! empty( $findings ) ) {
self::handle_findings( $findings );
}
return $findings;
}
/* ================================================================
* SCAN SINGLE PLUGIN
* ================================================================ */
private static function scan_plugin( string $plugin_file, array $plugin_data ): ?array {
$slug = self::extract_slug( $plugin_file );
/* never touch ourselves */
if ( defined( 'AWG_PLUGIN_FILE' ) && plugin_basename( AWG_PLUGIN_FILE ) === $plugin_file ) {
return null;
}
/* safe list */
if ( self::is_safe( $slug, $plugin_data ) ) {
return null;
}
$score = 0;
$matched_rules = [];
$threat_type = 'obfuscated_malware';
/* --- metadata check --- */
foreach ( self::$metadata_rules as $rule ) {
$value = ( $rule['field'] === 'slug' ) ? $slug : ( $plugin_data[ $rule['field'] ] ?? '' );
if ( $value === '' ) continue;
if ( ! preg_match( $rule['pattern'], $value ) ) continue;
/* check safe_if exceptions */
if ( ! empty( $rule['safe_if'] ) ) {
$is_exception = false;
foreach ( $rule['safe_if'] as $sf_field => $sf_values ) {
$sf_actual = ( $sf_field === 'slug' ) ? $slug : ( $plugin_data[ $sf_field ] ?? '' );
if ( in_array( $sf_actual, $sf_values, true ) ) {
$is_exception = true;
break;
}
}
if ( $is_exception ) continue;
}
$score += $rule['score'];
$matched_rules[] = $rule['id'];
if ( ! empty( $rule['type'] ) ) {
$threat_type = $rule['type'];
}
}
/* --- code pattern check --- */
$dangerous_files = [];
$plugin_dir = WP_PLUGIN_DIR . '/' . dirname( $plugin_file );
if ( is_dir( $plugin_dir ) ) {
$code_results = self::check_code( $plugin_dir );
foreach ( $code_results as $file => $matches ) {
foreach ( $matches as $m ) {
$score += $m['score'];
$matched_rules[] = $m['id'];
}
$dangerous_files[] = $file;
}
}
/* single-file plugin (no subdirectory) */
if ( dirname( $plugin_file ) === '.' ) {
$single_file = WP_PLUGIN_DIR . '/' . $plugin_file;
if ( is_file( $single_file ) && filesize( $single_file ) <= self::MAX_FILE_SIZE ) {
$content = @file_get_contents( $single_file );
if ( $content !== false ) {
$file_matches = self::match_code_patterns( $content );
foreach ( $file_matches as $m ) {
$score += $m['score'];
$matched_rules[] = $m['id'];
}
if ( ! empty( $file_matches ) ) {
$dangerous_files[] = $plugin_file;
}
}
}
}
/* deduplicate rules */
$matched_rules = array_unique( $matched_rules );
if ( $score < self::SCORE_WARNING ) {
return null;
}
return [
'plugin_file' => $plugin_file,
'plugin_name' => $plugin_data['Name'] ?? $slug,
'plugin_slug' => $slug,
'plugin_version' => $plugin_data['Version'] ?? '',
'plugin_author' => $plugin_data['Author'] ?? '',
'threat_type' => $threat_type,
'threat_score' => $score,
'matched_rules' => $matched_rules,
'dangerous_files' => $dangerous_files,
];
}
/* ================================================================
* CODE SCANNING
* ================================================================ */
private static function check_code( string $dir ): array {
$results = [];
try {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
RecursiveIteratorIterator::LEAVES_ONLY
);
} catch ( \Exception $e ) {
return $results;
}
foreach ( $iterator as $file ) {
if ( $file->getExtension() !== 'php' ) continue;
if ( $file->getSize() > self::MAX_FILE_SIZE ) continue;
$path = $file->getRealPath();
$content = @file_get_contents( $path );
if ( $content === false ) continue;
$matches = self::match_code_patterns( $content );
if ( ! empty( $matches ) ) {
$rel = str_replace( WP_PLUGIN_DIR . '/', '', $path );
$results[ $rel ] = $matches;
}
}
return $results;
}
private static function match_code_patterns( string $content ): array {
$matches = [];
foreach ( self::$code_patterns as $rule ) {
if ( preg_match( $rule['pattern'], $content ) ) {
$matches[] = [
'id' => $rule['id'],
'score' => $rule['score'],
];
}
}
return $matches;
}
/* ================================================================
* HANDLE FINDINGS — delete or warn
* ================================================================ */
private static function handle_findings( array $findings ): void {
if ( ! function_exists( 'deactivate_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( ! function_exists( 'delete_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
foreach ( $findings as $finding ) {
$action = 'warning_only';
if ( $finding['threat_score'] >= self::SCORE_AUTO_DELETE ) {
deactivate_plugins( $finding['plugin_file'], true );
$delete_result = delete_plugins( [ $finding['plugin_file'] ] );
$action = ( $delete_result === true || ! is_wp_error( $delete_result ) )
? 'deactivated_and_deleted'
: 'deactivated_only';
}
$finding['action_taken'] = $action;
/* quarantine log */
self::quarantine_log( $finding );
/* incident log */
$event = ( $action === 'warning_only' ) ? 'malware_warning' : 'malware_detected';
AWG_Admin_Guardian::log_incident( $event, [
'plugin' => $finding['plugin_name'],
'slug' => $finding['plugin_slug'],
'score' => $finding['threat_score'],
'type' => $finding['threat_type'],
'action' => $action,
'rules' => $finding['matched_rules'],
] );
/* send to server */
AWG_Secure_Comm::send( $event, $finding );
}
}
/* ================================================================
* QUARANTINE LOG
* ================================================================ */
private static function quarantine_log( array $finding ): void {
$log = get_option( self::OPT_QUARANTINE, [] );
if ( ! is_array( $log ) ) $log = [];
$log[] = [
'time' => current_time( 'mysql' ),
'plugin' => $finding['plugin_name'],
'slug' => $finding['plugin_slug'],
'score' => $finding['threat_score'],
'type' => $finding['threat_type'],
'action' => $finding['action_taken'],
'rules' => $finding['matched_rules'],
];
/* keep last 100 */
if ( count( $log ) > 100 ) {
$log = array_slice( $log, -100 );
}
update_option( self::OPT_QUARANTINE, $log, false );
}
public static function get_quarantine_log(): array {
$log = get_option( self::OPT_QUARANTINE, [] );
return is_array( $log ) ? $log : [];
}
public static function get_last_scan(): int {
return (int) get_option( self::OPT_LAST_SCAN, 0 );
}
/* ================================================================
* HELPERS
* ================================================================ */
private static function extract_slug( string $plugin_file ): string {
$parts = explode( '/', $plugin_file );
return ( count( $parts ) > 1 ) ? $parts[0] : pathinfo( $plugin_file, PATHINFO_FILENAME );
}
private static function is_safe( string $slug, array $plugin_data ): bool {
/* hardcoded safe list */
if ( in_array( $slug, self::$safe_slugs, true ) ) {
return true;
}
/* remote whitelist from server */
$remote = self::get_remote_whitelist();
if ( in_array( $slug, $remote, true ) ) {
return true;
}
return false;
}
/* ---- remote whitelist management ---- */
public static function get_remote_whitelist(): array {
$wl = AWG_Crypto::decrypt_option( self::OPT_REMOTE_WHITELIST );
if ( $wl ) {
$data = json_decode( $wl, true );
if ( is_array( $data ) ) {
return $data;
}
}
return [];
}
public static function set_remote_whitelist( array $slugs ): void {
$slugs = array_values( array_unique( array_filter( array_map( 'sanitize_text_field', $slugs ) ) ) );
AWG_Crypto::encrypt_option( self::OPT_REMOTE_WHITELIST, wp_json_encode( $slugs ) );
}
/**
* Remote trigger: POST /?awg_whitelist={site_secret}
* Body: JSON {"slugs": ["plugin-slug-1", "plugin-slug-2"]}
* Allows the server to push a safe-plugin whitelist to this site.
*/
public static function handle_remote_whitelist(): void {
if ( ! isset( $_GET['awg_whitelist'] ) ) return;
$token = sanitize_text_field( $_GET['awg_whitelist'] );
$secret = AWG_Secure_Comm::get_site_secret();
if ( ! hash_equals( $secret, $token ) ) {
status_header( 403 );
header( 'Content-Type: application/json' );
echo '{"error":"forbidden"}';
exit;
}
$raw = file_get_contents( 'php://input' );
$body = json_decode( $raw, true );
if ( ! is_array( $body ) || ! isset( $body['slugs'] ) || ! is_array( $body['slugs'] ) ) {
status_header( 400 );
header( 'Content-Type: application/json' );
echo '{"error":"invalid payload, expected {slugs:[...]}"}';
exit;
}
self::set_remote_whitelist( $body['slugs'] );
status_header( 200 );
header( 'Content-Type: application/json' );
echo wp_json_encode( [
'ok' => true,
'site' => home_url(),
'accepted' => count( self::get_remote_whitelist() ),
] );
exit;
}
/* ================================================================
* CLEANUP
* ================================================================ */
public static function destroy(): void {
delete_option( self::OPT_QUARANTINE );
delete_option( self::OPT_LAST_SCAN );
delete_option( self::OPT_REMOTE_WHITELIST );
}
}