Sploitus

Exploit for CVE-2026-82222

githubexploit Β· 2026-08-30

Exploit Code

README397 lines
## https://sploitus.com/exploit?id=9DDA56B5-0222-5425-A8DB-6D73151148C4
# CVE-2026-82222 β€” GiveWP Marker-Only RCE Validation Lab

Private, authorized security-research material for reproducing and validating
CVE-2026-82222 in an isolated Docker lab.

## Verdict

**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:

```text
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.

## Safety boundary

Use this repository only on systems you own or are explicitly authorized to
test.

The supplied PoC is intentionally constrained:

- It executes only `touch /tmp/CVE-2026-82222-RCE-GETBAG`.
- It offers no arbitrary-command option.
- It creates no shell, callback, persistence, or privilege escalation.
- It refuses non-loopback targets unless the operator supplies the explicit
  `--allow-authorized-non-loopback` flag.
- Docker publishes WordPress only on `127.0.0.1`.
- The Compose project name is derived from the checkout path so one clone cannot
  tear down another clone's containers or volumes.
- The lab uses pristine official plugin source; it does not instrument or patch
  the vulnerable target.

The HTTP test creates a disposable donor user, metadata, and GiveWP session
rows. Use the included reset command after testing.

## Quick start

Requirements:

- Docker with Compose v2
- Python 3.10 or later
- `curl`
- `unzip`
- `sha256sum` or `shasum`
- Network access to `downloads.wordpress.org` for official plugin archives

Run the complete vulnerable/patched matrix:

```bash
./lab verify
```

The command:

1. Downloads GiveWP 4.16.5.1 and 4.16.7.2 from WordPress.org.
2. Verifies both SHA-256 hashes.
3. Builds a fresh loopback-only WordPress 6.6.2/PHP 8.1 lab with normal
   WordPress registration explicitly disabled.
4. Tests pristine 4.16.5.1 and requires marker creation by the web user.
5. Builds a second fresh lab with pristine 4.16.7.2.
6. Requires the HTTP marker to remain absent.
7. Bypasses ingress in a direct control and confirms the patched
   `ProviderForwarder` terminal also rejects the string callable.

The patched lab remains running at the end. Remove it with:

```bash
./lab reset
```

To use another loopback port:

```bash
LAB_PORT=8099 ./lab verify
```

## Expected evidence

The vulnerable control must end with concrete terminal evidence:

```text
[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:

```text
[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.

## Manual lab lifecycle

Start and test the vulnerable release:

```bash
./lab start vulnerable
./lab test
```

Start and test the patched release:

```bash
./lab start patched
./lab test
```

Inspect current state:

```bash
./lab status
```

Remove containers, volumes, test users, sessions, and marker state:

```bash
./lab reset
```

Cached plugin ZIPs and extracted assets are preserved for faster reruns. Remove
those exact generated assets as well with:

```bash
./lab reset --purge-assets
```

## Affected and tested versions

| 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:

- [Patchstack advisory](https://patchstack.com/articles/unauthenticated-php-object-injection-to-remote-code-execution-on-givewp/)
- [CVE-2026-82222](https://vulners.com/cve/CVE-2026-82222)
- [GiveWP hardening commit](https://github.com/impress-org/givewp/commit/845acaa76feca99cc4c0eddfe2af908f8b69d1ac)
- [Official GiveWP plugin page](https://wordpress.org/plugins/give/)

## Root cause

The exploit combines several behaviors:

1. GiveWP 4.16.5.1 exposes a registration action that creates and authenticates
   a low-privilege donor even when normal WordPress registration is disabled.
2. That user can persist serialized data in their own name metadata.
3. `Give\Helpers\Utils::maybeSafeUnserialize()` uses
   `allowed_classes => false`, producing `__PHP_Incomplete_Class`, but a later
   serialization preserves the original class names and properties.
4. The graph is stored in a GiveWP purchase session.
5. A later unrestricted `maybe_unserialize()` revives the shipped classes.
6. Automatic destruction enters a complete POP chain to `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.

## Complete POP chain

```text
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:

```text
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.

## What earlier attempts missed

The rejected candidate graph used:

```text
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:

```php
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:

- `foreach` invokes `getIterator()`.
- `getIterator()` reaches `getBag($attributeName)`.
- Attacker-controlled `storage` is a `DonationFactory`.
- The undefined `getBag` invokes `ProviderForwarder::__call()`.
- `loadedProviders['getBag'] = 'system'` selects the callable.
- `attributeName` supplies the command argument.

## Source evidence

Relevant GiveWP 4.16.5.1 locations:

| Component | Location |
|---|---|
| Guarded initial unserialize | `src/Helpers/Utils.php:203-217,237-241` |
| Donation carrier | `includes/process-donation.php:157` |
| GiveWP session persistence | `includes/class-give-session.php:364-368,489-508` |
| Unrestricted session revival | `includes/class-give-session.php:347-350` |
| TCPDF destructor | `vendor/tecnickcom/tcpdf/tcpdf.php:2050-2052` |
| Attacker-controlled iteration | `vendor/tecnickcom/tcpdf/tcpdf.php:7885-7907` |
| Symfony iterator bridge | `vendor/vendor-prefixed/symfony/http-foundation/Session/Session.php:134-136` |
| Symfony `getBag` bridge | `vendor/vendor-prefixed/symfony/http-foundation/Session/Session.php:259-283` |
| Terminal callable | `src/TestData/Framework/ProviderForwarder.php:19-23` |
| GiveWP Composer autoloaders | `give.php:624-625` |

Official release hashes:

```text
give.4.16.5.1.zip
95fc6709b6ed284074bf09be096e28d6299fd4a103848c2b1c3bf8a5772cf98c

give.4.16.7.2.zip
c5bc98da2abb748c64d31a43679ff5f223092dfff6d214132a55dcbc948e91ae
```

The lab re-extracts each plugin from its verified ZIP before every fresh start,
copies it into WordPress without modification, and byte-compares the deployed
complete plugin file tree with the verified source. The three official Docker
base images are also pinned to their multi-architecture manifest digests.

## Why 4.16.7.2 blocks the chain

GiveWP 4.16.7.2 adds independent defenses across registration, serialized
input handling, session revival, data migration, and gadget hardening.

The terminal hardening requires the resolved object to implement the expected
provider contract:

```php
if ( ! $provider instanceof Contract\Provider ) {
    return null;
}
```

The attacker-controlled string `system` is therefore rejected. The direct
control in this repository bypasses the HTTP carrier and verifies that this
terminal defense alone leaves its marker absent.

## Checking a real deployment

A negative PoC result by itself does **not** prove a site is patched. A WAF,
different routing, disabled registration, missing legacy form, session
configuration, or disabled PHP command functions can all prevent the marker
on a still-vulnerable codebase.

Preferred validation procedure:

1. Record the deployed GiveWP version and source revision.
2. Back up or snapshot the WordPress site and database.
3. Restore that snapshot into an isolated staging environment.
4. Disable unnecessary outbound network access.
5. Run this marker-only verifier against the clone.
6. Update GiveWP to the latest supported release, with 4.16.7.2 as the minimum
   release containing the tested fixes.
7. Repeat the identical test and require marker absence.
8. Independently verify the installed plugin version and patched source.

Version check:

```bash
wp plugin get give --fields=name,status,version
```

## Remediation and incident review

Update GiveWP to the latest supported version. After updating:

- Clear PHP opcode caches where applicable.
- Confirm no older GiveWP copy remains active or web-accessible.
- Review unexpected WordPress or donor accounts.
- Search user metadata and GiveWP sessions for serialized `TCPDF`, Symfony
  `Session`, or `loadedProviders` graphs.
- Correlate registration, profile changes, donation requests, and subsequent
  HTTP 500 responses from the same session.
- Investigate unexpected web-server child processes or filesystem changes.

If an internet-exposed vulnerable installation contains object-injection
artifacts, treat it as a potential compromise rather than only updating the
plugin.

## Repository layout

```text
.
β”œβ”€β”€ .github/workflows/validate.yml
β”œβ”€β”€ .gitignore
β”œβ”€β”€ README.md
β”œβ”€β”€ SECURITY.md
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ lab
β”œβ”€β”€ poc
β”‚   β”œβ”€β”€ direct-pop-control.php
β”‚   └── poc.py
└── scripts
    └── fetch-assets.sh
```

Not committed:

```text
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
```

## Validation boundaries

| Claim | Status |
|---|---|
| Persistent PHP object-injection carrier | Proven |
| Revival of shipped classes | Proven |
| Complete stock POP chain | Proven |
| Fixed marker executed as the web user | Proven |
| Reproduction against pristine 4.16.5.1 | Proven |
| Negative controls against pristine 4.16.7.2 | Proven |
| Every intermediate affected release tested | Not tested |
| Root privilege | Not claimed |
| Container escape | Not tested |
| Host compromise | Not tested |

The proof standard is deliberately strict: only an observable command marker
through the unmodified stock chain is labeled RCE. HTTP acceptance,
serialization, exceptions, and detector hits are intermediate evidence.