后台被弱口令爆破:从暴露面清点到三道闸门

半年前接手一个二手站,后台只有一个人,密码是admin加三个数字,登录地址是 /admin/,没有验证码,没有失败锁定。当时我提醒过一句,得到的回答是「这么小的站谁盯上」。三个月后,服务器在凌晨两点开始往外发垃圾邮件,被机房封了 25 端口,站长第二天早上才发现——攻击者是通过后台弱口令进去的,传了个 PHP 脚本,然后把机器当成了发信用的跳板。

这件事之后我帮他把后台改了一遍。写这篇文章不是为了吓人,而是想说明一件事:后台爆破的防护重点不在「把密码改复杂」,而在于让攻击的成本高于收益,并且让爆破这件事在日志里留下可识别的痕迹。很多站主的做法是加一个验证码就结束了,效果很有限——现代爆破工具会带上 OCR 或者人工打码。

一、先摸清自己的暴露面

在改任何配置之前,先假定自己是攻击者,看看能拿到什么。

第一步,确认后台路径是不是众所周知的默认值。Typecho 是 /admin/,WordPress 是 /wp-login.php 和 /wp-admin/,宝塔面板是 :8888,phpMyAdmin 常见于 /phpmyadmin/。这些路径被扫描器收录在字典里,是第一批被打的目标。改路径不是安全措施(知道的人还是知道),但能挡掉 90% 的自动化扫描,成本极低,值得做。

第二步,从外部实际扫一遍自己的站,别凭记忆。用 nmap 或者直接看 Nginx 日志里被请求过的管理路径:

grep -oE '"(GET|POST) [^ ]*(admin|login|wp-login|phpmyadmin|\.php)[^"]*' \
  /var/log/nginx/access.log \
  | awk '{print $2}' | cut -d'?' -f1 | sort | uniq -c | sort -rn | head -30

如果日志里已经有几千条对 /wp-login.php 的 POST,说明你早就被扫过了,只是没成功。这也是判断「是否已被爆破」的第一个信号。

第三步,确认没有「藏起来的管理入口」。这条最容易被忽略:主站后台改了密码,但三年前装的图床程序、测试用的 /test.php、忘了删的 /install/ 目录还在,它们的密码还是初始值。用一个字典扫一遍自己的常见路径(不作为攻击手段,只用来清点),或者直接翻 web 目录里所有含 login、admin、install 的文件名:

find /www/wwwroot -maxdepth 4 -type f \
  \( -iname '*login*' -o -iname '*admin*' -o -iname '*install*' \) 2>/dev/null

二、口令本身的正确做法

先明确一点:「复杂度规则」对防爆破几乎无效。要求必须有大写、数字、符号,实际结果就是 Admin@2024 这种可预测的组合,字典里都有。真正有效的是长度和不重复使用。

给后台账号设口令,按这个顺序做:一,长度 16 位以上,用四到五个随机词拼起来(例如三个不相关的名词加一个数字),比 12 位随机字符更好记也更难猜;二,后台密码与任何其他服务不共用,尤其是邮箱——邮箱丢了可以重置所有东西;三,密码存在密码管理器里,不要写在便签、不要贴在 /root/note.txt。

除了主账号,还要处理两类「隐形的弱口令」:

一是数据库密码。很多站点的 MySQL root 口令和后台口令一样,甚至更简单(root / 123456)。MySQL 只监听 127.0.0.1 的话风险低一些,但如果 bind-address 是 0.0.0.0,那就是把数据库直接挂在公网。查一下:

ss -lntp | grep 3306
grep -E '^bind-address|^skip-networking' /etc/mysql/mysql.conf.d/mysqld.cnf

看到 0.0.0.0:3306 就说明暴露了。改 bind-address = 127.0.0.1,同时确认应用连接串写的是 localhost 或 127.0.0.1;如果应用在 Docker 容器里,要用容器网关地址而不是 127.0.0.1,改完务必验证应用能连上再重启——这是本文最容易引发「改完站挂了」的一步。

二是 SSH。默认 PermitRootLogin yes 加上 root 弱口令,被爆破成功只是时间问题。改成密钥登录、禁用密码:

# /etc/ssh/sshd_config.d/99-hardening.conf
PermitRootLogin prohibit-password
PasswordAuthentication no
MaxAuthTries 3
LoginGraceTime 20

改之前必须先把公钥装好并在另一个终端验证能登录,再执行 sshd -t 检查语法,最后 systemctl reload sshd。同时保留一个已登录的会话不要关,出问题还能救回来。

三、让爆破变成一件很贵的事

口令做对了,还要让攻击者连尝试的机会都很少。这里有三层,从便宜到贵,建议至少做前两层。

第一层:后台登录失败限制。Nginx 的 limit_req 对登录接口单独开一个 zone,限制同一 IP 的 POST 频率:

limit_req_zone $binary_remote_addr zone=login:10m rate=6r/m;

