PHP-Script / Shellscript: PHP-Backdoors auf einem Webserver finden
Hier findet sich ein PHP-Script und ein Shellscript, welches dabei helfen soll PHP-Backdoors auf Webservern zu finden. Das Script ist natürlich nicht perfekt und findet auch viele false positives, aber es ist zumindest ein guter Anfang.
Beide durchsuchen alle relevanten Dateien rekursiv nach typischen PHP-Backdoor-Mustern und schreiben einen Abschluss-Report, ohne Dateien zu verändern. Danach kann der Report durchgesehen werden, um verdächtigte Dateien manuell nachzuprüfen.
PHP-Script:
<?php
// find_backdoors.php
// Usage: php find_backdoors.php /path/to/search [output_report_path]
// Writes matches into a temp file and renames to the final report when finished.
$root = $argv[1] ?? '.';
$reportPath = $argv[2] ?? 'report.txt';
// Optional: restrict to these file extensions (empty = all files)
$extensions = ['php','phtml','inc','php5','php7','html','txt'];
// Patterns to search for (PCRE, case-insensitive).
$patterns = [
'/system\s*\(\s*\$_POST\b/i',
'/system\s*\(\s*base64_decode\s*\(/i',
'/\$_REQUEST\s*\[\s*[\'"]cmd[\'"]\s*\]/i',
'/eval\s*\(\s*\$_POST\b/i',
'/eval\s*\(\s*base64_decode\s*\(\s*\$_POST/i',
'/\bpreg_replace\s*\(.*\/e[imsx]*\//i',
'/base64_decode\s*\(\s*[\'"][A-Za-z0-9+\/]{80,}={0,2}[\'"]\s*\)/i',
'/(gzinflate|gzuncompress|str_rot13)\s*\(/i',
];
// helper: check extension filter
function allowed_ext($path, $exts) {
if (empty($exts)) return true;
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
return in_array($ext, $exts, true);
}
// Prepare atomic temp file in same directory as final report (best for rename)
$reportDir = dirname($reportPath) === '' ? getcwd() : dirname($reportPath);
$tempFile = $reportDir . DIRECTORY_SEPARATOR . 'report_' . getmypid() . '.tmp';
// open temp file for writing (will overwrite if exists for this pid)
if (($out = @fopen($tempFile, 'w')) === false) {
fwrite(STDERR, "ERROR: cannot open temp file for writing: {$tempFile}\n");
exit(2);
}
// iterate files recursively
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($it as $fileInfo) {
if (!$fileInfo->isFile() || !$fileInfo->isReadable()) continue;
$filePath = $fileInfo->getPathname();
if (!allowed_ext($filePath, $extensions)) continue;
// open as text and iterate lines to get line numbers
try {
$fh = new SplFileObject($filePath, 'r');
} catch (RuntimeException $e) {
continue;
}
$lineNo = 0;
while (!$fh->eof()) {
$line = $fh->fgets();
$lineNo++;
if ($line === "" || trim($line) === "") continue;
foreach ($patterns as $pat) {
if (preg_match($pat, $line, $m)) {
$snippet = trim($line);
$snippet = preg_replace("/\s+/", " ", $snippet);
if (strlen($snippet) > 300) {
$snippet = substr($snippet, 0, 290) . '...';
}
// Format: file:line:pattern:snippet
$outLine = "{$filePath}:{$lineNo}: {$pat} : {$snippet}\n";
fwrite($out, $outLine);
// break to avoid duplicate hits on same line (optional)
break;
}
}
}
}
// close temp file and atomically rename to final report
fclose($out);
// try to rename (overwrite if exists)
if (@rename($tempFile, $reportPath) === false) {
// fallback: try copy + unlink
if (@copy($tempFile, $reportPath) && @unlink($tempFile)) {
fwrite(STDOUT, "Report written to: {$reportPath}\n");
} else {
fwrite(STDERR, "ERROR: failed to move temp report to {$reportPath}\n");
// keep temp file for inspection
exit(3);
}
} else {
fwrite(STDOUT, "Report written to: {$reportPath}\n");
}
exit(0);
Shellscript:
#!/usr/bin/env bash
set -euo pipefail
# find_backdoors.sh
# Usage:
# ./find_backdoors.sh # uses default ROOT and writes ./report_YYYY-MM-DD.txt
# ./find_backdoors.sh /path/to/root
# ./find_backdoors.sh /path/to/root /path/to/report.txt
ROOT_DEFAULT="/var/www/vhosts/"
ROOT="${1:-$ROOT_DEFAULT}"
# default report: report_CURRENTDATE.txt (YYYY-MM-DD)
REPORT="${2:-report_$(date +%F).txt}"
# TMP placed beside final report for atomic move
REPORT_DIR="$(dirname "$REPORT")"
if [[ -z "$REPORT_DIR" || "$REPORT_DIR" == "." ]]; then
TMP="./report_$$.tmp"
else
TMP="$REPORT_DIR/report_$$.tmp"
fi
: > "$TMP"
# Exclude common large dirs
EXCLUDE_GLOBS=( --glob '!.git' --glob '!node_modules' --glob '!vendor' )
# Patterns (PCRE2 / -P style)
PATTERNS=(
'(?i)system\s*\(\s*\$_POST\b'
'(?i)system\s*\(\s*base64_decode\s*\('
'(?i)\$_REQUEST\s*\[\s*[\"\x27]cmd[\"\x27]\s*\]'
'(?i)eval\s*\(\s*\$_POST\b'
'(?i)eval\s*\(\s*base64_decode\s*\(\s*\$_POST'
'(?i)preg_replace\s*\(.*\/e'
'(?i)base64_decode\s*\(\s*[\"\x27][A-Za-z0-9+/]{80,}={0,2}[\"\x27]\s*\)'
'(?i)(gzinflate|gzuncompress|str_rot13)\s*\('
)
# prefer ripgrep
if command -v rg >/dev/null 2>&1; then
RG_BASE=(rg --pcre2 -nH --hidden -S "${EXCLUDE_GLOBS[@]}" "$ROOT")
for p in "${PATTERNS[@]}"; do
"${RG_BASE[@]}" -e "$p" >> "$TMP" || true
done
else
# grep fallback - limit to common text/web extensions
GREP_BASE=(grep -RInP --binary-files=without-match --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=vendor)
INCLUDES=(--include='*.php' --include='*.phtml' --include='*.inc' --include='*.html' --include='*.txt')
for p in "${PATTERNS[@]}"; do
"${GREP_BASE[@]}" "${INCLUDES[@]}" -e "$p" "$ROOT" >> "$TMP" || true
done
fi
# atomic move
mv -f "$TMP" "$REPORT"
printf 'Report written to: %s\n' "$REPORT"
Welche Möglichkeiten gibt es sonst noch um bösartige Dateien zu finden?
ClamAV hat jedoch nicht erfolgreich eine Test-Webshell gefunden:
[{"Expires":1,"Discard":false,"Value":"<?php exit; echo isset($_POST['SAMPLE']) ? eval($_POST['SAMPLE']) : die('no'); ?>","Path":"\/","Name":"SAMPLE","Domain":"localhost","Secure":false,"Httponly":false,"Max-Age":3}]Ich habe testweise eine solche Backdoor bei virustotal hochgeladen und dabei sind (nur) 6 Virenscanner angesprungen:

Darunter das sehr einfach zu nutzende und kostenlose Trend Micro House Call. So könnte etwa ein Backup aller Dateien erstellt und heruntergeladen werden, dann lokal auf eurem Windows-Rechner entpackt werden, um direkt danach einen Scan mit TrendMicro House Call auszuführen.