NAME scanWordPressBackdoors - heuristically flag PHP files that look like WordPress backdoors/webshells SYNOPSIS # Standalone: walk the whole tree itself perl scanWordPressBackdoors [options] /storage/backup/nfs/jenny/www/clients # List mode: score a pre-filtered file list fed on STDIN # (see scanWordPressBackdoors.sh, which builds this with find+grep) find ... -print0 | perl scanWordPressBackdoors [options] - [display_root] perl scanWordPressBackdoors --help DESCRIPTION Recurses (or, in list mode, accepts a pre-built list of) .php/.phtml/.phps files under a backup tree laid out as client#/web#/web/... and assigns each one a heuristic "suspiciousness" score based on: * dangerous execution primitives ("eval", "shell_exec", "curl_exec", ...) * string obfuscation: hex ("\xNN") and octal ("\NNN") escapes, decimal via chained "chr()" calls, "hex2bin()", "pack('H*'|'C*', ...)" * base64: literal-looking strings, "base64_decode()" calls, and dynamically-built "base"."64_decode" calls * "goto"-based control-flow obfuscation * location red flags: a ".php" file under "wp-content/uploads/" or "wp-content/cache/" (should never be executable there), or a suspicious marker inside "wp-config.php" * filename red flags: "shell"/"webshell"/"c99"/etc in the name, a double extension like image.jpg.php, or a file sitting directly in the WordPress docroot that isn't one of the real core filenames - especially one whose name starts with "wp-" but isn't an exact match (e.g. wp-conffq.php planted next to the real wp-config.php), a common typosquat disguise Every heuristic is weighted higher when the file also sits under a "wp-content/", "wp-admin/", or "wp-includes/" path, or directly in the docroot itself (detected via a "wp-config.php"/"wp-load.php"/ "wp-settings.php" sibling) - i.e. is plausibly part of an actual WordPress install - to keep noise down on backups that also contain non-WordPress PHP code. Files scoring at or above $THRESH_SCORE are reported, grouped by "client#/web#". This is a heuristic scanner, not a signature/AV engine: it will have both false positives (a legitimate plugin using "eval()" or "base64_decode()") and false negatives (a bespoke payload that avoids every listed primitive). Treat its output as a triage list to review by hand, not a verdict. A repeat false positive (e.g. a bundled library like phpseclib or PHPMailer that legitimately trips several heuristics, and shows up identically across many client sites) can be permanently suppressed once reviewed - see "--whitelist=FILE" under ARGUMENTS. ARGUMENTS "--whitelist=FILE" Optional. "FILE" lists SHA-256 hashes (one per line, "#" comments and blank lines ignored) of files that have been manually reviewed and confirmed OK. Only files that already scored at or above $THRESH_SCORE are hashed and checked against this list - matching is on the file's full content, not its name or path, so a compromised file can't evade detection by reusing a previously-whitelisted filename, and a single entry suppresses the same legitimate file everywhere it's bundled identically across client sites. Suppressed files are counted in a "(N file(s) suppressed by whitelist match)" report footer rather than vanishing silently. Not active unless passed explicitly - there is no default whitelist file. Each line just needs a bare 64-hex-character SHA-256 hash somewhere on it, so any of these common recipes work as-is: sha256sum knownGoodFile.php >> whitelist # GNU coreutils (Linux) sha256 -q knownGoodFile.php >> whitelist # FreeBSD base sha256 knownGoodFile.php >> whitelist # FreeBSD base, verbose form If "FILE" is given but can't be opened, the script dies immediately rather than silently running without the whitelist. "--max-bytes=N" Bytes read from the head and (for larger files) tail of each file for scoring. Default 512000. See TUNING KNOBS. "--threshold=N" Alert score a file must reach or exceed to be reported. Default 7. See TUNING KNOBS. "--max-per-site=N" Maximum number of suspicious files listed per site in the report (sites with more are truncated, but "[suspicious files: N]" still shows the true total). Default 12. "--help" Print usage/options and exit 0. "root" Top-level directory to scan (e.g. the clients directory containing every client#/web#/web tree). Standalone mode: the script does its own "File::Find" walk, pruning .git, .svn, node_modules, and vendor directories before descending into them. "-" Switches to list mode: file paths are read NUL-delimited from STDIN instead of walking a directory. Intended to be fed by "scanWordPressBackdoors.sh", which prefilters with "find"+"grep" first so this script's full multi-regex scoring only runs against real candidates. An optional second argument sets the "Scan root:" text shown in the report header (purely cosmetic). TUNING KNOBS $MAX_BYTES, $THRESH_SCORE, and $PRINT_TOP_FILES_PER_SITE are settable on the command line via "--max-bytes", "--threshold", and "--max-per-site" respectively (see ARGUMENTS); their defaults live near the top of the file if you'd rather change them permanently. The weighted pattern lists themselves (@DANGEROUS/@STAGING/@BAD_NAME/etc.) are code-only knobs - see their per-hit score weights throughout "process_file()". EXIT STATUS Always exits 0 on a completed scan (whether or not anything suspicious was found); dies with a usage message if invoked without a valid root directory or STDIN list mode marker, or if "--whitelist=FILE" is given but the file can't be read. SEE ALSO scanWordPressBackdoors.sh - find+grep wrapper that builds a candidate file list for list mode. ../testing/ - real-world backdoor samples used to regression-test detection.