日志先看状态码
今天中午翻了一遍站点日志,从凌晨 3:30 到 12:35 一共 1556 条请求,状态码分布是这样:
awk '{print $9}' /www/wwwlogs/littlesheep.cc.log | sort | uniq -c | sort -rn
1132 200
252 304
58 404
51 302
23 301
19 403
10 201
5 400
4 207
2 500
200 和 304 是正常流量,304 是浏览器带着缓存回来问「这个还是最新的吧」,不用管。要盯的是后面几项:58 个 404,4 个 207,2 个 500。207 是 WordPress REST 批量接口的状态码,正常浏览网页根本不会用到它。
404 的路径就是扫描器的指纹
把 404 的 URL 排一下:
awk '$9==404 {print $7}' /www/wwwlogs/littlesheep.cc.log | sort | uniq -c | sort -rn | head -12
输出截断了一部分,长这样:
1 /wp-includes/txets.php
1 /wp-includes/style.php
1 /wp-includes/hp2.php
1 /wp-editor.php
1 /wp-content/wp-setting.php
1 /wp-content/themes/txets.php
1 /wp-content/style.php
1 /wp-content/plugins/fix/000.php
1 /wp-content/ioxi-o.php
1 /wp-content/goods.php
清一色 .php,文件名短、随机,专挑 WordPress 的主题和插件目录钻,往下还有 /bless.php、/lufix.php、/dex.php、/abcd.php、/php8.php。这不是访客点错了链接,是有人在找一个能直接执行的 webshell 落点。58 个 404 里 31 个是 .php,剩下的多是 /wordpress、/blog、/oldsite、/backup、/demo 这类建站扫描器在碰运气,看有没有忘删的备份目录。
同一台机器干了 41 次,163.181.92.164,它的时间线更能说明问题:
/wp-includes/style.php 404
/bless.php 404
/wp-content/plugins/fix/up.php 404
/wp-admin/index.php 302
/user-sign?tab=signin&redirect_to=…%2Fwp-admin%2Findex.php 200
/?c_c 200
先在目录里摸一遍后门文件名,再顺着 302 去登录页,最后还试了 ?c_c 这种缓存插件的参数。它的 UA 是 Go-http-client/2.0,Go 写的脚本默认就报这个。
翻历史日志会发现这事天天都在发生,不是今天走运:
for f in /www/wwwlogs/littlesheep.cc.log-2026*.gz; do printf "%s " $(basename $f); zgrep -c " 404 " $f; done
littlesheep.cc.log-20261006.gz 284
littlesheep.cc.log-20261007.gz 336
littlesheep.cc.log-20261008.gz 484
littlesheep.cc.log-20261009.gz 263
littlesheep.cc.log-20261010.gz 517
littlesheep.cc.log-20261011.gz 106
一天几百个 404 全落在同一类路径上,说明被扫是常态。历史日志是 gz 压缩的,grep 换成 zgrep 就能直接读。
error.log 里躺着一条 UNION SELECT
访问日志只记状态码,payload 在返回 404 的时候看不见。去翻错误日志:
tail -50 /www/wwwlogs/littlesheep.cc.error.log
里面有一段 PHP 报错,报错上方挂着完整的 SQL:
WHERE 1=1 AND wp_posts.post_author NOT IN (0) UNION SELECT 999999,2,0x323032…-- -)
… client: 98.98.238.158, request: "POST /?rest_route=/batch/v1 HTTP/2.0"
配合访问日志看,98.98.238.152 到 98.98.238.162 这一段试了 POST /wp-json/batch/v1 和 POST /?rest_route=/batch/v1,拿到 207。WordPress 的 REST 批量接口一次能塞好几个请求,常被拿来打包探测。这次没成,数据库报错反倒帮了忙,把 payload 完整留在了日志里。所以 /www/wwwlogs 的权限别放开,那里面既有别人写的注入语句,也有你自己的后台路径。
别急着封,先看那是不是真 IP
163.181.92.164 一天扫 41 次,第一反应当然是 deny 掉。查一下归属:
curl -s https://ipinfo.io/163.181.92.164/json
返回的 org 是 AS24429 Zhejiang Taobao Network,阿里云的网络,不是某个家宽用户。再看同一段的请求就更明白了:163.181 段里既有 378 次 PetalBot(华为的搜索爬虫),也有 bingbot,也有空 UA 的扫描器,还有 25 条请求连 UA 都没有。一个「访客」不可能同时是这些东西。
真相是站点挂在阿里云 ESA 后面,nginx 看到的 remote_addr 全是回源节点。照着这个 IP 封禁,等于把整个回源网段拉黑,站点当场打不开。
真实访客的 IP 藏在 X-Forwarded-For 里。不确定的时候别猜,加一个临时日志格式看一眼。先在 nginx.conf 的 http 块里加一行:
log_format xff_test '$remote_addr | $request | $status | xff=[$http_x_forwarded_for]';
再到站点配置里,紧跟着原有的 access_log 那行补一行新的,原来的那行别动:
access_log /www/wwwlogs/xff-test.log xff_test;
nginx -t && /etc/init.d/nginx reload 之后随便刷几页,日志里就能看到:
8.25.82.157 | GET /author/1?tab=post… | 200 | xff=[207.46.13.151] ← bingbot
155.102.169.164 | GET /author/75?tab=follow&orderby=views | 200 | xff=[52.167.144.231] ← 还是 bingbot
207.46.13.151 和 52.167.144.231 才是 bingbot 的真实出口地址,前面那一列全是同一个回源节点。
有个细节要记牢:客户端自己带的 XFF 不会被丢掉,会被拼在前面。我故意带了个假的 X-Forwarded-For: 1.2.3.4 打过去,日志记的是:
xff=[1.2.3.4, 36.251.94.48]
取第一个值等于把伪造权交给攻击者,真值在尾部。
用 real_ip 把真 IP 换回 remote_addr
既然 XFF 里就是真 IP,就不用每次手动查了。先确认 nginx 编了 realip 模块:
nginx -V 2>&1 | tr ' ' '\n' | grep realip
--with-http_realip_module
宝塔装出来的 nginx 默认带这个。然后在站点配置的 server 块里加三行:
set_real_ip_from 36.251.253.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
第一行的网段从 ESA 控制台的「源站防护」页面拿,只写回源 IP。别图省事写 0.0.0.0/0,那等于宣布「谁说的我都信」,伪造的 XFF 立刻生效。
reload 之后再看那个临时格式:
36.251.94.48 | real=36.251.253.169 | GET /?realip=A HTTP/2.0 | 200 | xff=[36.251.94.48]
remote_addr 从回源节点换成真实访客地址,real 那一列保留原值方便对照。再带一次伪造头验证:
2408:8249:56:c020:d635:38ff:fe64:36c7 | real=36.251.253.163 | GET /?realip=B | 200 | xff=[1.2.3.4, 2408:8249:56:c020:d635:38ff:fe64:36c7]
伪造的 1.2.3.4 没被采纳,remote_addr 还是我自己。递归查找从右往左找第一个不在信任列表里的地址,尾部才是真值。
改完这些,按 IP 封禁、限流、统计访客才有意义。要掐掉那台扫描器,在站点配置里给它一条:
location = /bless.php { return 444; }
444 是 nginx 认的状态码,直接关连接不回响应,比 403 省事。
验证与回滚
改配置先 nginx -t,通过了再 reload。验证用带特征 UA 的请求打自己一下:
curl -s -o /dev/null -w "%{http_code}\n" -A "Go-http-client/2.0" https://www.littlesheep.cc/bless.php
回滚就是把加的行删掉,重新 nginx -t && /etc/init.d/nginx reload。动 nginx.conf 之前先备份一份,宝塔的站点配置在面板里改一次网站设置就可能被重写,改完回来确认那几行还在。
几个坑
别只看次数封 IP。这一天请求量排第一的是 61.170.77.21,48 次。看它请求了什么:同一个 webp 图片 45 次,全是 304。那是 CDN 和浏览器在回验缓存,封了等于封自己的访客。
403 不一定是被攻击。19 个 403 全来自 SemrushBot、MJ12bot、AhrefsBot、Bytespider 这些 SEO 爬虫,站点配置里本来就有一行 UA 黑名单在拦它们,是自己写的规则在干活。
注意 access_log /dev/null。宝塔默认把图片、JS、CSS 这些 location 的访问日志关掉了,静态资源根本不进文件,所以日志里的 404 会显得全集中在 PHP 路径上。想统计图片盗链得单独给那个 location 开日志。
日志会越滚越大。每个域名一天几百 KB 到几 MB,一个月就是几百兆,gz 压缩能省不少盘,但排查时别忘了 zgrep。要是盘本来就不宽裕,可以看看之前那篇服务器磁盘排查,日志和监控数据一样属于不知不觉长大的类型。
真要把扫描挡在门外,边缘那层比源站管用,ESA 规则里按 UA 或路径拦掉 Go-http-client 和空 UA 的请求,源站只留兜底。日志还是得看,它是唯一能告诉你「刚才到底发生了什么」的东西,比任何面板上的拦截数字都实在。加固相关的动作可以对着安全加固那篇一起做,两篇配合下来,扫到你的站点基本只会留下日志噪音。



![[教程][服务器]在阿里云服务器上卸载AliYunDunMonito(阿里盾)-小绵羊的小窝](https://www.littlesheep.cc/wp-content/uploads/2025/12/114612929920251229005735.png)

![[免费][教程]子比主题美化,优化教程-小绵羊的小窝](https://www.littlesheep.cc/wp-content/uploads/2026/09/3b06fd6a0520260920160148.webp)
![[免费][插件]将WordPress图片上传到LskyPro蓝空图床的插件-小绵羊的小窝](https://www.littlesheep.cc/wp-content/uploads/2026/09/b9e1998d8420260920160152.webp)

![[资源][教程]AI SEO - WordPress插件-小绵羊的小窝](https://www.littlesheep.cc/wp-content/uploads/2026/01/9f396016b220260112151830.png)



暂无评论内容