Nginx 的访问日志是服务器安全状况的晴雨表,而高频出现的 404 状态码往往是扫描器留下的最明显痕迹。正常的访客偶尔会因为输入错误或链接失效遇到 404,但扫描器不同,它会在短时间内针对大量不存在的路径发起请求,试图探测 PHP 探针、后台登录口、配置文件备份或已知漏洞端点。直接分析日志中的 404 模式,就能在不依赖第三方安全软件的情况下,快速识别出绝大多数自动化扫描行为。

明确扫描器 404 与正常 404 的本质区别

处理日志之前,必须清楚两者的差异。正常用户产生的 404 通常集中在少数几个被错误引用的旧链接上,请求间隔时间随机,User-Agent 多为常规浏览器,且 IP 地址往往伴随大量 200 状态码的成功访问记录。扫描器产生的 404 则完全相反:请求路径高度离散,短时间内会出现几十甚至上百个不同的无效路径;请求间隔极短且规律;User-Agent 常伪装成搜索引擎爬虫或使用脚本语言的默认标识;最关键的一点是,该 IP 几乎不会产生任何 200 请求,404 占比极高。理解了这个底层逻辑,筛选规则就有了明确的制定方向。

基础筛选:用命令行工具快速找出嫌疑 IP

不需要复杂的 ELK 堆栈,单台服务器上使用原生的 awk、sort 和 uniq 组合就能完成高效分析。假设 Nginx 日志格式为默认的 combined 格式,可以先统计每个 IP 产生的 404 请求总数,并按降序排列。命令如下:

awk '$9 == 404 {print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

这条命令会列出产生 404 最多的前 20 个 IP 地址。但仅凭总数判断不够精确,一个正常用户可能因为网站改版遗留问题反复请求某个固定旧地址,产生上百次 404。此时需要引入第二个维度:请求路径的唯一性。扫描器的特征是路径变化多端,因此需要统计每个 IP 请求的不同 404 路径数量。可以使用以下命令:

awk '$9 == 404 {print $1, $7}' /var/log/nginx/access.log | sort -u | awk '{print $1}' | uniq -c | sort -rn | head -20

这条命令先提取出 IP 和请求路径,去重后再统计每个 IP 出现的次数,这个数值就代表了该 IP 触发的唯一 404 路径数量。如果一个 IP 在短时间内触发了超过 50 个不同的 404 路径,基本可以断定是扫描器。

深度分析:针对特定扫描特征的路径模式匹配

扫描器通常会携带明显的路径指纹。直接在日志中检索这些高频敏感路径,可以反向定位扫描源。常见的扫描目标包括:各类后台管理入口,如 /admin、/wp-admin、/manager、/druid/index.html;配置文件泄露探测,如 /.env、/.git/config、/WEB-INF/web.xml、/config/database.yml;PHP 探针和调试文件,如 /info.php、/phpinfo.php、/_profiler/phpinfo;以及各种备份文件后缀,如 .bak、.swp、.save、~ 结尾的请求。

可以使用 grep 配合正则表达式在日志中批量匹配这些模式。例如,查找所有试图访问 .env 文件或 git 目录的请求:

grep -E '(\.env|\.git/|\.svn/|\.DS_Store|wp-config\.php|\.bak$|\.swp$)' /var/log/nginx/access.log | awk '$9 == 404 {print $1, $7}' | sort

通过观察匹配结果,如果某个 IP 连续尝试了 .env、.git/config 和 .DS_Store 等路径,且均返回 404,这就不是误报,而是典型的自动化环境探测行为。这类 IP 应当立即封禁。

时间窗口分析:捕捉短时高频异常

扫描器为了追求效率,往往在极短的时间窗口内发出大量请求。统计每分钟或每秒钟的 404 请求频率,能有效区分慢速爬虫和暴力扫描。可以先确定日志的时间格式,然后截取分钟级别的时间段进行聚合。假设日志时间格式为 02/Oct/2023:13:55:36,可以使用以下命令统计每分钟的 404 数量:

awk '$9 == 404 {print substr($4, 2, 17)}' /var/log/nginx/access.log | cut -d: -f1,2 | sort | uniq -c | sort -rn | head -20

如果某一分钟内 404 请求超过 100 次,就需要提取该时间段内的所有请求进行人工复核。更精细的做法是结合 IP 和时间两个维度,找出在 1 分钟内请求超过 30 个不同 404 路径的 IP:

awk '$9 == 404 {print $1, substr($4, 2, 17)}' /var/log/nginx/access.log | sort | uniq -c | awk '$1 > 30 {print $2}' | sort | uniq

这种时间窗口分析对于发现分布式扫描也有效果,即使攻击者使用了大量代理 IP,只要单个 IP 在窗口内的行为异常,依然能被捕获。

User-Agent 与请求指纹的交叉验证

扫描器为了躲避基础的安全策略,会伪造 User-Agent,但伪造行为本身也会留下破绽。正常的浏览器请求会伴随对 CSS、JS、图片等静态资源的加载,而扫描器通常只请求 HTML 或动态页面,几乎不请求静态资源。通过分析单个 IP 的请求类型分布,可以建立更精准的判断模型。如果一个 IP 发起了 100 次请求,其中 95 次是 .php 或 .asp 等动态路径,且全部返回 404,而没有任何 .js、.css、.png 请求,这几乎可以肯定是扫描器。

