'eval-dynamic-decode', 'weight' => 40, 'pattern' => '/\beval\s*\(\s*(base64_decode|gzinflate|gzuncompress|str_rot13|convert_uudecode)\s*\(/i', 'label' => 'eval() wrapping a decode/decompress function -- the single most common webshell obfuscation pattern' ), array( 'id' => 'superglobal-into-exec', 'weight' => 40, 'pattern' => '/\b(eval|assert|system|exec|shell_exec|passthru|proc_open)\s*\(\s*\$_(POST|GET|REQUEST|COOKIE)/i', 'label' => 'A request superglobal ($_POST/$_GET/etc.) passed directly into a code-execution function' ), array( 'id' => 'assert-as-eval', 'weight' => 25, 'pattern' => '/\bassert\s*\(\s*[\'"]/i', 'label' => 'assert() called with a string argument -- a legacy PHP trick equivalent to eval()' ), array( 'id' => 'dangerous-function-present', 'weight' => 15, 'pattern' => '/\b(system|exec|shell_exec|passthru|proc_open|popen)\s*\(/i', 'label' => 'A shell-execution function is present in the file' ), array( 'id' => 'variable-variable-obfuscation', 'weight' => 15, 'pattern' => '/\$\{\s*[\'"]\w+[\'"]\s*\}|\$\$\w+/', 'label' => 'Variable-variable syntax -- a common obfuscation technique to hide function/variable names from simple text search' ), array( 'id' => 'large-base64-blob', 'weight' => 20, 'pattern' => '/[A-Za-z0-9+\/]{800,}={0,2}/', 'label' => 'A very large base64-like blob is embedded in the file' ), array( 'id' => 'create-function', 'weight' => 15, 'pattern' => '/\bcreate_function\s*\(/i', 'label' => 'create_function() -- deprecated since PHP 7.2, rarely used except to obscure dynamically-generated code' ), array( 'id' => 'file-write-from-request', 'weight' => 20, 'pattern' => '/\b(file_put_contents|fwrite)\s*\([^,]+,\s*\$_(POST|GET|REQUEST)/i', 'label' => 'Writes request data directly to a file -- consistent with a dropper/uploader shell' ), ); } const THRESHOLD_HIGH = 55; const THRESHOLD_MEDIUM = 30; const QUARANTINE_SUFFIX = '.argus-quarantined'; const HTACCESS_MARKER = 'ARGUS WordPress Defence -- deny PHP execution (ADR-0053 ยง9.1)'; const RESTORED_TRUST_OPTION = 'argus_wpd_malware_restored_trust'; const SCAN_WATERMARK_OPTION = 'argus_wpd_malware_watermark'; const LOCKDOWN_STATUS_OPTION = 'argus_wpd_uploads_lockdown_status'; public static function mark_restored_trusted( $rel_path, $hash ) { $trust = get_option( self::RESTORED_TRUST_OPTION, array() ); if ( ! is_array( $trust ) ) { $trust = array(); } $trust[ $rel_path ] = $hash; update_option( self::RESTORED_TRUST_OPTION, $trust, false ); } protected static function is_restored_trusted( $rel_path, $hash ) { $trust = get_option( self::RESTORED_TRUST_OPTION, array() ); return is_array( $trust ) && isset( $trust[ $rel_path ] ) && hash_equals( $trust[ $rel_path ], $hash ); } public static function incremental_scan() { self::ensure_uploads_lockdown(); self::scan_uploads_for_php(); self::scan_recently_changed(); self::rescan_restored_trust(); } public static function ensure_uploads_lockdown() { $uploads = wp_get_upload_dir(); $base = $uploads['basedir'] ?? null; if ( ! $base || ! is_dir( $base ) || ! is_writable( $base ) ) { update_option( self::LOCKDOWN_STATUS_OPTION, 'not_writable', false ); return false; } $path = trailingslashit( $base ) . '.htaccess'; $rule = "# " . self::HTACCESS_MARKER . "\n" . "\n\tphp_flag engine off\n\n" . "\n\tphp_flag engine off\n\n" . "\n\tRequire all denied\n\n"; $existing = file_exists( $path ) ? file_get_contents( $path ) : false; // phpcs:ignore WordPress.WP.AlternativeFunctions $healthy = is_string( $existing ) && false !== strpos( $existing, self::HTACCESS_MARKER ) && false !== strpos( $existing, '\\.ph(p[3457]?|t|tml)"' ); if ( ! $healthy ) { $preserved = is_string( $existing ) ? $existing : ''; $preserved = (string) preg_replace( '/# ' . preg_quote( self::HTACCESS_MARKER, '/' ) . '.*?<\/FilesMatch>\n?/s', '', $preserved ); $written = file_put_contents( $path, $rule . $preserved ); // phpcs:ignore WordPress.WP.AlternativeFunctions if ( false === $written ) { update_option( self::LOCKDOWN_STATUS_OPTION, 'failed_write', false ); return false; } } update_option( self::LOCKDOWN_STATUS_OPTION, 'active', false ); return true; } public static function lockdown_status() { return get_option( self::LOCKDOWN_STATUS_OPTION, 'unknown' ); } protected static function scan_uploads_for_php() { $uploads = wp_get_upload_dir(); $base = $uploads['basedir'] ?? null; if ( ! $base || ! is_dir( $base ) ) { return; } $data_dir = wp_normalize_path( trailingslashit( $base ) . 'argus-wpd-data' ); $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::LEAVES_ONLY ); foreach ( $iterator as $file ) { if ( ! $file->isFile() || 'php' !== strtolower( $file->getExtension() ) ) { continue; } $path = $file->getPathname(); if ( 0 === strpos( wp_normalize_path( $path ), $data_dir ) ) { continue; } $rel = str_replace( wp_normalize_path( ABSPATH ), '', wp_normalize_path( $path ) ); if ( self::is_restored_trusted( $rel, hash_file( 'sha256', $path ) ) ) { continue; } $size = filesize( $path ); $mtime = gmdate( 'Y-m-d H:i:s', filemtime( $path ) ); $result = Argus_Quarantine::quarantine_file( $path, $rel, array( 'engine' => 'Argus_Malware_Scanner', 'rule' => 'php-in-uploads', 'type' => 'signature', 'severity' => 'critical', ) ); $neutralized = false !== $result; $finding_id = Argus_Findings::record( 'malware', 'critical', array( 'what_happened' => 'A PHP file was found inside the uploads directory: ' . $rel, 'why_it_matters' => 'WordPress does not execute PHP inside wp-content/uploads by design -- a legitimate install should never have PHP files there.', 'what_argus_found' => sprintf( 'File: %s, size %s bytes, last modified %s.', $rel, $size, $mtime ), 'when_it_happened' => current_time( 'mysql' ), 'why_suspicious' => 'This is one of the most common webshell placement techniques -- uploads directories are usually writable by the web application, unlike wp-admin/wp-includes.', 'what_could_be_affected' => 'A PHP file here can be executed directly by visiting its URL, potentially giving an attacker full code execution on your server.', 'what_should_you_do' => $neutralized ? 'This file has been quarantined -- moved to a protected, non-web-accessible store. Review it on the Quarantine page and decide whether to restore or permanently delete it.' : 'ARGUS detected this file but could not quarantine it (the uploads directory is not writable by PHP on this host, or it is a symlink ARGUS refuses to move automatically). Remove it manually and investigate how it got there.', ), array( 'quarantine_id' => $neutralized ? $result['id'] : null, 'file_path' => $rel, 'file_mtime' => $mtime, 'neutralized' => $neutralized, ), Argus_Findings::STATUS_OPEN ); if ( $neutralized ) { global $wpdb; $wpdb->update( Argus_DB::table( 'quarantine' ), array( 'finding_id' => $finding_id ), array( 'id' => $result['id'] ), array( '%d' ), array( '%d' ) ); } } } protected static function scan_recently_changed() { $since = (int) get_option( self::SCAN_WATERMARK_OPTION, time() - HOUR_IN_SECONDS ); foreach ( array( WP_PLUGIN_DIR, get_theme_root(), ABSPATH . 'wp-admin', ABSPATH . WPINC ) as $dir ) { if ( ! is_dir( $dir ) ) { continue; } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::LEAVES_ONLY ); foreach ( $iterator as $file ) { if ( ! $file->isFile() || 'php' !== strtolower( $file->getExtension() ) ) { continue; } if ( $file->getMTime() < $since ) { continue; } self::scan_file( $file->getPathname() ); } } update_option( self::SCAN_WATERMARK_OPTION, time(), false ); } protected static function rescan_restored_trust() { $trust = get_option( self::RESTORED_TRUST_OPTION, array() ); if ( ! is_array( $trust ) || empty( $trust ) ) { return; } foreach ( $trust as $rel_path => $known_hash ) { $abs = ABSPATH . ltrim( $rel_path, '/' ); if ( ! is_readable( $abs ) ) { continue; } if ( hash_file( 'sha256', $abs ) === $known_hash ) { continue; } self::scan_file( $abs ); } } public static function score_content( $content ) { $score = 0; $matched = array(); foreach ( self::heuristics() as $rule ) { if ( preg_match( $rule['pattern'], $content ) ) { $score += $rule['weight']; $matched[] = $rule; } } return array( $score, $matched ); } public static function scan_file( $path ) { if ( ! is_readable( $path ) || filesize( $path ) > self::MAX_FILE_BYTES ) { return; } $content = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions if ( false === $content ) { return; } list( $score, $matched ) = self::score_content( $content ); if ( $score < self::THRESHOLD_MEDIUM ) { return; } $severity = $score >= self::THRESHOLD_HIGH ? 'critical' : 'medium'; $rel = str_replace( wp_normalize_path( ABSPATH ), '', wp_normalize_path( $path ) ); $labels = wp_list_pluck( $matched, 'label' ); Argus_Findings::record( 'malware', $severity, array( 'what_happened' => sprintf( 'Suspicious PHP file detected: %s (heuristic score %d)', $rel, $score ), 'why_it_matters' => 'This file contains patterns strongly associated with webshells or backdoors -- code designed to give an attacker persistent, hidden access to your server.', 'what_argus_found' => implode( '; ', $labels ), 'when_it_happened' => current_time( 'mysql' ), 'why_suspicious' => 'These are static-analysis heuristics, not a claim that this file is definitely malicious -- some are also occasionally present in legitimate advanced plugins. ARGUS shows you exactly what matched so you can judge it yourself.', 'what_could_be_affected' => 'If this file is genuinely malicious, an attacker could have code execution on your server through it.', 'what_should_you_do' => 'Review the file manually. If you don\'t recognize it or can\'t explain why it exists, remove it and investigate how it got there (check recent file changes, plugin/theme sources, and user activity).', ), array( 'file_path' => $rel, 'score' => $score, 'matched_rules' => wp_list_pluck( $matched, 'id' ), 'file_mtime' => gmdate( 'Y-m-d H:i:s', filemtime( $path ) ) ) ); } }