WordPress 核心文件被篡改怎么查:校验值比对与定向重装

挂马处理最怕的不是删不掉文件,而是删完之后说不清还剩没剩。这篇指南教你如何用官方校验值,把「WordPress 核心文件被篡改」从主观判断变成一份可核对的差异清单:哪些核心文件被动过、哪些文件是凭空多出来的、重装之后怎么证明干净。整个过程不需要重装整台服务器,也不用停站搬家,多数站点半小时内就能跑完第一轮比对。

WordPress 核心文件完整性核查封面插图

为什么要先比对校验值,而不是先删文件

WordPress 每个正式版本都会为全部核心文件计算校验值,并随发行包公开。WP-CLI 提供的校验值比对命令把本地文件与这份官方清单逐个对照,直接把不一致的文件名打印出来。它的价值在于把判断依据从个人经验换成了官方基线:wp-admin 下有两千多个文件,肉眼翻找不可能覆盖完整,校验值一次就能给全。

第一次运行通常会看到两种输出。一种是明确的 File doesn't verify against checksum,另一种是整站大面积报差异。后者往往不是被入侵,而是语言包造成的——中文站点装上 zh_CN 语言包后,部分核心文件内容与 en_US 基线不同,校验值自然报差异。处理办法是用同版本的简体中文发行包做本地基线,先把语言差异排除,再去看真正的异常,否则很容易把正常翻译文件当成木马删掉。

这条命令也有容易被忽略的局限:它只覆盖核心文件。wp-content 下的主题和插件、上传目录、根目录的 wp-config.php 以及数据库内容都不在检查范围内,所以校验值比对是入口而不是全部。如果你还分不清「入侵」和「配置错误」的边界,可以先看 WordPress 被黑排查与恢复清单,把症状对上号再动手。

  • 校验值比对命令的退出码非 0 即表示存在差异文件,可以直接放进脚本做判断
  • 官方校验值按版本发布,跨版本比对会得到大量假差异,动手前先确认本地版本号

比对之前先留一份能回滚的现场备份

比对之前必须先留证据。直接删文件会让后续判断失去参照:无法再确认某个文件原本是否存在,也无法在清理失败时回滚。备份放在站点目录之外,并保留文件的修改时间——这一点比备份本身更关键,判断「新增文件」完全依赖 mtime。

# 整站打包,保留权限与时间戳
tar -czpf /backup/site-$(date +%F-%H%M).tar.gz -C /var/www html

# 数据库单独导出,含存储过程与触发器
mysqldump --single-transaction --routines --triggers -u root -p wordpress_db > /backup/db-$(date +%F-%H%M).sql

# 记录当前文件清单,清理后用来对比
find /var/www/html -type f -printf '%T@ %p\n' | sort > /backup/files-before.txt

备份策略本身也有讲究:全量加增量的组合比「每天一次全量」更实用,节奏可以参考 WordPress 备份策略:全量与增量怎么搭配。这里只需确认一件事——备份必须包含数据库,因为持久化入口经常藏在 wp_options 而不是文件系统里。只备份文件的站点,恢复后往往几天内再次被写入。

挂马处理从取证到验收的四段流程插图

拉出一份可信的差异清单

有了备份就可以正式比对。中文站建议先用同版本官方中文包建立基线,再和线上目录做目录级差异,这样输出里只剩真正被改动的内容:

# 下载同版本官方中文包到临时目录(示例版本号按实际情况替换)
cd /tmp && wget -q https://cn.wordpress.org/wordpress-6.8.2-zh_CN.tar.gz \
  && tar -xzf wordpress-6.8.2-zh_CN.tar.gz

# 只比核心目录,wp-content 与 wp-config.php 不参与
diff -rq /tmp/wordpress/wp-includes /var/www/html/wp-includes | head -40
diff -rq /tmp/wordpress/wp-admin    /var/www/html/wp-admin    | head -40

