WordPress_Malware_Scanner/wp_malware_scan.sh
#!/usr/bin/env bash
#
# WordPress Malware Scanner v2.3 (2025-05-18)
# by Jason Tools (搭配 AI 協助)
# -------------------------------------------------
# - 多維度風險評分（可調 MIN_RISK_SHOW）
# - 官方 MD5 雜湊比對（完整路徑＋basename）
# - 以 Tab 為欄位分隔，檔名可含空格
# -------------------------------------------------

###################### 使用者調整區 ######################
MIN_RISK_SHOW=9                                  # 風險門檻
TARGET_PATH="${1:-$(pwd)}"                       # 掃描目錄
OUTPUT_DIR="./wp_scan_results"                   # 輸出資料夾
#########################################################

DELIM=$'\t'                                      # 欄位分隔 (Tab)
SUSPICIOUS_FILES="$OUTPUT_DIR/suspicious_files.txt"
HIGH_RISK_RAW="$OUTPUT_DIR/high_risk_raw.txt"    # 格式：score<Tab>filepath
REPORT_TXT="$OUTPUT_DIR/detailed_report.txt"

mkdir -p "$OUTPUT_DIR"
:> "$SUSPICIOUS_FILES"  :> "$HIGH_RISK_RAW"

echo "==> 掃描目標：$TARGET_PATH"
echo "==> 結果輸出：$OUTPUT_DIR"
echo

###################### 1. 搜尋可疑檔 ######################
scan_pattern(){ grep -R --include="*.php*" -l -E "$1" "$TARGET_PATH" 2>/dev/null; }

echo "[1/4] 搜尋可疑程式碼…"
scan_pattern "eval|base64_decode|gzinflate|str_rot13|assert|system|exec|shell_exec|passthru|popen|preg_replace.*e|create_function|include.*\\$_|require.*\\$_|_POST|_GET|_REQUEST|_COOKIE|_SERVER\\[\"HTTP_|_FILES|move_uploaded_file|copy.*tmp_name|file_put_contents|file_get_contents.*http|fsockopen|stream_socket_client|curl|proc_open|posix_" >>"$SUSPICIOUS_FILES"
scan_pattern "FilesMan|WSO|c99|r57|weevely|webshell|b374k|meterpreter|reverse shell|bind shell|adminer" >>"$SUSPICIOUS_FILES"
scan_pattern "\\\\x[0-9a-fA-F]{2}"   >>"$SUSPICIOUS_FILES"
scan_pattern "chr\\([0-9]"           >>"$SUSPICIOUS_FILES"
scan_pattern "goto "                 >>"$SUSPICIOUS_FILES"

find "$TARGET_PATH" -type f -name "*.php*" -size -2k \
     -exec grep -l "_FILES\\|move_uploaded_file\\|copy.*tmp_name" {} + 2>/dev/null >>"$SUSPICIOUS_FILES"
find "$TARGET_PATH" -type f \( -iname "*.jpg" -o -iname "*.png" -o -iname "*.gif" \
      -o -iname "*.jpeg" -o -iname "*.pdf" -o -iname "*.txt" -o -iname "*.ico" \) \
     -exec grep -l "<?php" {} + 2>/dev/null >>"$SUSPICIOUS_FILES"
find "$TARGET_PATH" -type f -name ".*php*" 2>/dev/null >>"$SUSPICIOUS_FILES"

for dir in wp-content/uploads wp-content/cache wp-content/tmp wp-content/upgrade; do
  [ -d "$TARGET_PATH/$dir" ] && find "$TARGET_PATH/$dir" -name "*.php" 2>/dev/null >>"$SUSPICIOUS_FILES"
done

scan_pattern "<\\?(php)?\\s*.{1,200};\\s*\\?>" >>"$SUSPICIOUS_FILES"
scan_pattern "<title>.*[Vv]uln.*patch.*[Nn]ow.*</title>.*<\\?php" >>"$SUSPICIOUS_FILES"

sort -u "$SUSPICIOUS_FILES" -o "$SUSPICIOUS_FILES"
echo "    → 初步可疑檔案：$(wc -l <"$SUSPICIOUS_FILES") 個"
echo

################ 2. 官方 MD5 雜湊排除 ################
echo "[2/4] 官方 MD5 雜湊比對…"
WP_VERSION=""

if [ -f "$TARGET_PATH/wp-includes/version.php" ]; then
  WP_VERSION=$(awk -F"'" '/^\$wp_version/ {print $2; exit}' "$TARGET_PATH/wp-includes/version.php")
elif [ -f "$TARGET_PATH/wp-admin/includes/update-core.php" ]; then
  WP_VERSION=$(awk -F"'" '/^\$wp_version/ {print $2; exit}' "$TARGET_PATH/wp-admin/includes/update-core.php")
fi

if [ -z "$WP_VERSION" ]; then
  echo "⚠️  抓不到 WordPress 版本，跳過雜湊比對"
