Files
sbr-malwscan/helpers/remote-scan.php
Jacek Pyziak c4166d1cd4 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>
2026-04-17 19:18:32 +02:00

120 lines
3.7 KiB
PHP

<?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',
]);