location = /admin/login.php {
    limit_req zone=login burst=3 nodelay;
    limit_req_status 429;
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

location = /wp-login.php {
    limit_req zone=login burst=3 nodelay;
    limit_req_status 429;
    # 不跑的站直接返 444,连响应都不给
    # return 444;
}

几个必须注意的点。rate=6r/m 是每分钟 6 次,burst=3 允许短时突发 3 个,正常人登录不会触发,脚本爆破会被立刻限住。limit_req_status 429 要配合 add_header Retry-After always 一起用,否则 429 响应里不会有这个头——而这个头正是告诉正常爬虫「过一会再来」的关键,虽然这里的目标是攻击者,但不加 always 会让 429 变成完全裸的响应,浏览器表现也怪。

另外,如果前面挂了 CDN,$binary_remote_addr 拿到的是 CDN 节点 IP,所有用户共用一个 key,一被限就是全网被限。必须先配好 set_real_ip_from 和 real_ip_header,并且不要把 set_real_ip_from 0.0.0.0/0 写进去——那等于任何人带个 X-Forwarded-For 就能伪造 IP,限速形同虚设。

第二层:应用层的失败计数。Nginx 限的是频率,挡不住「每 10 秒试一次、试一整晚」的慢速爆破。应用层要按账号做累计计数:同一账号 15 分钟内失败 5 次,锁定 30 分钟;同一 IP 24 小时内失败 20 次,直接拒绝该 IP 的所有登录请求。

实现上不要把计数写进数据库的主表,用一个独立的小表或者 Redis 的 INCR + EXPIRE 结构。关键细节是失败计数要同时按账号和 IP 两个维度记,只按账号记会导致「攻击者拿一个不存在的用户名狂试」,永远不会触发锁定,但会持续消耗你的数据库资源。

第三层:fail2ban 兜住整台机器。把「某种日志形态出现 N 次」自动翻译成防火墙封禁:

[zz1984-admin]
enabled  = true
filter   = zz1984-admin
logpath  = /var/log/nginx/access.log
maxretry = 8
findtime = 600
bantime  = 86400

# jail.local 全局
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 你的固定办公 IP
backend  = systemd

过滤规则只需要匹配「POST 到登录接口且返回 401/429」这一类组合。注意开头那个 ignoreip:不写自己的 IP 就会把自己锁在门外,而且如果是通过跳板机或者内网访问,内网段也要加进去。

四、验证防护是否真的生效

配完不看效果等于没配。用 10 个连续的失败请求测一下:

for i in $(seq 1 10); do
  curl -s -o /dev/null -w '%{http_code} ' \
    -X POST -d 'name=admin&password=wrong' \
    http://127.0.0.1/admin/login.php
done; echo

预期前 3 次返回 200(登录失败页),后面开始出现 429。如果全是 200,说明 limit_req 没匹配上你的 location(常见原因是 location 用了正则而请求路径被 rewrite 了),去 nginx -T 输出里确认最终生效的那段配置。

再验证 fail2ban:故意失败 10 次后执行 fail2ban-client status zz1984-admin,应该看到 Banned IP list 里有 127.0.0.1——测完记得 fail2ban-client set zz1984-admin unbanip 127.0.0.1 解封,否则你自己接下来连不上。

五、如果已经被爆破了怎么办

判断是否被入侵,别只看首页正不正常。四个位置:

一是 web 目录里最近被修改或新增的 PHP 文件。find /www/wwwroot -name '*.php' -mtime -7,逐个看不是自己写的那些。木马常见形态是一长串混淆字符,或者文件名像 x.php、a1.php、.index.php 这种。

二是 crontab。攻击者最喜欢往 /var/spool/cron/crontabs/ 里写挖矿或回连任务:ls -la /var/spool/cron/crontabs/,root 的 crontab 里出现 curl xxx | bash 基本确诊。

三是进程。看有没有陌生进程在跑,尤其是高 CPU 的:ps aux --sort=-%cpu | head,以及 ss -antp 里有没有连向陌生 443/4444 端口的连接。

四是 SSH 的 authorized_keys。攻击者拿到 shell 后第一件事通常是加自己的公钥实现持久化,所以要检查所有用户的 ~/.ssh/authorized_keys,不是你加的全都删。

确认被入侵后,不要在线上清理。正确的顺序是:先拔网线(或者安全组只留自己的 IP)、导出数据、在干净的新机器上重建、再切换。清理一个已经被拿到 root 的机器几乎不可能彻底,rootkit 可能藏在任何地方,而你无法证明它干净。

六、一个常被忽略的收益:日志

做完这些之后,最大的收获其实不是「挡住了」——大部分个人站本来就不会被定向攻击——而是你终于知道有谁在敲门。每周看一眼拒绝次数最多的 IP 和请求最多的后台路径,你会看到很多有趣的东西:有人在扫 /wp-login.php(而你根本不用 WordPress),有人在找你三年前删掉的 /backup.zip,有人在试 /admin/config.php.bak。

这些日志同时也是「该清理什么」的清单。.bak、.sql、.git/、www.zip 这类文件如果在 web 目录里,不需要爆破就能直接下载。用一条命令查一遍:

find /www/wwwroot -type f \
  \( -name '*.bak' -o -name '*.sql' -o -name '*.sql.gz' \
     -o -name '*.zip' -o -name '*.tar.gz' -o -name '*.old' \) 2>/dev/null

有就直接移出 web 目录。这一条比改密码更有效——密码再强,备份文件摆在那里也白搭。

七、收口

后台加固不是一次性工作,而是一套「让攻击变贵、让痕迹可见」的组合:路径不显眼、口令够长且不重用、登录频率受限、失败有累计计数、整机有 fail2ban 兜底、可疑文件定期清点、出事时有干净重建的预案。

按重要性排序的话,我会说:先把 .bak/.sql 这类可直接下载的备份文件清掉(五分钟,收益最大),再把 MySQL 和 SSH 的暴露面收掉,然后加登录限速与失败锁定,最后才是定期看日志。全部加起来一个下午能做完,做完之后这个站就不会再成为别人发垃圾邮件的跳板。

Last modification:October 26th, 2026 at 08:27 pm

Leave a Comment