CVE-2026-1357

Migration, Backup, Staging <= 0.9.123 - Unauthenticated Arbitrary File Upload

criticalUnrestricted Upload of File with Dangerous Type
9.8
CVSS Score
9.8
CVSS Score
critical
Severity
0.9.124
Patched in
1d
Time to patch

Description

The Migration, Backup, Staging – WPvivid Backup & Migration plugin for WordPress is vulnerable to Unauthenticated Arbitrary File Upload in versions up to and including 0.9.123. This is due to improper error handling in the RSA decryption process combined with a lack of path sanitization when writing uploaded files. When the plugin fails to decrypt a session key using openssl_private_decrypt(), it does not terminate execution and instead passes the boolean false value to the phpseclib library's AES cipher initialization. The library treats this false value as a string of null bytes, allowing an attacker to encrypt a malicious payload using a predictable null-byte key. Additionally, the plugin accepts filenames from the decrypted payload without sanitization, enabling directory traversal to escape the protected backup directory. This makes it possible for unauthenticated attackers to upload arbitrary PHP files to publicly accessible directories and achieve Remote Code Execution via the wpvivid_action=send_to_site parameter.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
High
Confidentiality
High
Integrity
High
Availability

Technical Details

Affected versions<=0.9.123
PublishedFebruary 10, 2026
Last updatedFebruary 11, 2026
Affected pluginwpvivid-backuprestore

What Changed in the Fix

Changes introduced in v0.9.124

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

php` is sometimes filtered. I'll use `<?php system($_GET['cmd']); ?>`. Final check on `decrypt_message` lengths: ```php $len = substr($message, 0, 3); // hexdec('001') = 1. Correct. $key = substr($message, 3, 1); // 'A'. Correct. $cipherlen = substr($message, 4, 16); // Correct,…

Show full research plan

phpis sometimes filtered. I'll use`.

Final check on `decrypt_message` lengths:
```php
$len = substr($message, 0, 3); // hexdec('001') = 1. Correct.
$key = substr($message, 3, 1); // 'A'. Correct.
$cipherlen = substr($message, 4, 16); // Correct, offset is 3 + len.
```
Actually:
`substr($message, 0, 3)` -> 0, 1, 2
`substr($message, 3, $len)` -> 3 to 3+$len-1
`substr($message, ($len + 3), 16)` -> 3+$len to 3+$len+15.
Correct.

Example with `len=1`:
`0-2`: `001`
`3-3`: `A`
`4-19`: `0000000000000040` (16 chars)
`20-...`: ciphertext.

Wait, `(1 + 19) = 20`. Correct.

