「我家的網站是不是被攻擊了?」當被這樣問起時,答案多半就在伺服器的存取記錄裡。只是單純用 tail 盯著原始日誌看,也無法從一天數萬行裡把異常挑出來。

這篇文章是為了能透過 SSH 登入伺服器的網站負責人、網站管理者、製作公司而寫的,內容整理了如何把存取記錄經過 7 個指令,縮小到「只剩值得看的幾行」,再進一步用 fail2ban 自動封鎖找到的對象。前提是 nginx 或 Apache 的 combined 格式日誌(也會順帶提到 WordPress 特有的觀察重點)。

步驟 0:確認日誌中是否有「真正的客戶端 IP」

如果不先確認這一步,後面的統計全部都會白做。在 Cloudflare、負載平衡器、反向代理的架構下,若沒有額外設定,記錄下來的只會是代理伺服器的 IP。上游全部都變成同一個 IP,若直接拿去餵給 fail2ban,就可能把自己網站的入口整個封掉。

tail -1 /var/log/nginx/access.log

如果最前面的位址不是訪客的 IP,而是 CDN 或負載平衡器的位址,就必須做還原設定。

nginx 的情況

combined 格式的開頭是 $remote_addr。要把它改成代理伺服器傳來的標頭值,就要用 realip 模組。

# 列出信任的代理範圍(Cloudflare 的所有範圍請見 https://www.cloudflare.com/ips/)
set_real_ip_from 173.245.48.0/20;
# ... 請把公開的所有範圍都列出來

real_ip_header    CF-Connecting-IP;  # 預設是 X-Real-IP,請依你的路徑調整
real_ip_recursive off;               # 預設 off = 採用標頭中的最後一個值

可用 nginx -V 2>&1 | tr ' ' '\n' | grep realip 確認模組是否存在(套件版通常都會內建)。沒有寫在 set_real_ip_from 裡的來源所帶來的標頭會被忽略。 如果把它設成 0.0.0.0/0 這種全開狀態,任何人都可以偽造 IP,所以一定要只寫經路上的範圍。

Apache 的情況

用 mod_remoteip 還原,並且把日誌格式從 %h 改成 %a%a = 還原後的客戶端 IP,%{c}a = 前一個連線來源 = 代理伺服器)。

RemoteIPHeader       CF-Connecting-IP
RemoteIPTrustedProxy 173.245.48.0/20

LogFormat "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined_realip
CustomLog ${APACHE_LOG_DIR}/access.log combined_realip

套用後,用自己網路開啟網站,若 tail -1 看到的是自己的 IP,就代表準備完成。

統計的共通部分

combined 格式在 nginx 與 Apache 的欄位位置相同,因此可以直接共用同一段 awk。

203.0.113.10 - - [10/Sep/2026:07:12:33 +0900] "POST /wp-login.php HTTP/1.1" 200 1234 "-" "curl/8.5.0"
$1           $2$3 $4                    $5    $6    $7             $8       $9  $10  $11  $12

