captaincore/lib/remote-scripts/detect-fake-dates
2026-03-07 09:24:49 -05:00

141 lines
5.3 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Detect files with forged timestamps (mtime older than birth time)
# Filters out bulk migrations, host-managed deployments, and known-benign plugin files
# Works on Linux filesystems that support birth time (ext4, xfs with newer kernels)
#
# Usage:
# captaincore ssh <site> --script=detect-fake-dates
# captaincore ssh @all --script=detect-fake-dates
#
SCAN_DIR="."
TMPFILE=$(mktemp)
FILTFILE=$(mktemp)
trap "rm -f $TMPFILE $FILTFILE" EXIT
echo "Scanning for files with forged timestamps..."
echo ""
# Pass 1: Collect all PHP files where mtime < birth - 1 hour
while IFS= read -r -d '' file; do
read -r birth mtime ctime <<< "$(stat -c '%W %Y %Z' "$file" 2>/dev/null)"
[ -z "$birth" ] || [ "$birth" = "0" ] && continue
if [ "$mtime" -lt "$((birth - 3600))" ]; then
echo "$birth $mtime $file" >> "$TMPFILE"
fi
done < <(find "$SCAN_DIR" -type f -name '*.php' -print0 2>/dev/null)
if [ ! -s "$TMPFILE" ]; then
echo "No forged timestamps detected."
exit 0
fi
total=$(wc -l < "$TMPFILE")
cp "$TMPFILE" "$FILTFILE"
# Pass 2: Filter out bulk copy/migration clusters
# Group birth times into 10-minute buckets — any bucket with 20+ files is a bulk operation
while read -r count bucket; do
if [ "$count" -ge 20 ]; then
bucket_start=$((bucket * 600))
bucket_date=$(date -d "@$bucket_start" '+%Y-%m-%d %H:%M' 2>/dev/null)
echo "Filtered: bulk copy/migration around $bucket_date ($count files)"
awk -v b="$bucket" '{if (int($1/600) != b) print}' "$FILTFILE" > "${FILTFILE}.tmp"
mv "${FILTFILE}.tmp" "$FILTFILE"
fi
done < <(awk '{print int($1/600)}' "$TMPFILE" | sort | uniq -c | sort -rn)
if [ ! -s "$FILTFILE" ]; then
echo ""
echo "No forged timestamps detected (all mismatches are from bulk operations)."
exit 0
fi
remaining=$(wc -l < "$FILTFILE")
# Pass 3: Filter out host-managed directories (mu-plugins deployed by hosting)
# If >80% of remaining files cluster in a single directory tree with 10+ files, filter them
if [ "$remaining" -gt 0 ]; then
common_base=$(awk '{print $3}' "$FILTFILE" | sed "s|${SCAN_DIR}/||" | cut -d'/' -f1-3 | sort | uniq -c | sort -rn | head -1)
common_count=$(echo "$common_base" | awk '{print $1}')
common_path=$(echo "$common_base" | awk '{$1=""; print $0}' | xargs)
if [ "$common_count" -gt "$((remaining * 80 / 100))" ] && [ "$common_count" -ge 10 ]; then
echo "Filtered: managed deployment in $common_path ($common_count files)"
awk -v p="$common_path" '$3 !~ p' "$FILTFILE" > "${FILTFILE}.tmp"
mv "${FILTFILE}.tmp" "$FILTFILE"
fi
fi
# Pass 4: Filter known-benign files (legitimate plugin/hosting artifacts)
if [ -s "$FILTFILE" ]; then
grep -v -E '(uploads/backup/|uploads/mainwp/index\.php|uploads/template-kits/index\.php|uploads/sucuri/|uploads/wpallimport/|uploads/wpallexport/|uploads/wp-lister/|uploads/smile_fonts/|uploads/avia_fonts/|uploads/bws-custom-code/|uploads/wpo/|uploads/backupbuddy_temp/|uploads/mc4wp-|uploads/wp-activity-log/|uploads/backup-guard/|uploads/redux/index\.php|uploads/astra/index\.php|uploads/wpseo-redirects/|uploads/mailchimp-for-wp/|uploads/sl-uploads/|uploads/pb_backupbuddy/|lwHostsCheck\.php|wordfence-waf\.php|wp-config-orig\.php|MOJOWordpressInstaller|ss_installhelper\.php|500\.php)' "$FILTFILE" > "${FILTFILE}.tmp"
benign_count=$(( $(wc -l < "$FILTFILE") - $(wc -l < "${FILTFILE}.tmp") ))
if [ "$benign_count" -gt 0 ]; then
echo "Filtered: known plugin/hosting artifacts ($benign_count files)"
fi
mv "${FILTFILE}.tmp" "$FILTFILE"
fi
# Pass 5: Filter WordPress core index.php placeholders
if [ -s "$FILTFILE" ]; then
grep -v -E '\./wp-content/(index|themes/index|plugins/index)\.php$' "$FILTFILE" > "${FILTFILE}.tmp"
core_count=$(( $(wc -l < "$FILTFILE") - $(wc -l < "${FILTFILE}.tmp") ))
if [ "$core_count" -gt 0 ]; then
echo "Filtered: WordPress core index.php placeholders ($core_count files)"
fi
mv "${FILTFILE}.tmp" "$FILTFILE"
fi
echo ""
if [ ! -s "$FILTFILE" ]; then
echo "No forged timestamps detected (all mismatches are from bulk operations, managed deployments, or known artifacts)."
exit 0
fi
# Output remaining suspicious files with risk classification
high=0
found=0
while read -r birth mtime file; do
diff=$(( (birth - mtime) / 86400 ))
birth_date=$(date -d "@$birth" '+%Y-%m-%d %H:%M' 2>/dev/null)
mtime_date=$(date -d "@$mtime" '+%Y-%m-%d %H:%M' 2>/dev/null)
# Determine risk level
risk=""
# Content-based backdoor detection for high-confidence matches
if [ -f "$file" ]; then
if grep -qlP '(\$_(COOKIE|REQUEST|POST)\[.{15,}|base64_decode\s*\(\s*\$|eval\s*\(\s*\$|eval\s*\(\s*gzinflate|eval\s*\(\s*str_rot13|assert\s*\(\s*\$|preg_replace\s*\(.*/e)' "$file" 2>/dev/null; then
risk="BACKDOOR"
fi
fi
# PHP in uploads (not index.php) is high risk even without content match
if [ -z "$risk" ]; then
case "$file" in
*uploads/*.php)
case "$file" in
*/index.php) ;;
*) risk="HIGH" ;;
esac
;;
*phpinfo.php)
risk="HIGH" ;;
esac
fi
if [ -n "$risk" ]; then
printf "%-8s %s\n" "$risk" "$file"
high=$((high + 1))
else
printf "%-8s %s\n" "FORGED" "$file"
fi
printf " mtime: %s birth: %s (backdated %d days)\n\n" "$mtime_date" "$birth_date" "$diff"
found=$((found + 1))
done < "$FILTFILE"
echo "Found $found file(s) with suspicious forged timestamps ($high high-risk)."