0x0 背景介绍
WordPress的FS Registration Password插件在1.0.1及之前的所有版本中存在权限提升漏洞,可能导致账户被接管。该漏洞源于插件在更新用户密码前未正确验证用户身份,使得未经认证的攻击者能够修改任意用户(包括管理员)的密码,并利用此漏洞获取账户访问权限。
0x1 环境搭建
1、Ubuntu24+Docker搭建配置
- 保存
install.sh,并赋予执行权限chmod +x install.sh
bash
#!/bin/bash
if ! command -v unzip &> /dev/null; then
apt update && apt install -y unzip wget curl
fi
echo "[*] 阶段1/5:创建漏洞复现目录..."
mkdir -p wp-fs-pw-vuln && cd wp-fs-pw-vuln || { echo "[x] 创建目录失败"; exit 1; }
echo "[+] 工作目录: $(pwd)"
echo "[*] 阶段2/5:生成 docker-compose.yml..."
cat > docker-compose.yml <<EOF
services:
db:
image: mysql:8.0
container_name: fs-pw-db
environment:
MYSQL_ROOT_PASSWORD: vuln-root-pass
MYSQL_DATABASE: wordpress
MYSQL_USER: wpuser
MYSQL_PASSWORD: vuln-user-pass
volumes:
- db_data:/var/lib/mysql
command: --default-authentication-plugin=mysql_native_password
wordpress:
image: wordpress:6.5-php8.2-apache
container_name: fs-pw-wp
ports:
- "8087:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wpuser
WORDPRESS_DB_PASSWORD: vuln-user-pass
WORDPRESS_DB_NAME: wordpress
volumes:
- wp_plugins:/var/www/html/wp-content/plugins
depends_on:
- db
volumes:
db_data:
wp_plugins:
EOF
echo "[*] 阶段3/5:启动 Docker 环境..."
docker compose up -d
echo "[*] 等待服务启动(约60秒)..."
for i in {1..12}; do
echo -n "."
sleep 5
done
echo -e "\n[+] 服务启动完成"
echo "[*] 等待 WordPress 安装页面就绪..."
until [ "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8087/wp-admin/install.php)" = "200" ]; do
sleep 2
done
echo "[+] WordPress 已就绪"
echo "[*] 阶段4/5:下载并部署 FS Registration Password v1.0.1..."
PLUGIN_URL="https://downloads.wordpress.org/plugin/registration-password.1.0.1.zip"
wget -q "$PLUGIN_URL" -O registration-password.zip || {
echo "[-] 插件下载失败"
exit 1
}
unzip -q registration-password.zip
# 直接复制解压出的 registration-password 目录到容器
if [ ! -d "registration-password" ]; then
echo "[-] 解压后未找到 registration-password 目录"
exit 1
fi
# 复制到容器(使用明确的 container_name)
docker cp registration-password fs-pw-wp:/var/www/html/wp-content/plugins/
# 清理
rm -rf registration-password registration-password.zip
# 验证
if docker exec fs-pw-wp test -f /var/www/html/wp-content/plugins/registration-password/fs-registration-password.php; then
echo "[+] 插件部署成功!"
else
echo "[-] 插件部署失败:主文件 fs-registration-password.php 未找到"
exit 1
fi
echo "=============================================="
echo " CVE-2025-15001 漏洞环境部署完成!"
echo " - 访问站点: http://localhost:8087"
echo " - 站点标题: CVE-2025-15001"
echo " - 用户名: admin"
echo " - 密码: 默认强密码"
echo " - 电子邮箱: cve-2025-15001@local.test"
echo " - 登录后台 → 插件 → 启用 'Registration Password'"
echo "=============================================="
0x2 漏洞复现
1、Python检查
bash
https://github.com/Kai-One001/cve-/blob/main/WordPress_FS_%20CVE-2025-15001.py
- 依照管理,没有直接修改密码

2、手动复现步骤
-
找回密码接口,发送自定义KEY

-
请求重置链接

-
利用返回的
Set-Cookie,成功请求到重置密码接口

-
其实发送自定义KEY后,浏览器访问链接也可以,自动跳转(KEY就是设置的密码)
html
http://192.168.119.131:8087/wp-login.php?action=rp&key=22222&login=admin

- 验证成功修改密码,并正常登录

3、复现流量特征 (PCAP)
-
自定义KEY找回密码

-
请求重置密码链接

-
成功访问重置密码页面