# 根目录的散落文件最容易藏后门,单独看
ls -la /var/www/html/*.php

差异输出通常分三类。differ 表示同名文件被改,多是被注入 eval、base64_decode 之类的代码;Only in /var/www/html/... 表示凭空多出来的文件,是后门最典型的形态;Only in /tmp/wordpress/... 表示核心文件被删,常出现在攻击者想关掉某个安全检查函数时。

被篡改文件与新增文件的差异对比插图

按时间窗找出新增的可疑文件

核心目录的差异是明确的,麻烦在于 wp-content:这里没有官方基线,插件与主题文件本就随时变化,只能靠时间窗和代码特征缩小范围。先按异常出现时间往前推两周,列出这期间新增或修改过的 PHP 文件:

# 近两周被写入的 PHP 文件,排除缓存与上传目录
find /var/www/html -type f -name '*.php' -newermt '2026-10-10' \
  -not -path '*/wp-content/uploads/*' -not -path '*/wp-content/cache/*' \
  -printf '%TY-%Tm-%Td %TH:%TM  %p\n' | sort

# 常见后门特征串,命中即人工复核
grep -rIl --include='*.php' -e 'eval(' -e 'base64_decode(' -e 'gzinflate(' \
  -e 'assert($_' /var/www/html/wp-content /var/www/html/wp-includes | head -30

时间窗要配合业务节奏理解。插件自动更新、主题改模板都会产生新文件,正常写入的时间点通常聚在一起,而后门往往出现在凌晨、出现在不该有 PHP 的目录里(比如上传目录下冒出 cache.php),或刻意模仿核心文件名。另外三处常被漏掉:mu-plugins/ 里的文件会被自动加载却不出现在插件列表;.htaccess 末尾追加的 RewriteRule 会把特定来源转到外部站点;index.php 开头插入的 require 指向随机命名文件。

判断标准不是「看起来可疑」,而是「无法解释来源」。一个文件如果既不在官方清单里、也不在插件更新的时间点、内容还带编码函数,就按不可信处理,后面统一交给重装与替换流程,不要在服务器上逐个手工清理——手改核心文件很容易把站点改坏,也让下一轮比对失去意义。

定向重装核心文件,不动 wp-content 和数据库

确认差异清单后,修复动作本身很轻:把核心目录换成官方原版。--skip-content 保证 wp-content、wp-config.php 不被碰:

# 覆盖核心文件,保留内容目录与配置文件
wp core download --version=6.8.2 --force --skip-content --path=/var/www/html

# 语言包按需重新安装
wp language core install zh_CN --activate --path=/var/www/html

# 核心与插件的校验值复核
wp core verify-checksums --path=/var/www/html && echo VERIFY_DONE
wp plugin verify-checksums --all --path=/var/www/html

重装后必须处理权限,否则后门即使删掉,下一次写入依旧可能成功。目录 755、文件 644 是通用基线,wp-config.php 建议 600,上传目录禁止执行 PHP:

find /var/www/html -type d -exec chmod 755 {} +
find /var/www/html -type f -exec chmod 644 {} +
chmod 600 /var/www/html/wp-config.php

至于 wp-content 里的可疑文件,顺序是先备份再删除,主题和插件优先用官方包重装一遍(wp plugin install <slug> --force),比逐文件核查更可靠。希望把「哪些文件属于插件、哪些不属于」系统化的读者,可以参考 WordPress 分类内容 里的插件管理思路。

数据库、密钥与计划任务里的残留

文件清理干净不代表站点干净。挂马最常见的持久化手段都在数据库里,重装核心文件并不影响它们,这也是「清了又中」的主因。下面几条查询可以直接定位:

# 自动加载的选项里出现编码函数,八成是被注入
wp db query "SELECT option_name, LENGTH(option_value) AS len FROM wp_options \
  WHERE autoload='yes' AND (option_value LIKE '%base64%' OR option_value LIKE '%eval(%')" 

# 近期新增的管理员账号
wp user list --role=administrator --fields=ID,user_login,user_registered,user_email

# 正文里嵌了 iframe 或外链脚本的文章
wp db query "SELECT ID, post_title FROM wp_posts \
  WHERE post_status='publish' AND (post_content LIKE '%<iframe%' OR post_content LIKE '%document.write%')"

# 计划任务里陌生的钩子
wp cron event list --fields=hook,next_run_relative

查出问题后按来源处理:陌生账号用 wp user delete <ID> --reassign=<保留ID> 删除,注入的选项用 wp option delete <name> 清掉,可疑钩子用 wp cron event delete <hook> 移除。接着必须换掉全部安全密钥,让已泄露的登录态立即失效:

wp config shuffle-salts --path=/var/www/html

改完密钥后后台会提示所有用户重新登录,这是正常现象,说明旧会话凭证已作废。多人协作的站点提前通知编辑重新登录,避免被当成故障。

数据库与文件两处残留点的对照插图

验收,而不是「看起来正常了」

清理完成后的验收要可重复、可留档,别只看首页能不能打开。建议按下面五项逐项过一遍,全部通过再解除维护模式:

  • 核心校验值:比对命令退出码为 0,输出中没有 File doesn’t verify 行
  • 文件台账:对照 /backup/files-before.txt 与当前清单,确认删除的文件都有记录、没有新的未知文件
  • 数据库:管理员列表只有你认识的账号,wp_options 里没有新增的可疑自动加载项
  • 外发行为:查看访问日志里 POST /wp-admin/admin-ajax.php 与 wp-cron.php 的请求量是否回到日常水平
  • 搜索引擎侧:在 Search Console 查看「安全性问题」与「手动操作」,确认无「站点被入侵」标记后再申请重新抓取

若在另一台机器上保留过干净副本,可以用 rsync 的校验模式逐文件比对,思路与 rsync 增量同步的一致性校验 一致:先用 -c 按内容生成差异,再决定恢复哪些目录,比按时间戳判断准确。

什么时候该重建,而不是继续清理

清理适合「核心文件被改、后门数量有限、有干净备份可回滚」的情形。遇到下面三种情况,继续修补的性价比会明显下降:

  • 差异清单里被改的核心文件超过几十个,且分散在 wp-admin、wp-includes 多个目录,说明攻击者已获得较长时间的文件写入权限
  • 按上面流程清理两次后,一周内又出现新的后门或陌生管理员,属于持久化没清干净,或者存在未被发现的入侵入口
  • 服务器上同时跑着多个站点,无法确认感染范围,单个站点清理无法阻止横向写入

这时更稳妥的做法是新建一台干净环境,只把 wp-content/uploads、wp-config.php 与数据库按需迁移,再装回确认干净的主题与插件。选环境时我们建议优先考虑带快照、资源独立的产品,例如 VPS(虚拟专用服务器)主机,可以自行决定快照时点和系统版本,出事时回滚到已知干净状态,而不是在故障机器上反复排查。Hostease 的运维经验也印证了同一点:备份与快照的价值,取决于事故当天能不能真的被用上。

总结:把不可信变成可核对

挂马处理真正难的不是删文件,而是证明清理彻底。校验值比对给出客观起点,时间窗排查补上 wp-content 的空白,定向重装把核心文件恢复到官方状态,密钥重置切断残留登录态,最后用五项验收给出可留档的结论。整套流程不需要凭感觉判断「应该没事了」。

如果你正在处理这类事故,建议先把现场备份做完再动手,之后每一步都留下一份清单;下次再遇到类似情况,这份清单会直接告诉你感染范围和复发点。

发表评论