Nginx 日志攻击溯源实战:Access Log 分析

Nginx 访问日志分析是攻击溯源的第一手证据:站点被扫描、被 CC、被上传 WebShell 后,日志能还原攻击者的 IP、时间线、请求路径与工具指纹。本文给出可复用的完整流程——自定义可溯源的 log_format、TOP IP 与状态码统计、攻击特征识别、单 IP 行为时间线还原、封禁落地与自动化日报脚本,并附验证方法与常见问题排查。

适用场景

  • 站点出现异常流量或 502/504 抖动,需要快速定位来源 IP 与攻击手法。
  • 安全事件响应阶段,需要按时间线还原某 IP 的全部访问行为。
  • 需要为封禁动作(防火墙、WAF 黑名单、Fail2ban)提供证据与名单。
  • 日志保留与合规审计:需要证明某段时间内的访问记录完整可信。

前置条件

  • Nginx 1.18 及以上,具备 root 或可 reload 配置的运维账号。
  • 日志目录为默认的 /var/log/nginx/,或已知自定义路径。
  • 可选工具:awkipsetiotopgoaccessfail2ban
  • 站点已接入 CDN 或反向代理时,需先启用 realip 模块,否则日志中的 $remote_addr 只是节点 IP。

原理说明

Nginx 的 access_log请求结束阶段写入一条记录,日志格式由 log_format 指令定义,默认的 combined 格式包含客户端地址、时间、请求行、状态码、响应字节数、来源页与 User-Agent。

溯源的价值来自字段的交叉验证:$status 区分正常访问与探测(大量 404 通常是目录扫描),$request 暴露攻击 payload,$http_user_agent 暴露扫描器与脚本工具指纹,$request_time 配合 $upstream_response_time 可以判断是慢速攻击还是后端阻塞。需要特别注意两点前提:其一,$http_user_agent$http_x_forwarded_for 都是客户端可伪造的字段,只能作为辅助证据,IP 维度才是主证据;其二,日志文件在同一主机上可被高权限攻击者删除或改写,因此取证场景必须做异地副本与追加锁定。

操作步骤

步骤 1:定义可溯源的日志格式

/etc/nginx/nginx.confhttp 块内定义格式并应用到站点:

http {
    log_format forensic '$remote_addr - $remote_user [$time_local] "$request" '
                        '$status $body_bytes_sent "$http_referer" '
                        '"$http_user_agent" rt=$request_time '
                        'urt=$upstream_response_time xff="$http_x_forwarded_for"';

    access_log /var/log/nginx/forensic.log forensic buffer=64k flush=5s;
}

关键参数说明:rt 记录请求总耗时,urt 记录上游响应耗时,两者差值大说明慢速客户端;xff 保留原始转发链,便于核对 CDN 前的真实客户端。加 bufferflush 后减少磁盘写入频率,但会带来最多 5 秒的落盘延迟,应急排查时可临时去掉以换取实时性。

nginx -t && systemctl reload nginx

步骤 2:统计 TOP 来源 IP、状态码与路径

# 访问量 TOP 20 的 IP
awk '{print $1}' /var/log/nginx/forensic.log | sort | uniq -c | sort -rn | head -20

# 状态码分布
awk '{print $9}' /var/log/nginx/forensic.log | sort | uniq -c | sort -rn

# 404 命中最多的路径(目录扫描特征)
awk '$9==404 {print $7}' /var/log/nginx/forensic.log | sort | uniq -c | sort -rn | head -20

# 单 IP 的请求量峰值(按分钟)
awk '/203.0.113.10/ {split($4,t,":"); print t[2]":"t[3]}' /var/log/nginx/forensic.log \
  | sort | uniq -c | sort -rn | head

字段位置与默认 combined 格式一致:$1 客户端 IP、$7 请求路径、$9 状态码。若日志格式被改动(例如把 $request 放到了其他位置),先执行 head -1 /var/log/nginx/forensic.log 核对字段顺序。

步骤 3:识别攻击特征

按行过滤常见攻击 payload,把命中记录聚合到来源 IP:

grep -Ei "union.*select|sleep\(|benchmark\(|information_schema" /var/log/nginx/forensic.log \
  | awk '{print $1}' | sort | uniq -c | sort -rn | head

grep -E "\.\./|%2e%2e|/etc/passwd|/proc/self" /var/log/nginx/forensic.log \
  | awk '{print $1}' | sort | uniq -c | sort -rn | head

grep -Ei "wp-login|xmlrpc|\.php\?|shell|eval\(|base64_decode" /var/log/nginx/forensic.log \
  | awk '{print $1}' | sort | uniq -c | sort -rn | head

# 扫描器与脚本工具指纹
awk -F'"' '{print $6}' /var/log/nginx/forensic.log | sort | uniq -c | sort -rn | head -30

典型特征对应关系:大量 404 且 UA 为空或为 python-requestscurl 属于自动化扫描;同一 IP 在 1 分钟内请求数百个动态页面且状态码全为 200,UA 高度一致,属于 CC 或爬虫抓取;POST 到非入口 PHP 文件且状态码 200、字节数异常偏大,需立即核对是否已被上传 WebShell

步骤 4:还原单个 IP 的完整行为时间线

IP=203.0.113.10
grep -F "$IP" /var/log/nginx/forensic.log \
  | awk '{print $4,$5,$6,$7,$9,$11,$NF}' | head -50