0x3 漏洞原理分析
1、入口文件分析
- 插件代码简单,可以直接从入口分析
php
#fs-registration-password.php
<?php
/**
* Plugin Name: Registration Password
* Plugin URI: https://github.com/fsylum/fs-registration-password
* Description: Allow users to set their own password during site registration
* Version: 1.0.1
* Author: Firdaus Zahari
* Author URI: https://fsylum.net
* Requires at least: 5.6
* Requires PHP: 7.3
*/
require __DIR__ . '/vendor/autoload.php';
define('FSRP_PLUGIN_URL', untrailingslashit(plugin_dir_url(__FILE__)));
define('FSRP_PLUGIN_PATH', untrailingslashit(plugin_dir_path(__FILE__)));
define('FSRP_PLUGIN_BASENAME', plugin_basename(__FILE__));
define('FSRP_PLUGIN_VERSION', '1.0.1');
$app = new Fsylum\RegistrationPassword\App;
$app->addService(new Fsylum\RegistrationPassword\WP\Auth);
// Finally run it
$app->run();
- 加载自动加载器;
- 初始化
App容器; - 注册一个服务:
Fsylum\RegistrationPassword\WP\Auth; - 调用
$app->run()启动所有服务,关注Auth服务注册了哪些WordPress钩子
2、服务启动逻辑:App::run()
php
// src/App.php
<?php
namespace Fsylum\RegistrationPassword;
use Fsylum\RegistrationPassword\Contracts\Runnable;
class App
{
protected $services = [];
public function addService(Runnable $service)
{
$this->services[] = $service;
}
public function run()
{
foreach ($this->services as $service) {
$service->run();
}
}
}
- 遍历所有已注册服务,调用其
run()方法 - 当前只有一个服务:
Auth,那么直接进入Auth::run()
3、钩子注册分析
php
// src/WP/Auth.php
public function run()
{
add_action('login_enqueue_scripts', [$this, 'loadUserProfileJs']);
add_action('register_form', [$this, 'addPasswordFields']);
add_filter('registration_errors', [$this, 'validatePassword']);
add_filter('random_password', [$this, 'setUserPassword']); // 关键钩子
add_filter('wp_new_user_notification_email', [$this, 'modifyEmailNotification'], 10, 2);
}
random_password全局过滤器,任何调用wp_generate_password()的地方都会触发- 不仅用于新用户注册,还用于:
密码重置密钥生成(retrieve_password())用户导入、CLI 脚本、插件兼容等场景
4、核心逻辑函数
- 继续查看
setUserPassword()方法。
php
// src/WP/Auth.php
public function setUserPassword($password)
{
if (!isset($_POST['fs_is_password_for_registration']) ) {
return $password;
}
if (sanitize_text_field($_POST['fs_is_password_for_registration']) !== 'yes') {
return $password;
}
return $_POST['pass1'];
}
- 检查是否存在
fs_is_password_for_registration字段, 若不存在,返回原密码(安全路径)。 - 检查该字段值是否为
yes, 使用sanitize_text_field()处理,但该函数仅移除无效字符,不影响yes判断。 - 若满足条件,直接返回
$_POST['pass1'], 未做任何类型校验、长度限制、转义或上下文判断。
思考、疑问:
这个函数是否只应在"注册"时生效?
bash
从函数命名和上下文看,设计意图确实是仅用于注册流程;
但代码中没有任何机制确保这一点:
未检查当前请求是否为 action=register;
未验证 nonce;
未判断用户状态(如是否已登录);
未限制 HTTP 方法或路径。
只要任意请求携带这两个 POST 参数,即可触发覆盖行为。
假设下插件作者原本思想:
- 新用户注册(预期场景)
bash
路径:/wp-login.php?action=register
流程:
用户填写表单(含 pass1, pass2, fs_is_password_for_registration=yes);
WordPress 调用 wp_create_user() → 内部调用 wp_generate_password();
apply_filters('random_password', ...) → 触发 setUserPassword();
使用用户输入的密码创建账户。
- 盗刷账户(意外场景)
bash
路径:/wp-login.php?action=lostpassword
流程:
用户提交用户名;
WordPress 调用 retrieve_password();
内部调用 wp_generate_password(20, false) 生成 重置密钥(reset key);
apply_filters('random_password', $key) → 同样触发 setUserPassword();
若请求中携带 fs_is_password_for_registration=yes 和 pass1=xxx,则:
原本的随机密钥被替换为 xxx;
该值被哈希后存入数据库;
邮件中包含明文 key=xxx。
5、漏洞根因总结
bash
入口无害:插件初始化逻辑正常;
问题在于钩子注册过宽:将仅适用于注册场景的逻辑,挂载到全局过滤器 random_password;
场景判断缺失:setUserPassword() 仅依赖一个可伪造的 POST 字段判断上下文,未绑定到特定 action 或权限状态;
数据流失控:用户输入 $_POST['pass1'] 被直接返回,无任何安全边界;
核心危害路径:在"忘记密码"流程中,重置密钥被攻击者控制,从而接管账户。
0x4 修复建议
修复方案
- 升级到最新版本:建议受影响的用户升级至
2.0.1以上版本:插件地址 - 临时防护措施:
关注异常请求日志 :关注异常的密码修改行为,尤其是来自非预期行为
启用 WAF/IPS 规则 :拦截异常请求/wp-login.php?action=lostpassword地址,拦截异常的密码修改请求。
加强输入验证:对所有涉及用户身份变更的操作,强制实施双重验证机制
免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。