网站被目录扫描器狂刷404怎么办?封禁实战

服务器日志里满屏 404,来源高度集中,请求路径全是 /admin.php、/backup.zip、/.env 这种猜出来的名字——这不是搜索引擎在抓页面,而是目录扫描器在踩点。本文用 9 个问答讲清扫描请求的特征、怎么和正常抓取区分、404 洪峰怎么先降速再封禁,以及一套尽量不误伤真实用户的处置配置。

Q:目录扫描器和正常爬虫有什么区别?

核心差别在请求路径的来源:正常爬虫顺着站内链接走,目录扫描器是拿一份字典在拼路径。

  • 路径站内不可达:/admin、/manage、/wp-config.php.bak、/.git/config、/backup.zip、/phpinfo.php 这些名字,站内没有任何链接指向。
  • 404 占比极高:正常抓取 404 通常低于 5%,扫描器经常超过 50%。
  • 节奏均匀:单 IP 每秒固定几个请求,且不加载 css、js、图片。
  • UA 有特征:dirsearch、Nikto、Go-http-client、python-requests 等默认 UA,或伪装成 Chrome 却缺少 Sec-Fetch 系列头。

一句话:正常爬虫要内容,扫描器要入口。前者关心页面里的正文,后者关心服务器上存不存在某个文件。

Q:怎么确认是扫描而不是搜索引擎抓取?

三个信号命中两个就能定性,全部用访问日志算:

# 404 的来源 IP Top 20
grep ' 404 ' access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20

# 单 IP 的请求量与状态码分布
awk '{print $1, $9}' access.log | sort | uniq -c | sort -rn | head -20
  • 是搜索引擎:请求路径都能在站内找到,会拉取静态资源,会读 robots.txt,段内 IP 分散。
  • 是扫描器:路径大多数不存在,只请求 HTML 不拉资源,单 IP 404 超过 100 次/小时。

有个前提容易踩坑:先确认日志里的 IP 是真实客户端 IP。挂了 CDN 或 WAF 时,日志默认记录的是回源节点地址,不还原真实 IP 时封禁会直接封掉整个 CDN 节点:

set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

Q:404 洪峰有什么实际危害?

很多人觉得 404 只是日志脏,其实代价比想象中高:

  • 真实资源消耗:WordPress 这类动态站点每个 404 也要启动 PHP、查数据库,十万次请求就是十万次无用查询。
  • 磁盘与带宽:日志按天膨胀,回源流量被白白吃掉。
  • 缓存命中率被拉低:大量不存在的路径都是 cache miss,还会污染缓存。
  • 漏洞前置侦察:一旦猜到真实存在的备份文件或后台入口,下一步就是拿工具打。

所以 404 洪峰不是”噪音问题”,而是成本问题加攻击前置,必须按安全事件处理。

Q:怎么先降速止血?

第一步不是封 IP,而是限速,把洪峰摊平,让真实用户先不受影响:

# 单 IP 请求速率 + 并发连接双限
limit_req_zone  $binary_remote_addr zone=req_per_ip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;

limit_req_status  429;
limit_conn_status 429;

server {
    limit_req  zone=req_per_ip burst=20 nodelay;
    limit_conn conn_per_ip 20;
}

两个关键点:rate 要按单 IP 正常峰值定,不要用”全站峰值除以 IP 数”,否则正常用户一起被限;静态资源要单独放行,否则用户首屏的图片会被扫描器连坐拖慢。

Q:怎么针对 404 单独做处置?

实用思路是把 404 抽出来单独统计,再用封禁工具闭环。Nginx 默认所有请求混在一个日志里,先让 404 单独落盘:

map $status $is_404 {
    ~^404$ 1;
    default 0;
}

server {
    access_log /var/log/nginx/notfound.log main if=$is_404;
}

剩下的交给 fail2ban,因为它的封禁自带超时,天然避免永久误封:

