网站被恶意扫描怎么办?Nginx 拦截规则实战 + 4 种防护方案对比
本站部分链接为联盟推广链接。通过这些链接购买,我可能获得佣金...
这不仅不会增加您的实际费用,通常还能让您享受到专属折扣。所有上榜推荐均基于真实项目环境下的自费实测,排名完全由客观测评数据决定。详细规则请查阅《免责声明》。
在我翻看宝塔面的网站日志时,我看到一屏整齐的 404:同一秒里七八个不同的 IP,请求的全是 /pricing/virtual-servers、/cloud/repatriation 这种路径。我的博客压根没有这些页面,这种访问持续了很久。
这不是登录爆破。如果你日志里刷的是 wp-login.php,那是另一类问题,可以看登录页防暴力破解的插件配置。今天说的是另一种:机器人拿着一份”服务器厂商网站目录”在你站上盲猜。
先看日志:4 秒,12 条,全是 404
下面这张是我宝塔面板里截的访问日志,/www/wwwlogs 目录当时占了 623.92 MB。我把被盲猜的路径用红框标出来了。
我数了一下,就图片这 12 条里有 7 个不同的 IP,时间窗口只有 13:28:05 到 13:28:08,平均每秒约 3 条。这只是截图里一小截,不代表全天流量,但足够说明问题。
路径很有意思:/pricing/virtual-servers、/bare-metal-dedicated-servers/chicago、/locations、/colocation……看着像某家 IDC 官网的栏目结构。我的推测是,爬虫按”服务器、VPS”这类关键词挑中了我的博客,然后拿现成的字典去试。这只是推测,我没法证明它的真实意图。
更让我在意的是响应体:每条 404 都返回了约 48.5KB。静态 404 页不会有这么大,更像整个主题模板的 404 页被 PHP 渲染了一遍。如果你也像我一样用了让 404 交给主题模板显示的 Nginx 写法,每条盲猜都会吃一次 PHP 和数据库。这一点我没抓过你的 404 页面大小,你可以对着一个不存在的网址 curl 一下,对比自己的数字。
有人会问:只是 404,不会真被入侵,何必管?我的想法是,被盯上的站点值得多留个心眼。站内有一篇默认后台被爆破后整站重建的真实案例,客户当时也是从”看着无害的扫描”一路走到”首页弹赌博广告”。盲猜路径本身不致命,但它既浪费资源,又会把日志撑得很大。
日志里的 “Googlebot” 是真的吗
这 12 条里,有 6 条 UA 自称 Googlebot,而且结尾是 Chrome/W.X.Y.Z。
Google 官方爬虫文档里,W.X.Y.Z 是占位符,代表该 UA 使用的 Chrome 版本号,文档举的例子是 41.0.2272.96,并说明这个数字会随时间增长。真实的 Googlebot 发出请求时,这里是具体版本号,不会原样写字母。所以日志里出现字面的 W.X.Y.Z,基本可以判断是照抄文档字符串的脚本。这是我的推断,Google 文档没有直接写”出现字母就是假的”。
光看 UA 不够,Google 给的验证办法是反向 DNS:
- 对日志里的 IP 做反向 DNS 查询。
- 确认返回的主机名以
googlebot.com、google.com或googleusercontent.com结尾。 - 再对这个主机名做正向查询,确认能解析回原 IP。
我对这 4 个自称 Googlebot 的 IP 做了第 1 步:
213.230.86.42反查出的主机名是42.64.uzpak.uz,不是 Google 的域名。14.152.91.249、49.71.19.190、117.82.70.170三个 IP 没有返回任何反向记录。
四个都过不了第 2 步,所以不是真 Googlebot。有两点要说明:这是我从沙箱里查的,不同网络环境和不同时间的结果可能有差异;Google 还公布了 IP 段 JSON(common-crawlers.json)可以做”自动匹配”,这次我这边下载被拦了,没做这一步比对。
另外 213.230.86.42 在 12 条里出现了 3 次,一次用普通 Linux Chrome 的 UA,两次用 Googlebot 的 UA。同一个 IP 来回换 UA,这本身就是个强信号。
我的拦截思路:三段规则,各管一件事
下面这张是我站点配置里新增的三段规则。在宝塔里打开站点设置,左侧找到「配置」就能看到这个编辑器。
这三段分别管:
- 压缩包和备份文件。 扫描器爱试
/backup.zip、/db.sql这类文件。 - 伪造 UA。 精确匹配
Chrome/W.X.Y.Z这个假字符串。 - IDC 产品路径。 把日志里反复出现的盲猜路径做成一份名单。
三段的动作都是 return 444。Nginx 文档对这个非标准状态码的说明是:关闭连接,不发送响应头。也就是说请求不会走到 PHP,更不会碰数据库。
有一点要说清楚:444 省的是 PHP、数据库和响应体的流量,连接本身还是要先接受才能关。
这套规则里有两个坑,上线前先改
坑一:路径名单会误伤你自己的文章
我的名单是这样写的:
location ~* ^/(pricing|virtual-servers|…|cloud|tco|faq) { return 444; }
注意括号后面没有结尾限定。正则 ^/(…|cloud|…) 只要求网址以 /cloud 开头就算命中,后面接什么都行。
我拿站点 sitemap 里的所有网址,用同一份名单跑了一遍。结果有 3 篇文章会被这条规则拦下,全是 cloudflare- 开头的教程:域名转出、Turnstile 验证码、域名注册。原因是名单里有一个 cloud,而正则只要求网址以它开头,cloudflare 当然也满足。这三篇都是我自己的内容,被自己的规则挡在了门外。
修正很简单,在括号后面加 (/|$),要求匹配到完整的路径段:
^/(pricing|…|cloud|tco|faq)(/|$)
这样 /cloud、/cloud/repatriation 会被拦,/cloudflare-…html 不会。
还有一个提醒:名单里的 products、company、faq 都是常见页面名。哪天你真的做了一个 /faq 页面,记得把它从名单里删掉,否则访客打开是断连,而不是页面。
坑二:压缩包规则和执行顺序
压缩包那条 \.(zip|rar|gz|7z|sql|bak|tar|tar\.gz)$ 会把站内所有 .zip、.gz 都挡住。如果你在站里放了主题、插件资源的 zip 下载,它们会打不开。有这类下载的站,要么给下载目录单独加例外,要么别用这条。
另外,Nginx 处理正则 location 的规则是:先看前缀 location 里最具体的,再按配置文件里出现的顺序检查正则,第一个命中就停止。这是官方文档里写的。
所以你写在哪一行很重要。宝塔默认会有一段 PHP 的 location。如果它排在你的 cart\.php 规则之前,/cart.php 会先被它接走。具体谁在前面,我没法替你确认:在编辑器里按 Ctrl+F 搜 enable-php 看行号,再和你的规则行号比一比。不确定,要自己核。
修正版完整配置
把下面这段放进站点「配置」,位置按上面说的,放在 PHP 那段之前:
# 1. 压缩包与备份文件(有 zip 下载的站,请先看上文"坑二")
location ~* \.(zip|rar|gz|7z|sql|bak|tar)$ {
return 444;
}
# 2. 照抄文档的假 Googlebot(点号已转义)
if ($http_user_agent ~* "Chrome/W\.X\.Y\.Z") {
return 444;
}
# 3. 盲猜路径:加了 (/|$),并补上日志里出现、原名单没有的 case-studies
location ~* ^/(pricing|virtual-servers|bare-metal-dedicated-servers|network|solana-servers|knowledge|case-studies|cart\.php|cloud-repatriation|locations|colocation|dedicated|company|products|cloud|tco|faq)(/|$) {
return 444;
}
说明三处改动:
- 第一条去掉了多余的
tar\.gz,gz已经覆盖它。 - 第二条把点号转义成
\.,原写法里的点会匹配任意字符,虽然不出错,但不精确。 - 第三条是前面说的结尾限定,同时补了
case-studies,因为日志里出现了/case-studies/stakecraft,而名单里没有。
怎么验证没有误伤
改完别急着走,拿 curl 测三件事(把域名换成你自己的):
# 假 Googlebot UA:应该被断开,拿不到响应头
curl -I -A "Mozilla/5.0 Chrome/W.X.Y.Z Safari/537.36" https://你的域名/
# 盲猜路径:应该被断开
curl -I https://你的域名/pricing/virtual-servers
# 你自己的文章:必须是正常的 200
curl -I https://你的域名/你的某篇文章.html
444 不发响应头,所以前两条的报错提示会和正常响应完全不同,具体措辞以你机器上显示为准。第三条一定要测,尤其是文章 slug 里带 cloud、network、products 这些词的。
再回宝塔日志看一会儿。被拦的请求状态码会变成 444。如果你看到真实用户访问的页面出现 444,立刻回头检查名单。
其他 3 种方案:什么时候该换层拦
Nginx 规则是在源站拦。拦得早、够便宜,但有个缺点:请求已经打到你的服务器了。要不要换一层,看你的站点情况。
| 方案 | 在哪一层拦 | 要不要进 PHP | 适合谁 |
|---|---|---|---|
| Nginx 规则(本文) | 源站 Web 服务器 | 不进 | 有服务器配置权限、自己维护 |
| Cloudflare WAF / UA 规则 | CDN 边缘 | 不到源站 | 已经接了 Cloudflare 的站 |
| WordPress 安全插件 | PHP 内部 | 要进 | 没有服务器权限的站点 |
| 限速 / 自动封 IP | 源站 | 视写法而定 | 同一 IP 反复请求的场景 |
Cloudflare。 官方的用户代理拦截规则只支持精确匹配,不支持通配符,免费版能建 10 条,而且官方推荐改用 WAF 自定义规则,表达式写成 http.user_agent eq 加上具体值。这个信息来自 Cloudflare 开发者文档的预览站镜像,不是正式域名页面,数字以官网为准。如果想做子串匹配,运算符要换,我这次没核到具体写法。是否值得上 Cloudflare,站内有一篇专门分析的外贸站该不该用 Cloudflare,主要提醒是国内访问可能变慢,先实测再决定。
安全插件。 Wordfence、Limit Login Attempts 这类插件本身跑在 PHP 里,请求已经进来了,才轮到它拦。它们适合管登录爆破,对”盲猜 404″这种流量帮助有限。
限速和自动封 IP。 比如 Nginx 自带的 limit_req 或 fail2ban。我没抓到这两项的官方文档,所以只提思路,不给配置,免得你照抄出错。感兴趣的话请对照官方文档自己写。
选哪种?如果你自己管服务器,Nginx 规则够用,成本最低,这也是我选它的原因。如果你用的是没法改配置的共享空间,可以看看 WordPress 主机推荐里哪些主机自带防护;想完全掌控配置,自己上 VPS 主机推荐里的方案。顺带说一句,这套规则只适用于 Nginx,如果你还在 Apache 上,Nginx 和 Apache 的选择思路里聊过要不要迁移。
日志本身也要管
最后留个小提醒:我那个日志目录已经 623.92 MB 了。扫描流量会把日志撑得很快,哪天磁盘报警,先查日志。清理办法站内有VPS 磁盘占满后释放空间的 6 种方法,其中第一项就是清日志。