mirror of
https://ghproxy.net/https://github.com/CaptainCore/captaincore.git
synced 2026-08-06 14:31:28 +08:00
141 lines
5.3 KiB
Bash
Executable file
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)."
|