同时,注意那些声称自己是 Googlebot 或 Baiduspider 的 User-Agent。可以通过反查 IP 的 DNS 和归属来验证其真实性。虽然文章不涉及具体搜索引擎的反查细节,但原理是通用的:真实的搜索引擎爬虫 IP 会有对应的反向解析记录,且请求行为规律,不会去扫描 /admin.php 或 /.env 这类路径。在日志中筛选出 User-Agent 包含 bot 字样但请求路径明显是扫描特征的记录,往往能发现伪装者。

构建自动化脚本实现实时封禁

手动分析只能用于事后追溯,建立自动化处置机制才能实时阻断扫描。可以编写一个简单的 shell 脚本,配合定时任务,每分钟分析一次日志,对命中规则的 IP 自动加入 iptables 或 nginx 的拒绝列表。脚本的核心逻辑如下:

#!/bin/bash
LOG_FILE="/var/log/nginx/access.log"
BLOCK_LIST="/etc/nginx/block_ips.conf"
# 定义扫描特征阈值
THRESHOLD=50
# 统计过去1分钟内404路径唯一数超过阈值的IP
tail -10000 $LOG_FILE | awk -v date="$(date -d '1 minute ago' '+%d/%b/%Y:%H:%M')" '$0 ~ date && $9 == 404 {print $1, $7}' | sort -u | awk '{print $1}' | uniq -c | sort -rn | while read count ip; do
    if [ $count -gt $THRESHOLD ]; then
        # 检查是否已经被封禁
        if ! grep -q $ip $BLOCK_LIST; then
            echo "deny $ip;" >> $BLOCK_LIST
            # 重载nginx以应用新规则
            nginx -s reload
            echo "$(date) Blocked $ip for 404 scan, unique paths: $count" >> /var/log/scan_block.log
        fi
    fi
done

这个脚本仅作为示例,实际部署时需要根据服务器的日志切割策略和 Nginx 配置结构调整。更稳健的做法是使用 fail2ban 这类成熟工具,配置自定义的 filter 规则来匹配扫描器特征。在 /etc/fail2ban/filter.d/nginx-404-scan.conf 中定义规则,匹配短时间内产生大量 404 的 IP,然后通过 action 自动调用防火墙封禁。

长期运营中的误报控制与白名单机制

任何自动化封禁策略都必须考虑误报问题。某些合法的 SEO 工具、监控服务或内部系统可能会产生大量 404 请求。例如,内部健康检查探针可能每隔几秒就请求一个固定不存在的路径来验证服务可用性。因此,在封禁逻辑中必须加入白名单机制。白名单应包含:内部办公网络出口 IP、已知的监控服务 IP、合作伙伴的服务器 IP 等。

另外,可以通过分析请求的 Referer 字段来降低误报。如果一个 IP 产生大量 404,但 Referer 均来自同一个合法域名,说明可能是该网站存在大量失效外链,而非扫描行为。正常的扫描器请求通常没有 Referer,或者 Referer 为空或伪造。在筛选条件中加入对 Referer 的考量,能进一步提升判断的准确度。

利用 Nginx 自身配置进行前置拦截

除了事后分析,Nginx 本身也可以配置一些前置规则来过滤明显的扫描请求。例如,可以直接在 server 块中对已知的扫描路径返回 444 状态码(Nginx 特有的无响应关闭连接),让扫描器无法获取有效信息,同时减少日志噪音。配置示例如下:

location ~* (\.env|\.git|\.svn|\.DS_Store|wp-config\.php|\.bak$|\.swp$|\.save$) {
    return 444;
}
location ~* (/admin|/wp-admin|/manager|/druid) {
    # 仅允许内部IP访问
    allow 192.168.1.0/24;
    deny all;
}

这种前置拦截不仅能保护服务器,还能从源头上减少无效 404 请求对后端应用的压力。但需要注意,这种配置可能会影响正常用户,比如某个开源项目的后台路径恰好是 /admin,且需要对外提供服务。因此,这类规则必须根据自身业务实际情况进行调整。

日志数据的可视化与趋势监控

如果服务器数量较多或日志量巨大,纯命令行分析会变得吃力。此时可以将 Nginx 日志接入时序数据库或日志平台进行可视化监控。通过构建 404 请求的时序图表,可以直观地看到扫描活动的高峰时段。通常,扫描器会在凌晨或节假日等非工作时间加大扫描力度,这些趋势通过图表一目了然。设置告警规则,当每分钟 404 请求数或唯一路径数超过历史平均值的 3 倍时触发通知,能让运维人员在扫描活动升级前就介入处理。

分析 404 日志不仅是安全运维的日常工作,更是一种持续优化的过程。每次发现新的扫描路径特征,都应该更新匹配规则和封禁策略。将这些规则沉淀为自动化脚本或平台配置,服务器的抗扫描能力就会像滚雪球一样越来越强。真正成熟的运维体系,不是等扫描器打上门了再手动分析,而是让扫描器在发起探测的第一分钟就被识别并阻断,连完整的资产信息都拿不到。