Dungsocool/CVE-2025-7384
GitHub: Dungsocool/CVE-2025-7384
针对 WordPress 插件 contact-form-entries 的 PHP 对象注入到远程代码执行(RCE)漏洞的完整分析、复现环境与 POC。
Stars: 0 | Forks: 0
# CVE-2025-7384 — PHP 对象注入到 RCE
**插件:** Database for Contact Form 7 (contact-form-entries) ≤ 1.4.3
**CVSS:** 9.8 (严重)
**CWE:** CWE-502 — 不可信数据反序列化
**认证要求:** 无需认证 (未授权)
**影响:** 远程代码执行
## 目录
1. [漏洞概述](#1-vulnerability-overview)
2. [相关概念](#2-related-concepts)
3. [根本原因分析 (源代码 + 调试)](#3-root-cause-analysis--vulnerability-discovery-from-source-code)
4. [攻击链](#4-attack-chain)
5. [逐步复现 (POC)](#5-step-by-step-reproduction-poc)
6. [影响评估](#6-impact-assessment)
7. [修复措施](#7-remediation-measures)
## 1. 漏洞概述
“Database for Contact Form 7” 插件 (slug: `contact-form-entries`) 1.4.3 及以下版本包含一个 PHP 对象注入漏洞。当 WordPress 管理员在后台面板查看表单记录 (条目) 时,该插件直接对未经认证用户通过 Contact Form 7 提交的数据调用 `maybe_unserialize()` 函数,而没有控制允许实例化的类列表。
攻击者无需登录——他们只需提交一个常规的联系表单,同时将序列化的 PHP 对象插入任何表单字段中。此数据以原始形式存储在数据库中。当管理员打开并查看该条目时,反序列化函数将实例化攻击者选择的对象,触发 `__destruct()` 或 `__wakeup()` 等魔术方法 (magic methods) → 根据 WordPress 环境中可用的 POP gadget 执行任意行为。
| 属性 | 值 |
|---|---|
| CVE ID | CVE-2025-7384 |
| CVSS Score | 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`) |
## 2. 相关概念
### PHP 序列化 / 反序列化
PHP 使用 `serialize()` 将对象转换为结构化文本字符串,并使用 `unserialize()` 从该字符串还原对象。当 `unserialize()` 接收来自不可信源 (例如用户输入) 的数据时,攻击者可以构造属于此刻加载在 PHP 内存中的任何类的任意对象。
### PHP 魔术方法
在对象生命周期内 PHP 自动调用的特殊方法。在此上下文中最重要的有:
- `__wakeup()` — 在对象被反序列化时立即调用
- `__destruct()` — 在对象被销毁时调用 (超出作用域,或请求结束时)
- `__toString()` — 在对象被转换为字符串时调用
### POP 链 (面向属性编程)
一种将应用程序现有类中的多个魔术方法链接起来,以构造危险行为序列的技术。攻击者无需编写新代码——他们只需操纵现有对象的*属性*,以便在执行魔术方法时,执行开发人员非预期的操作。
### WordPress 中的 `maybe_unserialize()`
一个 WordPress Core 封装函数。它调用 `is_serialized()` 检查字符串是否为序列化数据——如果是,则调用 `unserialize()` 还原对象。问题在于:此函数**不**传递 `allowed_classes` 参数 (自 PHP 7.0 起可用) 来限制允许实例化的类。
## 3. 根本原因分析 — 从源代码发现漏洞
### 步骤 1:寻找 Sink 点 (Sink Hunting)
首先 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` 变量来源于哪里?如果它来自未经筛选的用户输入 → 这就是一个漏洞。
### 步骤 2:逆向追踪 — 数据从哪里来?
查找 `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` 内部的数据从何而来?谁写入了它?
### 步骤 3:寻找数据写入点 (Source)
从步骤 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 标签**,因此会完全不受阻碍地通过。
### 步骤 4:结论 — 确认漏洞
至此,完整的流程已经建立:
```
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
```
### 步骤 5:调试器验证 (Xdebug)
为了获得直观证明,在 `data.php` 的第 545 行使用 Xdebug 设置断点。在通过表单注入 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 从表单 → 数据库 → 反序列化函数畅通无阻
**执行行:**
- 第 545 行: `$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()`。
## 4. 攻击链
攻击链由 5 个阶段组成。攻击者只需执行阶段 1 (提交表单)。阶段 2-5 在管理员查看条目后自动发生。
### 阶段 1 — 注入 Payload (未授权)
攻击者提交一个 CF7 表单,并在消息字段中包含序列化的 PHP 对象。
- Endpoint: `POST /wp-json/contact-form-7/v1/contact-forms/{id}/feedback`
- 字段 `your-message` 包含: `O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;}`
- 插件将 payload 存入 `wp_vxcf_leads_detail` 表 — `sanitize_text_field()` 不会拦截序列化字符串
### 阶段 2 — 触发反序列化 (等待管理员)
管理员打开 Contact Form Entries 页面 → 查看条目详情 → 插件调用 `verify_val()` → `maybe_unserialize()`。
- PHP 实例化 `VulnerableFileHandler` 对象,其中 `file_path = "/var/www/html/wp-config.php"` 且 `cleanup = true`
- 当请求结束时,PHP 垃圾回收机制调用 `__destruct()` → `unlink("/var/www/html/wp-config.php")`
### 阶段 3 — 任意文件删除
`wp-config.php` 文件被删除 → WordPress 失去数据库连接。
- 访问 `http://target/` → 自动重定向至 `/wp-admin/setup-config.php` (初始设置界面)
- WordPress 将该站点视为已卸载
### 阶段 4 — WordPress 重新安装
攻击者利用已知的 (或爆破出的) 数据库凭据重新安装 WordPress。
- 创建由攻击者控制的新管理员账户
- 使用完整的管理员权限登录控制台
### 阶段 5 — 远程代码执行
安装包含 webshell 的插件 → 执行任意系统命令。
- 管理员控制台 → 插件 → 安装新插件 → 上传包含 PHP webshell 的 ZIP 文件
- 访问 webshell URL: `/wp-content/plugins/shell/shell.php?cmd=id`
- 输出: `uid=33(www-data) gid=33(www-data)` → RCE 完成
## 5. 逐步复现 (POC)
### 5.1 环境搭建
启动包含 WordPress + 漏洞插件的 Docker 实验环境:
```
cd CVE-2025-7384
docker-compose up --build -d
```
等待约 40 秒,直到日志显示 `LAB READY`。访问 `http://localhost:8181` 验证 WordPress 是否正在运行。
### 5.2 识别注入点
从第 3 节的源代码分析可知:
- **Sink** 位于 `data.php:545` — 对表单字段值进行 `maybe_unserialize()`
- **Source** 是表 `wp_vxcf_leads_detail` — 数据来自 CF7 表单
- **过滤** 仅依赖 `sanitize_text_field()` — 不会拦截序列化字符串
→ 结论:只需将序列化的 PHP 对象提交到 CF7 表单的**任何字段**中。选择 `your-message` 是因为它是一个 textarea,可接受长字符串,并且格式验证较少 (不像 `your-email` 需要电子邮件格式)。
### 5.3 通过联系表单注入 Payload
访问 `http://localhost:8181/contact/`,按如下方式填写表单:
| 字段 | 值 |
|--------|---------|
| Your name | dung |
| Your email | dung@twtt.com |
| 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()`
点击 **Submit**。表单显示邮件发送错误消息 (或成功) — 这无关紧要,因为 `contact-form-entries` 插件在发送邮件之前已经将所有数据保存到数据库中。
### 5.4 触发反序列化 — 管理员查看条目
登录 `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")`。
### 5.5 确认任意文件删除
在浏览器中导航至 `http://localhost:8181/` → WordPress 重定向至 `/wp-admin/setup-config.php` 页面 (初始设置界面) → `wp-config.php` 文件成功删除。

### 5.6 提权至 RCE
随着 `wp-config.php` 被删除,WordPress 还原到未安装状态。攻击者步骤:
**步骤 1 — 重新安装:**
访问 `http://localhost:8181/wp-admin/setup-config.php` → 输入数据库凭据:
| 字段 | 值 |
|--------|---------|
| Database Name | wordpress |
| Username | wpuser |
| Password | wppass |
| Database Host | db |
| Table Prefix | wp_ |
点击 Submit → 运行安装 → 创建由攻击者控制的新管理员账户。
**步骤 2 — 上传 Webshell:**
登录管理员控制台 → **Plugins → Add New → Upload Plugin** → 上传 `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` → **远程代码执行完成**
## 6. 影响评估
| CVSS 指标 | 值 | 解释 |
|---|---|---|
| Attack Vector | 网络 | 通过 HTTP 利用,无需物理访问 |
| Attack Complexity | 低 | 仅需发送 1 个包含 payload 的 POST 请求 |
| Privileges Required | 无 | 无需认证 — CF7 表单对公众开放 |
| User Interaction | 无* | 管理员在日常工作流程中查看条目 |
| Confidentiality | 高 | RCE 允许读取服务器上的任何文件 |
| Integrity | 高 | RCE 允许写入/修改任何文件 |
| Availability | 高 | 删除 wp-config.php 导致整个网站崩溃 |
*User Interaction:NVD 将其评为无,因为管理员查看表单条目属于预期行为,而非异常的用户交互。
### 现实中的影响范围
- 插件 “Database for Contact Form 7” 在 wordpress.org 上有超过 **100,000+ 个活跃安装**
- 任何运行此插件版本 ≤ 1.4.3 并结合使用 Contact Form 7 的 WordPress 站点都存在漏洞
- 攻击者不需要任何先验信息 — 只需识别出站点正在使用 Contact Form 7 (通过 HTML 源代码很容易检测到)
- Payload 会持久化存储在数据库中,使得攻击在条目被删除之前持续有效
## 7. 修复措施
### 对于插件开发者
1. **不要对用户提供的数据使用 `maybe_unserialize()`。** 当需要结构化数据存储时,请改用 `json_decode()`。
2. 如果严格需要进行反序列化,请提供 `allowed_classes: false` 选项 (PHP 7.0+):
```
$data = unserialize($string, ['allowed_classes' => false]);
```
这可以防止 PHP 实例化任何对象 — 仅允许标量类型和数组。
3. 在存储层验证输入数据:如果表单字段应仅包含纯文本,则拒绝任何匹配模式 `/^[OaCis]:\d+/` (序列化数据的标志) 的值。
### 对于 WordPress 管理员
1. **立即更新插件** 至 1.4.4 或更高版本
2. 检查 `wp_vxcf_leads_detail` 数据库表,看是否包含匹配 `O:XX:"ClassName":` 格式字符串的条目 — 存在此类条目表明曾遭受攻击尝试
3. 确保 `wp-config.php` 具有严格的文件权限 (440 或 400) — 降低被 Web 服务器进程删除的概率
4. 部署配置了规则的 WAF (Web Application Firewall) 以检测 POST 数据中的序列化 PHP 对象
### 补丁对比 (参考)
```
// 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分析, Go语言工具, PHP反序列化, Web安全, 文件完整性监控, 编程工具, 蓝队分析, 请求拦截, 远程代码执行