else
  WP_LOCALE=$(awk -F"'" '/define *\('"'"'WPLANG'"'"'/ {print $4}' "$TARGET_PATH/wp-config.php" 2>/dev/null)
  [ -z "$WP_LOCALE" ] && WP_LOCALE="en_US"
  echo "    → 版本/語系：$WP_VERSION / $WP_LOCALE"

  CHECKSUM_JSON=$(curl -fsSL "https://api.wordpress.org/core/checksums/1.0/?version=${WP_VERSION}&locale=${WP_LOCALE}")
  if [ $? -eq 0 ] && [ -n "$CHECKSUM_JSON" ]; then
    TMP_CS=$(mktemp)
    echo "$CHECKSUM_JSON" | jq -r 'to_entries[] | "\(.key) \(.value)"' >"$TMP_CS"
    TMP_FILTER=$(mktemp)

    while IFS= read -r FILE_PATH; do
      REL="${FILE_PATH#$TARGET_PATH/}"           # 相對路徑
      BASE=$(basename "$REL")

      EXPECTED_MD5=$(awk -v rel="$REL" '$1==rel{print $2; exit}' "$TMP_CS")
      [ -z "$EXPECTED_MD5" ] && EXPECTED_MD5=$(awk -v base="$BASE" '$1~"/"base"$"{print $2; exit}' "$TMP_CS")

      if [ -n "$EXPECTED_MD5" ]; then
        CALC_MD5=$(md5sum "$FILE_PATH" | awk '{print $1}')
        if [ "$CALC_MD5" = "$EXPECTED_MD5" ]; then
          echo "    ✅  排除官方核心檔：$REL"
          continue
        fi
      fi
      echo "$FILE_PATH" >>"$TMP_FILTER"
    done <"$SUSPICIOUS_FILES"

    mv "$TMP_FILTER" "$SUSPICIOUS_FILES"
    rm -f "$TMP_CS"
  else
    echo "⚠️  雜湊下載失敗，跳過比對"
  fi
fi

sort -u "$SUSPICIOUS_FILES" -o "$SUSPICIOUS_FILES"
total_count=$(wc -l <"$SUSPICIOUS_FILES")
echo "    → 比對後剩餘可疑檔案：$total_count 個"
echo

###################### 3. 風險評分 ######################
echo "[3/4] 風險評分…  (門檻 = $MIN_RISK_SHOW)"

while IFS= read -r file; do
  [ ! -f "$file" ] && continue
  score=0

  grep -Eq "eval.*(base64_decode|gzinflate|str_rot13)" "$file" && score=$((score+6))
  grep -Eq "(system|exec|shell_exec|passthru|popen).*?\\$[_A-Z]+" "$file" && score=$((score+5))
  grep -Eiq "(FilesMan|WSO|c99|r57|weevely|webshell)" "$file" && score=$((score+5))
  grep -Eq "\\\\x[0-9a-fA-F]{2}|chr\\([0-9]" "$file" && score=$((score+4))
  grep -Eq "move_uploaded_file|copy.*_FILES.*'tmp_name'" "$file" && score=$((score+3))
  grep -Eq "<\\?(php)?\\s*.{1,200};\\s*\\?>" "$file" && score=$((score+3))
  case "$file" in *.jpg|*.png|*.gif|*.jpeg|*.pdf|*.txt|*.ico) grep -q "<?php" "$file" && score=$((score+3));; esac
  [[ $(basename "$file") == .*php* ]] && score=$((score+2))
  [[ "$file" == *"/wp-content/uploads/"* || "$file" == *"/wp-content/cache/"* || "$file" == *"/wp-content/tmp/"* ]] && score=$((score+2))
  grep -Eq "(eval|assert|system|exec|shell_exec|passthru|popen)" "$file" && score=$((score+2))

  [ $score -ge $MIN_RISK_SHOW ] && printf "%d${DELIM}%s\n" "$score" "$file" >>"$HIGH_RISK_RAW"
done <"$SUSPICIOUS_FILES"

###################### 4. 產出報告 ######################
echo "[4/4] 產出報告…"
sort -nr -t"$DELIM" -k1,1 "$HIGH_RISK_RAW" >"$OUTPUT_DIR/high_risk_sorted.txt"
high_risk_count=$(wc -l <"$OUTPUT_DIR/high_risk_sorted.txt")

{
echo "WordPress 安全掃描報告"
echo "======================="
echo "掃描時間: $(date)"
echo "掃描目標: $TARGET_PATH"
echo "剩餘可疑檔案: $total_count"
echo "列出門檻 (MIN_RISK_SHOW): $MIN_RISK_SHOW"
echo "-----------------------"
echo "★ 高風險檔案 (共 $high_risk_count 個，分數高→低)"
echo
awk -F"$DELIM" '{printf "%3d 分  %s\n",$1,$2}' "$OUTPUT_DIR/high_risk_sorted.txt"
} >"$REPORT_TXT"

echo
echo "🚩 報告已輸出到 $REPORT_TXT"
[ $high_risk_count -gt 0 ] && echo "🚩 符合門檻的高風險檔：$high_risk_count 個"
