
前几天维护壹导航时,我在 WordPress 后台发现了一个完全不认识的管理员账号。更让我困惑的是,站点明明把“新用户默认角色”设为订阅者,这个账号却直接拥有了管理员权限。
后来我顺着数据库、Nginx 日志和 OneNav 的数据字段一路排查,才确认这不是普通注册,也不是 WordPress 自己把角色配错了,而是一次典型的 Stored XSS(存储型跨站脚本):恶意内容借用了我已登录的管理员浏览器会话,自动创建了后门账号。
从一个陌生管理员开始
安全事件排查最忌讳凭感觉全站乱翻。我先查询 wp_users,确认该账号的创建时间为 2026 年 8 月 20 日 15:32。有了这个准确时间点,后续检索日志的范围就一下收窄了。
SELECT ID, user_login, user_email, user_registered
FROM wp_users
ORDER BY ID DESC
LIMIT 20;
我也因此确认了一件事:所谓“默认角色为订阅者”,只作用于普通前台注册。后台的添加用户功能本来就能指定角色;如果攻击者能够借用管理员会话提交后台请求,默认角色设置并不会介入。
三条日志把攻击链串了起来
在 15:32 前后的 Nginx access log 中,最关键的是下面这段时间线:
15:32:28 打开一个网址条目页面
15:32:29 页面调用 get_sites_seo
15:32:30 GET /wp-admin/user-new.php
15:32:31 POST /wp-admin/user-new.php
15:32:32 跳转 users.php?update=add&id=3
这说明新用户不是从前台注册产生的,而是走了 /wp-admin/user-new.php——WordPress 后台的添加用户页面。更异常的是,这些请求的 IP、浏览器环境都与我当时自己操作后台时完全一致,且创建页面的 GET 与 POST 只相隔一秒。
正常人不可能在一秒内打开添加用户页、填写账号资料、选择角色并提交;POST 的来源页又是刚打开的网址条目,而非用户编辑页面。结论很清楚:请求由浏览器里的 JavaScript 自动发出。
恶意内容藏在一个本应只放普通文本的字段里
继续查对应条目的 wp_postmeta,我在 _sites_country 里找到了恶意内容。这个字段原本只应该保存国家或地区等普通文本,却被写入了带事件处理器的 HTML。
当管理员打开该条目时,主题将字段内容直接放进页面;浏览器把它当作 HTML 解析,于是脚本获得了管理员当前会话能够获得的权限。它随后访问“添加用户”页面、读取当前有效的 WordPress nonce,再提交创建用户请求。由于 Cookie、权限和 nonce 都来自真实管理员会话,WordPress 会把它视为一次合法操作。
这也是为什么 nonce 并没有“失效”:它擅长防止外站伪造请求,却无法区分“管理员亲自点击”和“已经在管理员页面中执行的恶意脚本”。
不要只删账号:我做了这些处置
- 删除陌生管理员并复查所有管理员。先切断最直接的后门入口,同时检查是否还有未知账号、异常应用程序密码或近期角色变更。
- 让全部会话失效并重设管理员密码。我清理了
session_tokens,让所有用户重新登录;管理员密码也全部更换。安全事件里,“暂时没发现泄露”不应等同于“可以继续沿用”。 - 暂停前台投稿,先保存证据。触发事件的网址条目先改为草稿,不立即永久删除。样本、日志和数据库记录都有助于后续溯源与验证修复效果。
- 扫描整个数据库,而不是只搜 <script>。这次攻击没有用传统 script 标签,因此还要检索事件属性、
javascript:、后台用户创建路径、角色关键字等攻击指纹,重点查看wp_postmeta与文章正文。 - 清理确认过的恶意 meta 后再复扫。清理前先备份;清理后用相同条件复查,确认没有残留。
根治关键:输入净化 + 输出转义
删掉恶意数据并不等于修复漏洞。根本问题是:_sites_country 为什么能接收 HTML,又为什么会被当成 HTML 原样输出?
对于国家/地区这类普通文本字段,保存时应限制为纯文本,例如使用 sanitize_text_field();显示时仍必须使用 esc_html()。这两层防护缺一不可:前者减少脏数据进入数据库,后者保证即使历史数据或其他入口带入异常内容,浏览器也只把它显示为文字。
同时,我会继续检查 OneNav 投稿流程、get_sites_seo 相关返回值,以及所有写入自定义字段的入口:外部网页标题、描述、投稿资料和 AJAX 响应都不能直接进入 innerHTML 或未经转义的模板输出。
这次事件给我的提醒
- “默认注册角色是订阅者”不是管理员会话被利用时的防线。
- 日志显示的是自己的 IP、Cookie 和浏览器,不代表操作一定出自自己之手;XSS 正是借浏览器替攻击者行动。
- 排查要先建立精确时间线,再把页面访问、AJAX、后台请求和数据库变化对应起来。
- 前台投稿、抓取第三方网页信息和自定义字段,都是必须按不可信输入处理的边界。
这次事件没有因为“删掉陌生账号”就结束。真正值得完成的是:清除残留、使旧会话失效、修补输入和输出两端、关闭未确认安全性的入口,并持续验证。把这个过程记录下来,也希望能提醒同样使用 WordPress 和 OneNav 的站长:后台登录状态并不能抵挡存储型 XSS,数据边界才是第一道防线。
你认为这个网站怎么样?
-人
-人
-人
-人
-人
© 版权声明
文章版权归作者所有,未经允许请勿转载。