The exploit uses `WPvivid_crypt::decrypt_message` which is unauthenticated.
The `WPvivid_Send_to_site::send_
Research Findings
Static analysis — not yet PoC-verified

Summary

WPvivid Backup & Migration is vulnerable to unauthenticated arbitrary file upload due to a failure in handling RSA decryption errors and a lack of filename sanitization. Attackers can exploit this by providing a malformed ciphertext that causes RSA decryption to return false, which is subsequently used as a null-byte AES key, and then utilizing directory traversal to upload PHP files to the web root.

Vulnerable Code

// includes/class-wpvivid-crypt.php

    public function decrypt_message($message)
    {
        $len = substr($message, 0, 3);
        $len = hexdec($len);
        $key = substr($message, 3, $len);

        $cipherlen = substr($message, ($len + 3), 16);
        $cipherlen = hexdec($cipherlen);

        $data = substr($message, ($len + 19), $cipherlen);
        $rsa = new Crypt_RSA();
        $rsa->loadKey($this->public_key);
        $key=$rsa->decrypt($key);
        $rij = new Crypt_Rijndael();
        $rij->setKey($key);
        return $rij->decrypt($data);
    }

---

// includes/customclass/class-wpvivid-send-to-site.php

                $dir=WPvivid_Setting::get_backupdir();

                $file_path=WP_CONTENT_DIR.DIRECTORY_SEPARATOR.$dir.DIRECTORY_SEPARATOR.str_replace('wpvivid','wpvivid_temp',$params['name']);

                if(!file_exists($file_path))
                {
                    $handle=fopen($file_path,'w');

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpvivid-backuprestore/0.9.123/includes/class-wpvivid-crypt.php /home/deploy/wp-safety.org/data/plugin-versions/wpvivid-backuprestore/0.9.124/includes/class-wpvivid-crypt.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpvivid-backuprestore/0.9.123/includes/class-wpvivid-crypt.php	2024-03-18 00:39:00.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpvivid-backuprestore/0.9.124/includes/class-wpvivid-crypt.php	2026-01-28 06:23:20.000000000 +0000
@@ -57,6 +57,10 @@
         $rsa = new Crypt_RSA();
         $rsa->loadKey($this->public_key);
         $key=$rsa->decrypt($key);
+        if ($key === false || empty($key))
+        {
+            return false;
+        }
         $rij = new Crypt_Rijndael();
         $rij->setKey($key);
         return $rij->decrypt($data);
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpvivid-backuprestore/0.9.123/includes/customclass/class-wpvivid-send-to-site.php /home/deploy/wp-safety.org/data/plugin-versions/wpvivid-backuprestore/0.9.124/includes/customclass/class-wpvivid-send-to-site.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpvivid-backuprestore/0.9.123/includes/customclass/class-wpvivid-send-to-site.php	2024-03-18 00:39:00.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpvivid-backuprestore/0.9.124/includes/customclass/class-wpvivid-send-to-site.php	2026-01-28 06:23:20.000000000 +0000
@@ -627,8 +627,18 @@
                 $wpvivid_plugin->wpvivid_log->WriteLog('start upload.','notice');
                 $dir=WPvivid_Setting::get_backupdir();
 
-                $file_path=WP_CONTENT_DIR.DIRECTORY_SEPARATOR.$dir.DIRECTORY_SEPARATOR.str_replace('wpvivid','wpvivid_temp',$params['name']);
-
+                $safe_name = basename($params['name']);
+                $safe_name = preg_replace('/[^a-zA-Z0-9._-]/', '', $safe_name);
+                $allowed_extensions = array('zip', 'gz', 'tar', 'sql');
+                $file_ext = strtolower(pathinfo($safe_name, PATHINFO_EXTENSION));
+                if (!in_array($file_ext, $allowed_extensions, true))
+                {
+                    $ret['result'] = WPVIVID_FAILED;
+                    $ret['error'] = 'Invalid file type - only backup files allowed.';
+                    echo wp_json_encode($ret);
+                    die();
+                }
+                $file_path=WP_CONTENT_DIR.DIRECTORY_SEPARATOR.$dir.DIRECTORY_SEPARATOR.str_replace('wpvivid', 'wpvivid_temp', $safe_name);
                 if(!file_exists($file_path))
                 {
                     $handle=fopen($file_path,'w');
@@ -663,8 +673,7 @@
                     if (md5_file($file_path) == $params['md5'])
                     {
                         $wpvivid_plugin->wpvivid_log->WriteLog('rename temp file:'.$file_path.' to new name:'.WP_CONTENT_DIR.DIRECTORY_SEPARATOR.$dir.DIRECTORY_SEPARATOR.$params['name'],'notice');
-                        rename($file_path,WP_CONTENT_DIR.DIRECTORY_SEPARATOR.$dir.DIRECTORY_SEPARATOR.$params['name']);
-
+                        rename($file_path,WP_CONTENT_DIR.DIRECTORY_SEPARATOR.$dir.DIRECTORY_SEPARATOR.$safe_name);
                         $ret['result']=WPVIVID_SUCCESS;
                         $ret['op']='finished';
                     } else {

Exploit Outline

The exploit targets the `wpvivid_action=send_to_site` parameter, which is processed globally in the `plugins_loaded` hook without authentication. 1. Attackers construct a binary message consisting of a length header, a dummy RSA-encrypted key, another length header, and AES-encrypted ciphertext. 2. By providing an invalid RSA-encrypted key, the plugin's `decrypt_message` function fails during `openssl_private_decrypt()`, which returns `false`. 3. The code continues execution and passes this `false` value to `Crypt_Rijndael::setKey()`. The phpseclib library interprets boolean `false` as a null-byte string key. 4. The attacker encrypts their payload using a predictable key composed of null bytes. 5. The payload is a JSON object containing a `name` parameter and file content. By using directory traversal in the `name` (e.g., `../../shell.php`), the attacker can bypass the intended backup directory. 6. The plugin writes the decrypted content to the traversal path, resulting in the creation of a PHP shell in a web-accessible directory.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.