Server : Apache System : Linux webd003.cluster111.gra.hosting.ovh.net 5.15.206-ovh-vps-grsec-zfs-classid #1 SMP Fri May 15 02:41:25 UTC 2026 x86_64 User : edevmultrx ( 811899) PHP Version : 8.1.33 Disable Function : _dyuweyrj4,_dyuweyrj4r,dl Directory : /home/edevmultrx/ |
<?php
/**
* Watchdog Filester v2 (COMPREHENSIVE) - OVH cron compatible
*
* Usage:
* php watchdog_filester.php # report-only
* php watchdog_filester.php --auto-clean # auto-delete + DB cleanup
*
* Detects ALL 4 infection waves from the May 2026 incident.
* Exit code = warning count (0 = clean)
*/
$BASE = '/home/edevmultrx/www';
$LOG_DIR = '/home/edevmultrx/watchdog-logs';
$TS = gmdate('Ymd-His');
$LOG_FILE = $LOG_DIR . "/watchdog_{$TS}.log";
$AUTO_CLEAN = in_array('--auto-clean', $argv ?? [], true);
if (!is_dir($LOG_DIR)) @mkdir($LOG_DIR, 0755, true);
$LOG_FH = fopen($LOG_FILE, 'w');
function plog($msg) {
global $LOG_FH;
fwrite($LOG_FH, $msg . "\n");
}
function rrmdir($dir) {
if (!is_dir($dir)) return;
foreach (scandir($dir) as $f) {
if ($f === '.' || $f === '..') continue;
$full = $dir . '/' . $f;
@chmod($full, 0700);
if (is_dir($full) && !is_link($full)) rrmdir($full);
else @unlink($full);
}
@rmdir($dir);
}
$warn = 0;
plog('=== Watchdog v2 ' . gmdate('Y-m-d H:i:s') . ' UTC ===');
plog('Mode: ' . ($AUTO_CLEAN ? 'AUTO-CLEAN' : 'REPORT-ONLY'));
plog('');
// SECTION 1: Filester/fileorganizer plugins
plog('--- 1) Filester/fileorganizer in production ---');
$bad_names = ['filester', 'fileorganizer', 'ninja-file-manager'];
$found_dirs = [];
foreach ($bad_names as $name) {
foreach (glob("$BASE/*/wp-content/plugins/$name", GLOB_ONLYDIR) ?: [] as $d) $found_dirs[] = $d;
foreach (glob("$BASE/*/*/wp-content/plugins/$name", GLOB_ONLYDIR) ?: [] as $d) $found_dirs[] = $d;
}
if (!empty($found_dirs)) {
plog('ALERT FOUND:');
foreach ($found_dirs as $d) plog($d);
$warn++;
if ($AUTO_CLEAN) {
foreach ($found_dirs as $d) {
rrmdir($d);
plog(" DELETED: $d");
}
}
} else {
plog('OK No filester/fileorganizer in production');
}
plog('');
// SECTION 2: active_plugins DB check
plog('--- 2) active_plugins (BDD) ---');
$cfg_paths = array_merge(
glob("$BASE/*/wp-config.php") ?: [],
glob("$BASE/*/*/wp-config.php") ?: []
);
$infected = 0;
foreach ($cfg_paths as $cfg) {
$site = basename(dirname($cfg));
$content = @file_get_contents($cfg);
if (!$content) continue;
$db_pass = $db_name = $db_user = $db_host = $prefix = '';
if (preg_match("/DB_PASSWORD['\"]\s*,\s*['\"]([^'\"]+)['\"]/", $content, $m)) $db_pass = $m[1];
if (preg_match("/DB_NAME['\"]\s*,\s*['\"]([^'\"]+)['\"]/", $content, $m)) $db_name = $m[1];
if (preg_match("/DB_USER['\"]\s*,\s*['\"]([^'\"]+)['\"]/", $content, $m)) $db_user = $m[1];
if (preg_match("/DB_HOST['\"]\s*,\s*['\"]([^'\"]+)['\"]/", $content, $m)) $db_host = $m[1];
if (preg_match('/\$table_prefix\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) $prefix = $m[1];
if (!$db_name || !$prefix) continue;
$conn = @new mysqli($db_host, $db_user, $db_pass, $db_name);
if ($conn->connect_errno) continue;
$opt_tbl = $prefix . 'options';
$res = @$conn->query("SELECT option_value FROM {$opt_tbl} WHERE option_name='active_plugins' LIMIT 1");
if (!$res) { $conn->close(); continue; }
$row = $res->fetch_assoc();
$res->close();
if (!$row) { $conn->close(); continue; }
$val = $row['option_value'];
if ((strpos($val, 'filester/') !== false) || (strpos($val, 'fileorganizer/') !== false)) {
plog("ALERT $site has filester/fileorganizer active in DB");
$infected++;
if ($AUTO_CLEAN) {
$arr = @unserialize($val);
if (is_array($arr)) {
$filtered = [];
foreach ($arr as $p) {
if (strpos($p, 'filester/') === false && strpos($p, 'fileorganizer/') === false) {
$filtered[] = $p;
}
}
$new = serialize(array_values($filtered));
$stmt = $conn->prepare("UPDATE {$opt_tbl} SET option_value=? WHERE option_name='active_plugins'");
$stmt->bind_param('s', $new);
if ($stmt->execute()) plog(" CLEANED $site");
$stmt->close();
}
}
}
$conn->close();
}
if ($infected === 0) {
plog('OK No filester/fileorganizer in active_plugins');
} else {
$warn++;
}
plog('');
// SECTION 3: Webshells at site ROOT
plog('--- 3) Webshells at site ROOT ---');
$webshells = [];
$jp_patterns = [
"$BASE/*/jp.php", "$BASE/*/*/jp.php", "$BASE/*/*/*/jp.php",
"$BASE/*/jp.pHtMl", "$BASE/*/*/jp.pHtMl", "$BASE/*/*/*/jp.pHtMl",
];
foreach ($jp_patterns as $pat) foreach (glob($pat) ?: [] as $f) $webshells[] = $f;
$typo_names = ['wp-loiin.php', 'wp-crrm.php', 'wp-haader.php', 'wp-conffg.php', 'wper.php', 'wp-sx-generator.php', 'uploded.php', 'configs.php'];
foreach ($typo_names as $n) {
foreach (glob("$BASE/*/$n") ?: [] as $f) $webshells[] = $f;
foreach (glob("$BASE/*/*/$n") ?: [] as $f) $webshells[] = $f;
foreach (glob("$BASE/*/*/*/$n") ?: [] as $f) $webshells[] = $f;
}
foreach (glob("$BASE/*/admin.php") ?: [] as $f) $webshells[] = $f;
foreach (glob("/home/edevmultrx/home/edevmultrx/jp.*") ?: [] as $f) $webshells[] = $f;
if (!empty($webshells)) {
plog('ALERT WEBSHELLS:');
foreach ($webshells as $w) {
plog($w);
if ($AUTO_CLEAN) {
@unlink($w);
plog(" DELETED: $w");
}
}
$warn++;
} else {
plog('OK No webshells at site root');
}
plog('');
// SECTION 4: Attacker .htaccess pattern
plog('--- 4) Attacker .htaccess pattern ---');
$attacker_htaccess = [];
function scan_htaccess_recursive($dir, &$results, $depth = 0) {
if ($depth > 8) return;
$h = $dir . '/.htaccess';
if (is_file($h)) {
$c = @file_get_contents($h);
if ($c !== false &&
preg_match('/Deny\s+from\s+all/i', $c) &&
preg_match('/<FilesMatch\s+"\^\([^)]+\.php\)\$?"\s*>\s*Order\s+Allow,Deny\s*Allow\s+from\s+all/i', $c)) {
$results[] = $h;
}
}
$entries = @scandir($dir);
if ($entries === false) return;
foreach ($entries as $e) {
if ($e === '.' || $e === '..') continue;
$full = $dir . '/' . $e;
if (is_dir($full) && !is_link($full)) {
scan_htaccess_recursive($full, $results, $depth + 1);
}
}
}
foreach (glob("$BASE/*", GLOB_ONLYDIR) ?: [] as $site_dir) {
scan_htaccess_recursive($site_dir, $attacker_htaccess, 0);
}
if (!empty($attacker_htaccess)) {
plog('ALERT attacker .htaccess:');
foreach ($attacker_htaccess as $h) {
plog($h);
if ($AUTO_CLEAN) {
@unlink($h);
plog(" DELETED: $h");
}
}
$warn++;
} else {
plog('OK No attacker .htaccess');
}
plog('');
// SECTION 5: Content scan
plog('--- 5) Content scan (suspicious paths) ---');
function is_malicious_content($c) {
$reasons = [];
if (md5($c) === 'f73248c0aaebdb31086c3a695a413628') $reasons[] = 'md5-gif89-rot13';
if (md5($c) === 'dee80ccc46986deaf453cefe6fd0040e') $reasons[] = 'md5-fake-wplogin';
if (preg_match('/<!--\s*GIF89/', $c)) $reasons[] = 'gif89-comment';
if (preg_match('/str_rot13/', $c) && preg_match('/file_get_contents/', $c)) $reasons[] = 'rot13-fetch';
if (preg_match_all('/\bgoto\s+[A-Za-z0-9_]{3,15}\s*;/', $c, $m) && count($m[0]) >= 20) {
$reasons[] = 'goto-obfusc(' . count($m[0]) . ')';
}
if (preg_match_all('/\\\\x[0-9a-fA-F]{2}/', $c, $m) && count($m[0]) >= 100) {
$reasons[] = 'hex-esc(' . count($m[0]) . ')';
}
if (preg_match('/PHP\s+File\s+manager/i', $c)) $reasons[] = 'chinese-filemanager';
if (preg_match('/PNG\s+%[a-z0-9]+%[a-z0-9]+!/', $c)) $reasons[] = 'png-decoy';
return $reasons;
}
$suspect_globs = [
"$BASE/*/wp-admin/css/*.php",
"$BASE/*/wp-admin/css/colors/*/*.php",
"$BASE/*/wp-admin/css/colors/*/*/index.php",
"$BASE/*/wp-admin/images/*.php",
"$BASE/*/wp-admin/images/*/*.php",
"$BASE/*/wp-admin/js/widgets/*.php",
"$BASE/*/wp-admin/js/widgets/*/*.php",
"$BASE/*/wp-admin/maint/*/*.php",
"$BASE/*/wp-admin/user/*/*.php",
"$BASE/*/wp-admin/network/*/*.php",
"$BASE/*/wp-admin/includes/*/index.php",
"$BASE/*/wp-includes/css/*/*.php",
"$BASE/*/wp-includes/css/*/*/*.php",
"$BASE/*/wp-includes/js/*/*/index.php",
"$BASE/*/wp-includes/images/*/index.php",
"$BASE/*/wp-includes/images/*/*/index.php",
"$BASE/*/wp-includes/fonts/*/index.php",
"$BASE/*/wp-includes/blocks/*/*/index.php",
"$BASE/*/wp-includes/blocks/*/*/*/index.php",
"$BASE/*/wp-includes/ID3/*/index.php",
"$BASE/*/wp-includes/l10n/*/index.php",
"$BASE/*/wp-includes/customize/*/index.php",
"$BASE/*/wp-includes/interactivity-api/*/index.php",
"$BASE/*/wp-includes/certificates/*/index.php",
"$BASE/*/wp-includes/sitemaps/*/*/index.php",
"$BASE/*/wp-includes/Text/*/*/index.php",
"$BASE/*/wp-includes/Requests/*/*/index.php",
"$BASE/*/wp-includes/widgets/*/index.php",
"$BASE/*/wp-includes/rest-api/*/index.php",
"$BASE/*/wp-includes/rest-api/fields/*/index.php",
"$BASE/*/wp-includes/rest-api/endpoints/*/index.php",
"$BASE/*/wp-includes/style-engine/*/index.php",
"$BASE/*/wp-includes/html-api/*/index.php",
"$BASE/*/wp-includes/block-bindings/*/index.php",
"$BASE/*/wp-includes/php-compat/*/index.php",
"$BASE/*/wp-includes/assets/*/index.php",
"$BASE/*/wp-includes/SimplePie/library/*/index.php",
"$BASE/*/wp-content/uploads/*/*/*/index.php",
"$BASE/*/wp-content/upgrade*/index.php",
"$BASE/*/wp-content/upgrade*/*/index.php",
"$BASE/*/wp-content/cache/*/index.php",
"$BASE/*/wp-content/ai1wm-backups/*/index.php",
"$BASE/*/wp-content/themes/*/.tmb/index.php",
"$BASE/*/wp-content/themes/*/*/*/index.php",
"$BASE/*/wp-content/languages/themes/*/index.php",
"$BASE/*/wp-content/languages/plugins/*/index.php",
"$BASE/*/wp-content/plugins/configs.php",
"$BASE/*/.well-known/acme-challenge/.quarantine/*.php",
];
$whitelist_paths = [
'/wp-includes/SimplePie/',
'/wp-includes/js/tinymce/wp-tinymce.php',
'/wp-includes/fonts/class-wp-font-',
'/wp-content/uploads/wflogs/',
'/wp-content/uploads/redux/',
'/wp-content/uploads/sucuri/',
'/wp-content/uploads/yoast/',
'/wp-content/uploads/wpallimport/',
'/wp-content/uploads/wpo_wcpdf/',
'/wp-content/uploads/forminator/',
];
$found_malware = [];
foreach ($suspect_globs as $g) {
foreach (glob($g) ?: [] as $f) {
$skip = false;
foreach ($whitelist_paths as $wp) if (strpos($f, $wp) !== false) { $skip = true; break; }
if ($skip) continue;
if (basename($f) === 'index.php' && filesize($f) < 100) {
$cc = file_get_contents($f);
if (stripos($cc, 'silence is golden') !== false || stripos($cc, 'silence is the noblest') !== false) continue;
}
$c = @file_get_contents($f);
if ($c === false) continue;
$reasons = is_malicious_content($c);
if (!empty($reasons)) {
$found_malware[$f] = ['reasons' => $reasons, 'size' => filesize($f), 'md5' => md5($c)];
}
}
}
if (!empty($found_malware)) {
plog('ALERT malware:');
foreach ($found_malware as $f => $info) {
plog(" $f");
plog(" sz={$info['size']} md5={$info['md5']} reasons=" . implode(',', $info['reasons']));
if ($AUTO_CLEAN) {
@unlink($f);
plog(" DELETED");
}
}
$warn++;
} else {
plog('OK No malware in suspicious paths');
}
plog('');
// SECTION 6: WP core integrity
plog('--- 6) wp-blog-header.php integrity ---');
$OFFICIAL_MD5 = '5f425a463183f1c6fb79a8bcd113d129';
$tampered = [];
foreach (array_merge(glob("$BASE/*/wp-blog-header.php") ?: [], glob("$BASE/*/*/wp-blog-header.php") ?: []) as $f) {
if (md5_file($f) !== $OFFICIAL_MD5) $tampered[] = $f;
}
if (!empty($tampered)) {
plog('ALERT tampered wp-blog-header.php:');
foreach ($tampered as $t) plog(" $t (MD5 " . md5_file($t) . ")");
$warn++;
} else {
plog('OK wp-blog-header.php integrity good');
}
plog('');
plog("=== Warnings: $warn ===");
$all_logs = glob($LOG_DIR . '/watchdog_*.log') ?: [];
usort($all_logs, function ($a, $b) { return filemtime($b) - filemtime($a); });
foreach (array_slice($all_logs, 30) as $old) @unlink($old);
fclose($LOG_FH);
exit($warn);