时间线能直接回答三个问题:首次出现时间(攻击起点)、攻击手法顺序(先探测后利用)、是否命中成功(状态码 200 且响应体较大)。如站点同时保留 error_log,用请求时间对齐可确认后端是否抛出过异常:

grep -F "$IP" /var/log/nginx/error.log | tail -20

步骤 5:封禁落地

临时封禁用 ipset + iptables,封禁动作常数级生效,适合黑名单数量上百的场景:

ipset create wafban hash:ip timeout 86400 -exist
ipset add wafban 203.0.113.10 -exist
iptables -I INPUT -m set --match-set wafban src -j DROP
ipset list wafban | tail -20

需要在 Nginx 层返回 403(保留访问日志便于观察)时,用 map 指令实现:

# /etc/nginx/conf.d/blocked_ips.conf
# 203.0.113.10 1;

map $remote_addr $blocked_ip {
    default 0;
    include /etc/nginx/conf.d/blocked_ips.conf;
}

server {
    if ($blocked_ip) { return 403; }
}
nginx -t && systemctl reload nginx

需要按 UA、请求频率自动封禁时,交给 Fail2ban 处理:/etc/fail2ban/jail.d/nginx-scan.local 中把 logpath 指向 forensic.log,并自定义 filter 匹配 404 与攻击 payload,可避免人工维护名单。

步骤 6:自动化日报脚本

#!/bin/bash
# /usr/local/bin/nginx_report.sh
LOG=/var/log/nginx/forensic.log
OUT=/var/log/nginx/report-$(date +%F).txt
{
  echo "=== $(date '+%F %T') TOP IP ==="
  awk '{print $1}' "$LOG" | sort | uniq -c | sort -rn | head -10
  echo "=== 404 TOP ==="
  awk '$9==404 {print $7}' "$LOG" | sort | uniq -c | sort -rn | head -10
  echo "=== 攻击特征命中 IP ==="
  grep -Ei "union.*select|\.\./|
chmod +x /usr/local/bin/nginx_report.sh
echo "0 8 * * * root /usr/local/bin/nginx_report.sh" > /etc/cron.d/nginx-report

配置验证

  • 格式生效curl -s -o /dev/null -H "User-Agent: probe-test" https://example.com/probe-path,随后 grep "probe-test" /var/log/nginx/forensic.log 应能命中,且 rt=xff= 字段齐全。
  • 封禁生效:被封 IP 请求返回 403(map 方案)或直接超时(ipset 方案);iptables -L INPUT -n --line-numbers | grep wafban 可见匹配规则。
  • 统计准确wc -l /var/log/nginx/forensic.logawk '{print $1}' ... | wc -l 行数一致,说明格式没有导致字段错位。
  • 日报可用:手动执行一次脚本,确认输出文件非空且含 TOP IP 段落。

常见问题

FAQ 1:日志里的 IP 全是 CDN 节点地址,怎么拿到真实客户端 IP?

先启用 realip 模块并声明可信来源,再读取右侧 XFF 值:

set_real_ip_from 203.0.113.0/24;   # 替换为 CDN 回源网段
real_ip_header X-Forwarded-For;
real_ip_recursive on;

配置后 $remote_addr 即为真实客户端 IP。不要把公网任意网段写进 set_real_ip_from,否则攻击者自带的 XFF 头可直接伪造来源,日志与限流都会被绕过。

FAQ 2:攻击者删除了 access.log,还能溯源吗?

只能依赖异地副本。建议三件事同时做:其一,配置远程 syslog 或日志采集把 forensic.log 实时推到独立日志主机;其二,对本机日志文件加追加属性 chattr +a /var/log/nginx/forensic.log,高权限进程也无法直接截断;其三,日志轮转使用 rename 而非删除,被删前的副本仍可恢复。已删除且无副本时,可尝试 debugfsextundelete,但成功率取决于磁盘写入量。

FAQ 3:某个 IP 请求量不大,但站点被拖慢,怎么定位?

优先级看耗时而非次数。按 rt= 降序取慢请求:awk -F'rt=' '{split($2,a," "); print a[1],$0}' forensic.log | sort -rn | head -20。若 urt 接近 rt,说明瓶颈在后端应用或数据库;若 urt 为空且 rt 很大,属于典型的慢速客户端占用连接,应结合 limit_conn 与超时配置处理。

FAQ 4:日志文件增长过快,如何控制体积?

使用 logrotate 做按天轮转与压缩:/etc/logrotate.d/nginx 中设置 dailyrotate 30compressdelaycompress,并保留 postrotate 中的 kill -USR1 $(cat /run/nginx.pid) 让 Nginx 切换句柄。取证需要的周期可按合规要求延长到 90 天以上,但建议把历史日志转移到独立存储,避免占满系统盘。

总结

Nginx 访问日志分析的核心是「字段完整 + 维度交叉 + 结论可落地」。先用自定义 log_format 补齐耗时与转发链字段,再用 awk 完成 IP、状态码、路径三个维度的统计,接着按 payload 特征与 UA 指纹识别攻击手法,最后通过 ipset、map 或 Fail2ban 完成封禁闭环。任何一次溯源都应同时保留日志副本与操作记录,确保结论在事后复核时依然成立。