站点被入侵之后追源头,十次有七次不是「漏洞被利用」,而是有人拿到了一个能用的账号。口令是从别处泄露的、2FA 嫌麻烦没开、外包结束后管理员账号忘了删、离职同事的编辑权限还留着——这些都不需要攻击者有技术,只需要有耐心。
WordPress 安全加固指南的第四节把这件事放在 P0 第二位,给的是判断框架:口令要长、限流有三层、2FA 优先 TOTP、管理员越少越好。这一篇往下走一层,讲具体怎么配、配完会连带影响什么、出事之后按什么顺序处理。
如果你已经怀疑站点被入侵过,先不要在这里花时间——按 WordPress 被黑后怎么清理 走完取证和清理,再回来改账号。后门还在的时候改口令,只是换了一把锁,人还在屋里。
一、三种攻击,防御动作完全不同
「防爆破」是个笼统说法。实际打过来的方式有三种,混在一起谈会导致配了措施却没防住该防的东西。
| 攻击形态 | 怎么打 | 典型特征 | 对应防御 |
|---|---|---|---|
| 撞库(凭据填充) | 拿别处泄露的邮箱+口令组合,批量试几万个站点 | 大量不同用户名、每个只试 1–2 次,单个 IP 失败次数不高 | 口令不复用 + 泄露库自查 + 2FA |
| 定向爆破 | 盯住一个站点,试 admin、站点名、公司名等字典 | 同一 IP 或同一 IP 段高频失败,集中在 wp-login.php | 限流 + fail2ban + 改登录路径 |
| 会话劫持 | 偷 Cookie 或利用 XSS 拿到已登录状态 | 不需要登录成功,直接复用会话 | HTTPS 强制 + 会话失效 + 前台 XSS 防护 |
三种里最难防的是撞库。因为它看起来不像攻击:请求频率低、每个账号只错一两次、分散在大量 IP 上,任何按「失败次数」触发的规则都抓不到。唯一的有效防线是「这个口令在别处泄露过所以不能用」和「除了口令还要第二样东西」。
所以在动手之前先想清楚:你要防的是脚本乱扫,还是有人在针对你。前者靠限流和改路径就够;后者只能靠 2FA——这是唯一一个「即使口令泄露也不会立刻失守」的措施。
二、第一步不是加固,是盘点
多数站点从来没数过自己有多少个能登录的账号。先把门数清楚。
# 列出全部用户及其角色
wp user list --fields=ID,user_login,display_name,user_email,roles,user_registered
# 只看管理员
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
没有 WP-CLI 的话,直接查库:
SELECT u.ID, u.user_login, u.user_email, u.user_registered, m.meta_value AS caps
FROM wp_users u
JOIN wp_usermeta m ON m.user_id = u.ID AND m.meta_key = 'wp_capabilities'
WHERE m.meta_value LIKE '%administrator%';
看四个地方:
- 不认识的账号。名字像
wordpress、support、test、admin2的一律警惕。 - 注册时间。如果一批账号的
user_registered集中在某个深夜的几分钟内,那基本不是人注册的。 - 邮箱域名。攻击者常留
@example.com、临时邮箱域名,或者干脆和站点业务不相干的域名。 - 最后登录时间。WordPress 默认不记录登录时间,装了安全插件才有。没有记录时,用
session_tokens这个 usermeta 判断——用户登录过就会有这个字段。
隐藏管理员怎么查
攻击者拿到的第一个管理员账号,通常不会老老实实待在用户列表里。有的会改 user_login 让它排在列表末尾不显眼,有的通过 mu-plugin 过滤掉用户列表查询。可靠的判断方式是绕过界面直接比对:
-- 所有在 usermeta 里带 administrator 能力的用户
SELECT user_id, meta_value FROM wp_usermeta
WHERE meta_key = 'wp_capabilities' AND meta_value LIKE '%administrator%';
把这份 ID 列表和后台「用户」页显示的管理员逐个对,对不上的就是被做了手脚的。多站点还要额外查 wp_sitemeta 里的 site_admins,超级管理员的权限不在 wp_capabilities 里。
这一步花不了十分钟,但它是后面所有加固的前提——账号没盘干净,配再多规则也可能留着一个后门钥匙。
三、口令:为什么「强制复杂度」是反效果
很多安全插件提供「强制密码复杂度」:至少 8 位、含大小写、含数字、含符号、每 90 天必须换。这套规则来自 2003 年的 NIST 建议,2017 年就被 NIST 自己废除了(SP 800-63B)。
原因很直接:人面对复杂度规则的行为是可预测的。要求大小写+数字+符号,用户就会写 Company2024!、Password1!、Abc12345!——这些全在世界通用破解字典的前一万条里。要求 90 天换一次,用户就在末尾加数字轮换:Company2024! → Company2025!。
NIST 现在的建议是三条:
- 长度优先,最低 8 位,建议 12 位以上,不强制字符种类组合
- 比对泄露库,出现在已知泄露数据里的口令直接拒绝
- 不做定期强制轮换,只在「疑似泄露」时换
落地怎么做
不用装插件也能做到大半:
- 管理员和数据库口令用密码管理器生成 20 位以上的随机串,一个站点一个,绝不复用
- 主机面板(宝塔)、SSH、数据库、WordPress 后台这四个口令互相独立——很多入侵是从面板口令泄露开始的,四处同一个等于一处破全破
- 泄露库自查:在 haveibeenpwned.com 查管理员邮箱,看是否出现在已知泄露事件里。有的话立刻换口令
改口令的正确方式
不要直接 UPDATE wp_users SET user_pass = MD5('新密码')。WordPress 从 2.5 起就不用 MD5 了,现在(6.8 之后)新口令默认用 bcrypt,旧哈希仍兼容 phpass。手写 MD5 的结果是你把自己锁在门外,而且没有任何报错。
正确做法:
# 用 WP-CLI 改,自动走正确的哈希
wp user update 1 --user_pass='新的长口令'
# 批量把所有管理员的会话踢掉(改完口令必须做)
wp user session destroy $(wp user list --role=administrator --field=ID) --all
如果没有 WP-CLI,写一个一次性脚本走 wp_set_password(),跑完立刻删掉文件。
改口令不会自动踢掉已登录的会话,这一点下面第七节会细说,但它是最容易被忽略的一步:你以为改了口令就安全了,攻击者的 Cookie 还有效。
四、登录入口:改路径值不值得做
「把 /wp-login.php 改成别的地址」是加固清单里的常客。它有用,但用处被高估了。
| 做法 | 挡得住什么 | 挡不住什么 | 副作用 |
|---|---|---|---|
| 改登录路径(插件) | 无脑扫 /wp-login.php 的自动化脚本 | 知道你新路径的定向攻击(路径会出现在页面源码、邮件、浏览器历史里) | 一串连带故障,见下 |
| 限制后台 IP 白名单 | 几乎所有外部登录尝试 | 自己换网络、出差、移动办公 | 一旦 IP 变化就把自己关在外面 |
| 关闭 XML-RPC | XML-RPC 爆破和应用密码滥用的入口 | 正常表单登录 | 部分 App、Jetpack、旧版发布工具失效 |
| 什么都不改,只上 2FA | 口令泄露后的登录 | 无 | 几乎为零 |
结论:对批量扫描有效,对定向攻击无效。如果只能做一件事,做 2FA,不要做改路径。
改路径的连带影响清单
这是实际操作里真正费时间的部分。改完之后失效的东西通常不会立刻暴露,而是在几天后某个业务环节出问题:
- 忘记密码邮件里的重置链接。链接指向的是旧路径,点了 404,用户以为账号废了
- WP-CLI 和 cron。多数改路径插件会在 CLI 环境下不生效,导致定时任务里的登录相关操作异常
- 支付回调与第三方通知。部分支付网关、ERP 对接会回调
/wp-admin/admin-ajax.php或/wp-json/,如果插件一刀切把/wp-admin也改了,回调会 404,表现为「客户付款了但订单没生成」 - 缓存插件与 CDN 规则。原有的「不缓存 wp-login.php」规则匹配不上新路径,登录页可能被缓存,出现「别人登录后你看到他的后台」
- 主题自定义登录页。硬编码
/wp-login.php的模板会失效
如果确实要改,改完按这张清单逐项回归,别只看「能登录了」就算完成。
五、限流:三层怎么分工,别互相抵消
三层的定位不一样,不是「装得越多越安全」。
| 层 | 拦在哪 | 统计口径 | 适合 |
|---|---|---|---|
Nginx limit_req | PHP 之前 | 请求速率(每秒/每分钟) | 挡住高频暴力请求,几乎零成本 |
| fail2ban | 系统层读日志 | 失败次数(按时间窗) | 挡住持续尝试,封禁时间长 |
| 安全插件 | WordPress 内部 | 失败次数 + 图形验证 | 没有服务器权限时的唯一选择 |
Nginx:只对登录接口限流
# http {} 块
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=5r/m;
# server {} 块,只对两个入口生效
location = /wp-login.php {
limit_req zone=wplogin burst=10 nodelay;
# 原 fastcgi 配置保持不变
}
location = /xmlrpc.php {
limit_req zone=wplogin burst=5 nodelay;
}
不要把限流加在整个站点上。区块编辑器保存文章时会并发发起几十个请求,burst 设小了会导致编辑页白屏、保存失败——这个坑我们踩过,细节写在WordPress 安全加固指南里,这里不重复。
fail2ban:规则写错会把正常登录算成失败
这是最容易出错的一处。WordPress 登录失败时返回 200(重新渲染表单),成功时返回 302(跳转到后台)。所以最常见的过滤规则——「匹配所有 POST /wp-login.php」——会把你自己每次成功登录也算成一次失败,五次之后把自己封了。
正确的过滤规则只匹配 200:
# /etc/fail2ban/filter.d/wordpress-login.conf
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php[^"]*" 200
ignoreregex =
# /etc/fail2ban/jail.local
[wordpress-login]
enabled = true
filter = wordpress-login
logpath = /www/wwwlogs/你的域名.log
maxretry = 5
findtime = 600
bantime = 86400
改之前先 tail -20 看一条真实日志,确认字段顺序和引号形式跟正则对得上——不同 Nginx 日志格式(combined、main、自定义 JSON)差异很大,直接抄别人的正则会静默失效,fail2ban-client status 里显示运行中,但一条都没封过。
把自己封了怎么救
# 看当前封禁名单
fail2ban-client status wordpress-login
# 解封自己的 IP
fail2ban-client set wordpress-login unbanip 你的IP
如果 SSH 也连不上(fail2ban 同时管着 sshd),只能从云厂商控制台的 VNC 登录,或者临时关机挂载磁盘改配置。所以上规则之前先把自己的 IP 加进 ignoreip,这一条比什么都重要。
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 你的固定IP
六、2FA:难的不是开启,是别把自己锁在门外
三种形式的强度差别WordPress 安全加固指南里已经讲过,结论是 TOTP 优先、邮件次之、短信最弱。这里只讲落地时会遇到的六个问题,这些是装完插件之后才会发现的。
一、强制范围给谁。 只强制 administrator 是最小可行方案。编辑、作者这些角色如果共用电脑、多人轮班,强制 2FA 会带来大量「换班登不上」的求助。判断标准是:这个账号能不能装插件、改主题——能,就必须 2FA。
二、备用码必须离线存。 所有 2FA 插件都会生成一批一次性恢复码,多数人点「已保存」就关掉了。正确做法是打印出来或者存进密码管理器,并且不要存在同一台手机上——手机丢了,码也跟着丢。
三、换手机是最高频的事故。 换机、恢复出厂、卸载验证器 App,都会让 TOTP 密钥消失。这时候只剩两条路:用备用码,或者让另一个管理员在后台临时关闭该账号的 2FA。所以站点上永远要保留至少两个管理员账号,其中一个的 2FA 由不同的人持有——只有一个管理员时,丢手机等于丢站点。
四、应用密码可能绕过 2FA。 WordPress 5.6 引入的「应用密码」(Application Passwords)走的是 Basic Auth,不经过登录表单。是否受 2FA 约束取决于插件实现,相当一部分插件不管这条路径。也就是说,开了 2FA 不等于关死了后门——如果不用,直接在 wp-config.php 里关掉:
// 完全禁用应用密码(不需要 REST API 外部调用时)
define( 'WP_ENVIRONMENT_TYPE', 'production' ); // 先确认环境类型
add_filter( 'wp_is_application_passwords_available', '__return_false' );
第二行要放在 mu-plugin 里,不要放主题的 functions.php——换主题就失效了。
五、REST API 和 XML-RPC 的兼容性。 有些 2FA 插件会连带拦截 /wp-json/ 的认证请求,表现是区块编辑器正常但手机 App 登不上、或者第三方发布工具报 401。上线前用编辑器保存一篇草稿验证一遍。
六、插件停更的风险。 2FA 插件一旦停止维护、与新版本 WordPress 不兼容,你会处于「关掉它不安全、留着它登不上」的两难。选插件时优先选装机量大、更新频繁的,并且确认它支持用 WP-CLI 或数据库方式紧急关闭,出问题时至少有退路。
七、会话:登录之后还在发生的事
口令和 2FA 管的是「能不能进来」,会话管的是「进来之后能待多久」。这一层最常被忽略,但在处置入侵时它是关键。
WordPress 登录后会种两个 Cookie:wordpress_[hash](认证信息)和 wordpress_logged_in_[hash](身份标识)。服务端对应的是一份存在 usermeta 的 session_tokens——每次登录生成一个 token,记住登录状态的有效期是 14 天,不勾选是 2 天。
关键点:改口令不会让已登录的会话失效。 攻击者如果已经拿到 Cookie,你改完口令,他那边照样能用。必须显式销毁会话:
# 踢掉某个用户的全部会话
wp user session destroy 1 --all
# 踢掉所有管理员的会话
wp user session destroy $(wp user list --role=administrator --field=ID) --all
# 全局:让所有用户的会话立即失效
wp eval 'wp_destroy_all_sessions();'
如果连 WP-CLI 都用不了,换密钥盐也能达到同样效果——所有 Cookie 的签名失效,等于全体强制重新登录。具体做法和顺序在 wp-config.php 安全配置 里,注意顺序是「先清后门 → 改口令 → 换盐」,反了等于给攻击者发新门票。
另外两个会话相关的设置值得调:
- 后台会话过期时间:默认 2 天(不勾记住我)对后台偏长,公共电脑场景可以缩短到几小时
- 并发会话数:WordPress 默认不限制,同一个账号可以在几十个设备上同时在线。对管理员账号,可以考虑限制并发数,异常时能更早发现
八、权限:五个角色各能干什么
WordPress 内置五个角色,能力是包含关系。多数误配来自「不清楚边界,索性都给管理员」。
| 能力 | 管理员 | 编辑 | 作者 | 投稿者 |
|---|---|---|---|---|
| 装插件、换主题 | ✅ | ⛔ | ⛔ | ⛔ |
| 改设置、管用户 | ✅ | ⛔ | ⛔ | ⛔ |
| 改他人的文章 | ✅ | ✅ | ⛔ | ⛔ |
| 发布自己的文章 | ✅ | ✅ | ✅ | ⛔ |
| 审核评论 | ✅ | ✅ | ⛔ | ⛔ |
| 上传文件 | ✅ | ✅ | ✅ | ✅ |
投稿者发布的文章需要编辑或管理员审核才能上线;订阅者只能管理自己的资料,不能发文、不能上传文件,所以表里不单列一栏。
三个常见误配:
- 给内容编辑管理员权限「方便改版式」。管理员能装插件,而插件是 WordPress 最常见的入侵入口。要改版式,用编辑角色 + 主题自带的版式选项,不要给管理员
- 离职只改口令不删账号。账号还在,只是口令变了——如果这个账号曾经被泄露过,攻击者手上可能还有有效会话。正确做法是删账号,并把其文章转移给新作者
- 共用一个管理员账号。出问题时无法定位是谁操作的,而且一个人泄露全体沦陷。宁可多建几个账号
自定义角色的坑
需要「只能改某几个栏目」这类细粒度权限时,会用 add_role() 自定义。要注意:add_role() 写进主题的 functions.php 会随换主题失效,而角色数据是存在数据库里的,失效后用户会变成「无角色」,登录后看不到任何菜单,表现为「后台一片空白」。
正确做法是放进 mu-plugin(wp-content/mu-plugins/),它不受主题切换影响:
<?php
// wp-content/mu-plugins/custom-roles.php
add_action( 'init', function () {
if ( get_role( 'content_manager' ) ) {
return; // 已存在就不重复创建
}
add_role( 'content_manager', '内容管理', [
'read' => true,
'edit_posts' => true,
'publish_posts'=> true,
'upload_files' => true,
'edit_others_posts' => true,
] );
} );
九、外包与交接:账号怎么给,怎么收
给建站公司、运营外包、临时开发开账号,是最容易留下长期口子的一环。原则只有一条:给临时账号,不给主账号;有开始时间,也有结束时间。
| 场景 | 给什么 | 注意 |
|---|---|---|
| 建站交付期 | 新建管理员,专属该外包方 | 不要把自己账号口令给出去 |
| 内容代运营 | 编辑角色 | 不给管理员,装插件需求走你这边 |
| 临时排障 | 管理员 + 约定回收日期 | 结束当天删账号,不是改口令 |
| 交接给客户 | 客户自己的管理员 | 你的账号降权或删除 |
交接时按这份清单过一遍:删外包账号 → 改所有共享口令 → 换密钥盐(强制全体重新登录)→ 检查用户列表有无新增 → 查 wp_options 里 active_plugins 有无陌插件 → 看一遍定时任务。
十、异常登录怎么识别,怎么处置
没有安全插件时,判断是否被登录过靠日志和几个痕迹:
| 迹象 | 在哪里看 | 说明 |
|---|---|---|
| 登录日志里出现没见过的时间点 | Nginx access log 中 POST /wp-login.php 后跟 302 | 成功登录的标志 |
session_tokens 有多个陌生条目 | wp_usermeta 中 session_tokens | 每个条目对应一次登录 |
| 用户列表多出账号 | 后台用户页 + SQL 比对 | 见第二节 |
| 文章被改、出现外链 | 文章列表按修改时间排序 | 攻击者常植入外链做 SEO 作弊 |
| 定时任务多出陌生项 | wp cron event list | 后门常靠 cron 复活 |
确认被登录过之后,顺序不能乱:
- 先取证(日志、用户列表、文件时间),别急着删东西
- 再清后门,按 被黑清理手册 走
- 然后改口令 + 换密钥盐 + 销毁会话,三件一起做
- 最后才加固登录层——在还没清干净的时候加固,等于给入侵者演示你的防御
十一、常见问题
Q:只用一个管理员账号,行不行?
技术上可以,运维上是风险。丢手机、忘记口令、账号被锁,都会导致完全无法进入后台。至少两个管理员,其中一个的 2FA 由不同的人持有。
Q:改登录地址之后,攻击者还能找到吗?
能。新地址会出现在页面源码的登录链接、密码重置邮件、浏览器历史、甚至搜索引擎缓存里。它只挡无脑扫描,不挡定向攻击。
Q:2FA 开了还会被入侵吗?
会。2FA 挡的是「口令泄露后的直接登录」,挡不了插件漏洞、文件上传漏洞、服务器层入侵。它是账号层的最后一道,不是整个站点的最后一道。
Q:多久换一次管理员口令?
不按时间换,按事件换:疑似泄露、人员离职、外包结束、发现异常登录。定期强制轮换会让用户写出可预测的变体,反而更弱。
Q:WordPress 能不能限制登录失败后锁定账号?
能,但要小心。按账号锁定会被用来做拒绝服务——攻击者故意输错你的账号口令,把你锁在外面。优先按 IP 锁定(fail2ban),而不是按账号锁定。
Q:用了 CDN 或反向代理,fail2ban 封的是 CDN 的 IP 怎么办?
需要在 Nginx 里还原真实 IP(real_ip_header X-Forwarded-For + set_real_ip_from CDN 段),否则日志里全是 CDN 节点 IP,一封就是整片用户。配置错了会出现「全站用户突然都访问不了」。
十二、一页纸清单
按顺序做,做完打勾。
| # | 动作 | 优先级 | 怎么验证 |
|---|---|---|---|
| 1 | 盘点全部用户,列出管理员 | P0 | wp user list --role=administrator |
| 2 | SQL 比对隐藏管理员 | P0 | wp_usermeta 中 wp_capabilities 比对 |
| 3 | 删除不认识的账号 | P0 | 用户列表数量下降 |
| 4 | 管理员换长口令,不复用 | P0 | 密码管理器生成 |
| 5 | 管理员全部开启 TOTP 2FA | P0 | 退出后重新登录需验证码 |
| 6 | 备用码离线保存 | P0 | 两张不同介质各存一份 |
| 7 | 至少保留两个管理员账号 | P0 | 用户列表 |
| 8 | 销毁所有旧会话 | P0 | wp user session destroy --all |
| 9 | 只对登录接口做 Nginx 限流 | P1 | 编辑页保存不报错 |
| 10 | fail2ban 规则只匹配 200 | P1 | fail2ban-client status 有封禁记录 |
| 11 | ignoreip 加入自己的固定 IP | P1 | 连续输错 10 次不被封 |
| 12 | 关闭或限制 XML-RPC | P1 | curl -I /xmlrpc.php 返回 403 |
| 13 | 禁用应用密码(如不用) | P1 | 用户资料页无该区块 |
| 14 | 离职/外包账号删除而非改密 | P1 | 用户列表 |
| 15 | 自定义角色代码放 mu-plugin | P2 | 切换主题后角色仍存在 |
十五项里,前八项是 P0,加起来大约一个小时。如果只做三件:删掉多余管理员、开 2FA、销毁旧会话——这三件覆盖了绝大多数「账号被拿走」的场景。
至于账号以外的地方,插件漏洞、文件上传、服务器层,在WordPress 安全加固指南里按优先级排过一遍;插件怎么选型,看 8 款安全插件推荐;连登录地址都找不到的新手,先看 登录网址怎么找。