$1 = 來源 IP,$6 = 方法(前面會帶 "),$7 = 路徑,$9 = 狀態碼,$10 = 回應大小。因為也想看已輪替的舊日誌,所以先把目標集中到變數裡。GNU 的 zcat -f 會直接處理未壓縮檔,因此 .gz 和原始日誌可以混著丟。

LOGS="/var/log/nginx/access.log /var/log/nginx/access.log.1 /var/log/nginx/access.log.*.gz"
zcat -f $LOGS | wc -l      # 確認目標行數

Apache 則改成 /var/log/apache2/access.log*(RHEL 系統則是 /var/log/httpd/access_log*)。

統計 1:來源 IP 的前幾名

zcat -f $LOGS | awk '{print $1}' | sort | uniq -c | sort -rn | head -20

先看整體輪廓。前幾名出現自家監控服務、爬蟲、CDN 都算正常。如果某個不認識的 IP 以明顯異常的請求數衝到第一名,再進一步看它的內容。

統計 2:登入相關端點的 POST

zcat -f $LOGS | awk '$6=="\"POST" && $7 ~ /wp-login\.php|xmlrpc\.php/ {print $1}' \
  | sort | uniq -c | sort -rn | head -20

這是最容易辨識的跡象。正常情況下,wp-login.php 只有管理者登入時才會送出 POST,因此若同一個 IP 出現數百到數千次,幾乎就是暴力破解。xmlrpc.php 如果根本沒在用,理論上應該是 0 筆;只要有數字就很可疑。

統計 3:大量產生 404 的 IP

zcat -f $LOGS | awk '$9==404 {print $1}' | sort | uniq -c | sort -rn | head -20

這代表對方在從頭到尾亂打不存在的路徑,也就是弱點掃描器的行為。若單一 IP 產生了數百筆 404,就可以視為正在被針對性探測。

統計 4:「正在找什麼」的清單

zcat -f $LOGS | awk '$9==404 {print $7}' | sed 's/?.*//' \
  | sort | uniq -c | sort -rn | head -30

被鎖定的目標會直接列出來。像是 /.env/.git/config/wp-config.php.bak/phpinfo.php,以及沒在用的外掛路徑等等。這份清單同時也是「自己網站絕對不應該回傳 200 的路徑清單」

統計 5:其中已經以 2xx 回應的項目(最重要)

zcat -f $LOGS | awk '$9 ~ /^2/ && $7 ~ /\.env|\.git|\.bak|\.old|\.sql|\.zip|wp-config|phpinfo|adminer|phpmyadmin|\.DS_Store/ {print $7}' \
  | sed 's/?.*//' | sort | uniq -c | sort -rn | head -30

這是最優先要確認的統計。只要這裡出現一行,就代表不是「正在被攻擊」,而是「已經被拿走了」。表示備份檔或環境變數檔真的被對外提供了,因此在封鎖之前,必須先刪除那些檔案,並修改其中包含的認證資訊。目標是確認這裡的輸出為空。

統計 6:傳輸量最大的來源

zcat -f $LOGS | awk '$9 ~ /^2/ {b[$1]+=$10} END {for (i in b) printf "%15.0f  %s\n", b[i], i}' \
  | sort -rn | head -20

將回應大小($10)依 IP 累計。即使單筆不明顯,但總量特別突出的來源,可能是在大量抓取內容或外洩檔案(Apache 的 %b 在 0 bytes 時會顯示 -,不過在 awk 加總時會視為 0,因此不影響)。

統計 7:User-Agent 的偏態

zcat -f $LOGS | awk -F'"' '{print $6}' | sort | uniq -c | sort -rn | head -20

以雙引號切分後,第 6 欄就是 User-Agent。如果空白的 - 或某些工具名稱排在前面,就要跟統計 1~3 一起交叉比對。不過 User-Agent 是可以自由偽裝的,不能單獨當成證據

把徵兆變成「每天一次的差分」

人工手動檢查只需要第一次,之後改成每天跑一次,只看跟前一天的差異。

#!/bin/bash
# /usr/local/bin/access-log-digest.sh
set -euo pipefail
LOG=/var/log/nginx/access.log.1        # 因為是在 logrotate 之後執行,所以看前一天的內容
OUT=/var/backups/access-digest
install -d -m 700 "$OUT"
today=$(date +%F)

{
  echo "== 404最多的前10個 IP =="
  awk '$9==404 {print $1}' "$LOG" | sort | uniq -c | sort -rn | head -10
  echo "== 以 2xx 回應的可疑路徑(應為空) =="
  awk '$9 ~ /^2/ && $7 ~ /\.env|\.git|\.bak|\.sql|wp-config|phpinfo/ {print $7}' "$LOG" | sort | uniq -c | sort -rn | head -10
} > "$OUT/$today.txt"

prev=$(ls -1 "$OUT"/*.txt | tail -2 | head -1)
[ "$prev" = "$OUT/$today.txt" ] || diff -u "$prev" "$OUT/$today.txt" || true
# 每天早上 6:05 以電子郵件接收前一天摘要的差分
5 6 * * * /usr/local/bin/access-log-digest.sh | mail -s "access log digest $(hostname)" [email protected]

不可能長期每天逐行閱讀整份日誌。重點是只看差分,只有在有新增內容時才打開原始日誌。這才是能持續下去的方法。

把找到的對象交給 fail2ban 封鎖

若在統計 2、3 中找到常客,就可以交給自動封鎖。fail2ban 的 jail.conf 預設值是 maxretry = 5 / findtime = 10m / bantime = 10m(1.1.0),也就是「10 分鐘內 5 次就封鎖 10 分鐘」。

要注意的是,內建的 nginx 類 jail(nginx-http-auth / nginx-limit-req / nginx-botsearch)都是看錯誤日誌。但 wp-login.php 的 POST 只會出現在存取日誌,所以需要自己做一個 filter。

# /etc/fail2ban/filter.d/wordpress-auth.conf
[Definition]
failregex = ^<HOST> .* "POST [^"]*/(wp-login\.php|xmlrpc\.php)
ignoreregex =
# /etc/fail2ban/jail.local
[wordpress-auth]
enabled  = true
filter   = wordpress-auth
port     = http,https
logpath  = /var/log/nginx/access.log
# 放寬到不至於誤傷正常的登入失誤
maxretry = 10
findtime = 10m
bantime  = 1h
# 一定要先把公司內網與監控服務的 IP 加進去
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24

# 對反覆被封鎖的慣犯做長期封鎖(預設 bantime 1w / findtime 1d)
[recidive]
enabled = true

套用前,一定要先用真實日誌確認過濾條件是否命中

# 確認手邊的日誌會命中幾筆(不會執行封鎖)
fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress-auth.conf

fail2ban-client reload
fail2ban-client status wordpress-auth        # 目前的封鎖清單
fail2ban-client set wordpress-auth unbanip 203.0.113.10   # 解除誤封

為了避免誤封,有 3 個重點:

  1. 先把公司 IP、監控服務、金流/預約系統的 Webhook 來源加入 ignoreip 最常見的事故就是把自己擋掉。
  2. 在反向代理架構下,務必先完成步驟 0。 沒有還原真實 IP 的話,就會把 CDN 的 IP 封掉,結果所有訪客都被擋。
  3. 不要一開始就把 maxretry 設太嚴。 先用 bantime = 10m 觀察幾天,再視情況延長。

另外,封鎖不能取代修補。 BAN 只能阻止重複嘗試,弱點本身仍然存在。

日誌沒留下來的陷阱

  • 保留期間:入侵往往是幾週後才發現,但很多環境只保留日更或十幾代的日誌。請檢查 /etc/logrotate.d/nginxrotate 設定,搭配 compress 之後可延長到大約 90 天。
  • 緩衝:如果你設定成 access_log ... combined buffer=64k flush=5m;,那麼最近幾行不會立刻寫出來。調查期間要留意 flush 的影響。
  • 雜訊:如果健康檢查一天就佔掉數萬行,請把那部分排除,統計會更容易閱讀。
# nginx:不要把監控的健康檢查寫進日誌
location = /healthz {
    access_log off;
    return 200;
}
# Apache:條件式記錄(用 env= 排除)
SetEnvIf Request_URI "^/healthz$" dontlog
CustomLog ${APACHE_LOG_DIR}/access.log combined_realip env=!dontlog

總結

  • 一開始先確認日誌中的客戶端 IP 是否真實(realip / mod_remoteip)
  • 統計 5(以 2xx 回應的可疑路徑)最為關鍵。只要出現,就要先刪檔與更換認證資訊,再談封鎖
  • 以每日摘要 + 差分的方式運作,只有在變化時才打開原始日誌
  • 常客交給 fail2ban。先填好 ignoreip,再用 fail2ban-regex 確認命中後才啟用

日誌不只是在回答「有沒有被攻擊」,還能回答設定漏洞是否真的被踩中。先從跑一次統計 5、確認輸出是否為空開始吧。

相關文章

像本文這類設定上的缺口,可以透過能以 9 道防線整體守護網站的 SiteDock 一次性整合對策 → https://sitedock.jp


原文出處:https://qiita.com/jiis-sasaki/items/7affa2e2ef22ebf93fa6


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝30   💬1  
442
🥈
我愛JS
3
🥉
NewsData
2
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登