initial: v0.1 MVP scaffold
Phase A complete — CLI + 5 scanner modules + reporter: - ftp-walker: basic-ftp + ssh2-sftp-client adapters with upload/download/walk - core-diff: MD5 check vs api.wordpress.org checksums - dropper-hunter: extension-blind PHP detection (catches .css/.svg/.tmp droppers) - cloaker-test: dual-UA (Googlebot vs browser) with sitemap auto-discovery - db-scanner: options, users, sessions, action-scheduler hooks - remote-helper: server-side scan with base64-obfuscated patterns (WAF bypass) - reporter: JSON + HTML + CLI output with severity-based exit codes Inspired by sweetbabyroom.pl hack recovery — captures techniques that detected a dropper Wordfence/custom scanners missed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
119
helpers/remote-scan.php
Normal file
119
helpers/remote-scan.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/**
|
||||
* sbr-malwscan remote helper.
|
||||
* Uploaded to WP root, executes server-side scan, returns JSON, self-deletes.
|
||||
*
|
||||
* Protection:
|
||||
* - Token-based auth via GET param
|
||||
* - Signatures loaded from separate base64-encoded JSON (WAF bypass)
|
||||
* - Self-deletes on shutdown
|
||||
* - Only scans if token valid
|
||||
*/
|
||||
register_shutdown_function(function () {
|
||||
@unlink(__FILE__);
|
||||
});
|
||||
|
||||
$token = $_GET['t'] ?? '';
|
||||
$expected_token = '__TOKEN_PLACEHOLDER__';
|
||||
if ($token === '' || $token !== $expected_token) {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
@set_time_limit(300);
|
||||
@ini_set('memory_limit', '256M');
|
||||
|
||||
$root = dirname(__FILE__);
|
||||
$patterns_file = $root . '/sbr-patterns.json';
|
||||
$patterns = [];
|
||||
if (file_exists($patterns_file)) {
|
||||
$raw = @file_get_contents($patterns_file);
|
||||
$data = @json_decode($raw, true);
|
||||
if (is_array($data) && isset($data['patterns'])) {
|
||||
foreach ($data['patterns'] as $p) {
|
||||
if (isset($p['b64'], $p['id'], $p['severity'])) {
|
||||
$patterns[] = [
|
||||
'id' => $p['id'],
|
||||
'severity' => $p['severity'],
|
||||
'needle' => base64_decode($p['b64']),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@unlink($patterns_file);
|
||||
}
|
||||
|
||||
$findings = [];
|
||||
$files_scanned = 0;
|
||||
$bytes_scanned = 0;
|
||||
|
||||
$dir_iter = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::LEAVES_ONLY
|
||||
);
|
||||
|
||||
$exclude_dirs = ['/wp-content/cache/', '/wp-content/uploads/', '/wp-content/updraft/', '/node_modules/'];
|
||||
$scan_extensions = ['php', 'phtml', 'phar', 'css', 'js', 'svg', 'woff', 'woff2', 'tmp', 'dat', 'html'];
|
||||
|
||||
foreach ($dir_iter as $file) {
|
||||
if (!$file->isFile()) continue;
|
||||
$path = $file->getPathname();
|
||||
$rel = str_replace('\\', '/', substr($path, strlen($root)));
|
||||
foreach ($exclude_dirs as $ex) {
|
||||
if (strpos($rel, $ex) !== false) continue 2;
|
||||
}
|
||||
$ext = strtolower($file->getExtension());
|
||||
if (!in_array($ext, $scan_extensions, true)) continue;
|
||||
$size = $file->getSize();
|
||||
if ($size > 5 * 1024 * 1024) continue;
|
||||
if ($size === 0) continue;
|
||||
|
||||
$files_scanned++;
|
||||
$bytes_scanned += $size;
|
||||
|
||||
$content = @file_get_contents($path);
|
||||
if ($content === false) continue;
|
||||
|
||||
$head = substr($content, 0, 256);
|
||||
$php_extensions = ['php', 'phtml', 'phar'];
|
||||
|
||||
// H1: non-PHP extension starting with <?php
|
||||
if (!in_array($ext, $php_extensions, true)) {
|
||||
$trimmed = ltrim($head);
|
||||
if (strpos($trimmed, '<?php') === 0 || strpos($trimmed, '<?=') === 0) {
|
||||
$findings[] = [
|
||||
'path' => $rel,
|
||||
'severity' => 'critical',
|
||||
'reason' => 'PHP shebang in .' . $ext . ' file',
|
||||
'size' => $size,
|
||||
'mtime' => $file->getMTime(),
|
||||
];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// H2: pattern match
|
||||
foreach ($patterns as $pat) {
|
||||
if ($pat['needle'] !== '' && stripos($content, $pat['needle']) !== false) {
|
||||
$findings[] = [
|
||||
'path' => $rel,
|
||||
'severity' => $pat['severity'],
|
||||
'reason' => 'Matched signature: ' . $pat['id'],
|
||||
'size' => $size,
|
||||
'mtime' => $file->getMTime(),
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'files_scanned' => $files_scanned,
|
||||
'bytes_scanned' => $bytes_scanned,
|
||||
'findings' => $findings,
|
||||
'wp_version' => file_exists($root . '/wp-includes/version.php') ? (function () use ($root) {
|
||||
$c = @file_get_contents($root . '/wp-includes/version.php');
|
||||
return preg_match('/\$wp_version\s*=\s*[\'"]([^\'"]+)/', $c, $m) ? $m[1] : 'unknown';
|
||||
})() : 'not-a-wp',
|
||||
]);
|
||||
Reference in New Issue
Block a user