# /etc/fail2ban/filter.d/nginx-404.conf
[Definition]
failregex = ^<HOST> - .* "GET [^"]*" 404
ignoreregex =

# /etc/fail2ban/jail.d/nginx-404.local
[nginx-404]
enabled  = true
filter   = nginx-404
logpath  = /var/log/nginx/notfound.log
maxretry = 60
findtime = 300
bantime  = 3600

参数含义:5 分钟内 404 超过 60 次就封 1 小时。阈值不能设太松,也不要设太紧,正常用户刷新几次页面不会触发。

Q:UA 黑名单能挡住扫描器吗?

能挡一部分,挡不住改变 UA 的那部分。低端扫描工具大多懒得改 UA,先做一层粗筛成本最低:

map $http_user_agent $bad_ua {
    default 0;
    ~*(dirsearch|nikto|nmap|masscan|sqlmap|wpscan|acunetix|nuclei) 1;
    ~*(python-requests|Go-http-client|curl/|Wget/|Java/) 1;
}

server {
    if ($bad_ua) {
        return 403;
    }
}

注意两个坑:一是 UA 判断必须用 map 出变量再 return,不要写复杂的 if 嵌套,Nginx 的 if 是 rewrite 模块的一部分,嵌套容易出不可预期行为;二是这条规则会误伤正常的接口调用和监控探针,上线前把自家脚本、CDN 回源、可用性监测的 UA 加进白名单。

结论很清楚:UA 只能做第一层粗筛,扫描器把 UA 改成 Chrome 就绕过了,真正管用的是行为层加封禁闭环。

Q:主动封禁怎么做得又准又可回退?

用 ipset 加超时,封禁自动过期,不用手工维护名单:

# 建集合,默认 1 小时自动解封
ipset create dirscan hash:ip timeout 3600

# 挂到 INPUT 链(按需改成 FORWARD 或只封 80/443)
iptables -I INPUT -p tcp -m multiport --dports 80,443 -m set --match-set dirscan src -j DROP

# 手动加封(扫描器常整段换 IP,优先封 /24)
ipset add dirscan 203.0.113.0/24 timeout 7200

# 查看与解封
ipset list dirscan | head
ipset del dirscan 203.0.113.0/24

三条经验:封禁一定带 timeout;优先按 /24 或 ASN 聚合封,不要一个 IP 一个 IP 追;CDN 场景先在 Nginx 还原真实 IP,再把真实 IP 送进 ipset,否则封的是回源节点。

Q:怎么防止误封正常用户?

用三级阶梯,不要一上来就 DROP:

  • 记录观察:先在日志和看板上标出可疑 IP,不动流量。
  • 降速:返回 429 而不是 403,用户还能用,只是慢一点。
  • 带超时封禁:只有”404 集中 + 不拉静态资源 + 无 Referer”三条同时成立的 IP 才封。

另外把三类来源加进永久白名单:搜索引擎官方 IP 段、CDN 回源段、内部监控与健康检查探针。封禁时长控制在 1 小时以内并自动解封,是防误伤最有效的保险。

Q:还有什么补充建议?

  • 先消灭可猜的目标:备份文件不要留在 Web 目录,.git、.env、phpinfo、数据库导出文件一律禁止访问,”扫不到”比”扫到再拦”更彻底。
  • 404 页面统一:别让 404 页面暴露框架版本、报错堆栈、后台路径。
  • 定期自己扫自己:拿扫描器跑一遍自己的站,先于攻击者发现问题。
  • 拦在最外层:CDN 或 WAF 层能拦掉的扫描,就不要让它到源站消耗 PHP 和数据库。
  • 看趋势而不是看单点:把每日 404 总量、Top 来源段画成曲线,扫描开始和结束一眼就能看出来。

一句话总结:目录扫描器要的是入口不是内容;用”限速止血、404 单独统计、带超时封禁、白名单兜底”这套闭环,既能压掉洪峰,又不会把真实用户和搜索引擎一起误伤。