Migration, Backup, Staging – WPvivid Backup & Migration <= 0.9.128 - Authenticated (Admin+) Arbitrary Directory Deletion
Description
The Migration, Backup, Staging – WPvivid Backup & Migration plugin for WordPress is vulnerable to arbitrary directory deletion due to insufficient file path validation in the delete_cancel_staging_site() function in all versions up to, and including, 0.9.128. This makes it possible for authenticated attackers, with Administrator-level access and above, to delete arbitrary folders on the server, which leads to a loss of data.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:LTechnical Details
<=0.9.128What Changed in the Fix
Changes introduced in v0.9.129
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2025-12656 (Arbitrary Directory Deletion) ## 1. Vulnerability Summary **CVE-2025-12656** is an arbitrary directory deletion vulnerability in the **WPvivid Backup & Migration** plugin (<= 0.9.128). The vulnerability exists within the `delete_cancel_staging_site()` f…
Show full research plan
Exploitation Research Plan: CVE-2025-12656 (Arbitrary Directory Deletion)
1. Vulnerability Summary
CVE-2025-12656 is an arbitrary directory deletion vulnerability in the WPvivid Backup & Migration plugin (<= 0.9.128). The vulnerability exists within the delete_cancel_staging_site() function in includes/staging/class-wpvivid-staging.php. Due to a lack of sufficient path validation, an authenticated user with Administrator-level privileges can provide a manipulated file path that causes the plugin to recursively delete an arbitrary folder on the server. Although it requires Admin privileges, it represents a significant data loss risk and can be used to disable security plugins or delete critical system directories.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - AJAX Action:
wpvivid_delete_cancel_staging_site(inferred based on function name) - HTTP Method: POST
- Vulnerable Parameter:
path(inferred) ordir(inferred) - Authentication: Required (Administrator)
- Preconditions: The plugin must be active, and the attacker must have valid administrator credentials.
3. Code Flow
- Registration: The
WPvivid_Staging_Freeclass constructor (inincludes/staging/class-wpvivid-staging.php) calls$this->load_ajax(). - AJAX Handler: Inside
load_ajax(), the plugin registers the callback:add_action('wp_ajax_wpvivid_delete_cancel_staging_site', array($this, 'delete_cancel_staging_site'));(inferred registration pattern). - Vulnerable Function: The
delete_cancel_staging_site()function is triggered. - Input Extraction: The function retrieves a directory path from the user input (e.g.,
$_POST['path']). - Insufficient Validation: The function fails to check if the provided path is contained within the authorized staging directory (e.g., it does not check if the path starts with
WPVIVID_STAGING_DIRor similar). - Sink: The path is passed to a recursive deletion routine, likely
WPvivid_Staging_Copy_Files_Ex::delete_dir($path)or a standard PHPrmdir()wrapper, which deletes the entire directory tree at that location.
4. Nonce Acquisition Strategy
The plugin likely protects staging operations with a WordPress nonce.
- Identify Script Localization: The function
enqueue_scriptsinclass-wpvivid-staging.php(Line 52) enqueues scripts that handle the staging UI. This is where the nonce is typically localized. - Create Trigger Content: To ensure the staging scripts and nonces are loaded, create a page that includes any WPvivid staging-related shortcode (if applicable) or simply navigate to the plugin's staging page.
wp post create --post_type=page --post_status=publish --post_content='[wpvivid_staging_placeholder]'(inferred shortcode)
- Navigate and Extract:
- Login as Administrator using
browser_navigate. - Navigate to the WPvivid Staging page:
/wp-admin/admin.php?page=wpvivid-staging. - JS Variable: Look for the localized object, likely
wpvivid_staging_varsorwpvivid_ajax_object. - Execution:
browser_eval("wpvivid_staging_vars.nonce")(inferred name).
- Login as Administrator using
5. Exploitation Strategy
The goal is to delete a non-staging directory, such as a dummy folder in uploads.
- Step 1: Prepare a target directory.
- Step 2: Obtain a valid Admin session and Nonce.
- Step 3: Construct the malicious AJAX request.
Request Details:
- URL:
https://<target>/wp-admin/admin-ajax.php - Headers:
Content-Type: application/x-www-form-urlencodedCookie: <Admin Cookies>
- Payload:
(Note: Theaction=wpvivid_delete_cancel_staging_site&path=../../uploads/target_to_delete&_wpnonce=<NONCE>pathmay need to be an absolute path if the plugin usesABSPATH, or relative if it uses the current working directory).
6. Test Data Setup
- Install Plugin: Ensure WPvivid version 0.9.128 is installed and active.
- Create Admin User:
wp user create exploit_admin admin@example.com --role=administrator --user_pass=password123 - Create Target Directory:
mkdir -p /var/www/html/wp-content/uploads/delete_me_test/touch /var/www/html/wp-content/uploads/delete_me_test/evidence.txt
7. Expected Results
- HTTP Response: The server should return a JSON success response (e.g.,
{"result":"success"}) or a1(standard WP AJAX success). - Filesystem Impact: The directory
/var/www/html/wp-content/uploads/delete_me_test/and its contents should be completely removed from the server.
8. Verification Steps
After sending the HTTP request, verify the deletion using WP-CLI:
# Check if the directory still exists
if [ ! -d "/var/www/html/wp-content/uploads/delete_me_test/" ]; then
echo "Exploit Successful: Directory deleted."
else
echo "Exploit Failed: Directory still exists."
fi
9. Alternative Approaches
- Absolute Path Injection: If relative pathing (
../) is filtered but the validation is still weak, try providing the full absolute path retrieved via other leaks (e.g.,phpinfoor error logs):/var/www/html/wp-content/plugins/akismet/. - Different AJAX Action: If
wpvivid_delete_cancel_staging_siteis not the correct action name, checkclass-wpvivid-staging-sites-list.phpfor actions related to "removing" or "cleaning up" staging sites, such aswpvivid_delete_staging_site. - Bypass via Directory Traversal: If the plugin prepends a base path, use traversal sequences:
path=/wp-content/wpvividbackups/staging/../../../wp-content/plugins/some-plugin.
Summary
The WPvivid Backup & Migration plugin for WordPress is vulnerable to arbitrary directory deletion in versions up to 0.9.128. This occurs because the `delete_cancel_staging_site()` function lacks sufficient validation to ensure that the directory being deleted is actually a staging site, allowing an authenticated administrator to recursively delete any folder on the server.
Vulnerable Code
/* includes/staging/class-wpvivid-staging.php */ public function delete_cancel_staging_site(){ global $wpvivid_plugin; check_ajax_referer( 'wpvivid_ajax', 'nonce' ); // ... [retrieval of site_path from staging info] ... if (!empty($site_path)) { $home_path = untrailingslashit(ABSPATH); if ($home_path != $site_path) { if (file_exists($site_path)) { if (!class_exists('WP_Filesystem_Base')) include_once(ABSPATH . '/wp-admin/includes/class-wp-filesystem-base.php'); if (!class_exists('WP_Filesystem_Direct')) include_once(ABSPATH . '/wp-admin/includes/class-wp-filesystem-direct.php'); $wp_filesystem = new WP_Filesystem_Direct(array()); WPvivid_Staging_Copy_Files_Ex::delete_dir($site_path); } } }
Security Fix
@@ -1264,6 +1264,126 @@ } } + private function wpvivid_normalize_delete_path($path, $use_realpath = true) + { + if (empty($path) || !is_string($path)) { + return false; + } + + $path = wp_normalize_path($path); + $path = untrailingslashit($path); + if ($use_realpath) { + $real_path = realpath($path); + if ($real_path !== false) { + $path = wp_normalize_path($real_path); + $path = untrailingslashit($path); + } + } + + if ((defined('PHP_OS_FAMILY') && PHP_OS_FAMILY === 'Windows') || DIRECTORY_SEPARATOR === '\\') { + $path = strtolower($path); + } + + return $path; + } + + private function wpvivid_get_cancel_staging_cleanup($site_path) + { + $normalized_path = $this->wpvivid_normalize_delete_path($site_path, false); + if ($normalized_path === false) { + return false; + } + + $cleanup = get_transient('wpvivid_cancel_staging_cleanup_' . md5($normalized_path)); + if (!is_array($cleanup) || empty($cleanup['staging_path'])) { + return false; + } + + $cleanup_path = $this->wpvivid_normalize_delete_path($cleanup['staging_path'], false); + $delete_path = $this->wpvivid_normalize_delete_path($site_path, false); + if ($cleanup_path === false || $delete_path === false || $cleanup_path !== $delete_path) { + return false; + } + + return $cleanup; + } + + private function wpvivid_is_allowed_staging_delete_path($site_path) + { + if (empty($site_path) || !is_string($site_path)) { + return false; + } + + if (!file_exists($site_path) || !is_dir($site_path)) { + return false; + } + + $real_site_path = $this->wpvivid_normalize_delete_path($site_path, true); + if ($real_site_path === false) { + return false; + } + + $real_site_path = wp_normalize_path(untrailingslashit($real_site_path)); + + $protected_paths = array( + ABSPATH, + WP_CONTENT_DIR, + WP_PLUGIN_DIR, + get_theme_root(), + ABSPATH . 'wp-admin', + ABSPATH . 'wp-includes' + ); + + $upload_dir = wp_get_upload_dir(); + if (!empty($upload_dir['basedir'])) { + $protected_paths[] = $upload_dir['basedir']; + } + + foreach ($protected_paths as $protected_path) { + $real_protected_path = $this->wpvivid_normalize_delete_path($protected_path, true); + if ($real_protected_path === false) { + continue; + } + + if ($real_site_path === $real_protected_path) { + return false; + } + + if (strpos($real_protected_path . '/', $real_site_path . '/') === 0) { + return false; + } + } + + $tasks = get_option('wpvivid_staging_task_list', array()); + if (is_array($tasks)) { + foreach ($tasks as $task) { + $allowed_paths = array(); + + if (!empty($task['path']['des_path'])) { + $allowed_paths[] = $task['path']['des_path']; + } + + if (!empty($task['site']['path'])) { + $allowed_paths[] = $task['site']['path']; + } + + foreach ($allowed_paths as $allowed_path) { + $real_allowed_path = $this->wpvivid_normalize_delete_path($allowed_path, true); + if ($real_allowed_path !== false && $real_site_path === $real_allowed_path) { + return true; + } + } + } + } + + $cleanup = $this->wpvivid_get_cancel_staging_cleanup($site_path); + if ($cleanup !== false) { + return true; + } + + return false; + } + public function delete_cancel_staging_site(){ global $wpvivid_plugin; check_ajax_referer( 'wpvivid_ajax', 'nonce' ); @@ -1285,9 +1405,18 @@ $db_name = $staging_site_info['staging_additional_db_name']; $db_host = $staging_site_info['staging_additional_db_host']; if (!empty($site_path)) { - $home_path = untrailingslashit(ABSPATH); - if ($home_path != $site_path) { - if (file_exists($site_path)) { + if (file_exists($site_path)) { + if (!$this->wpvivid_is_allowed_staging_delete_path($site_path)) { + echo wp_json_encode(array( + 'result' => 'failed', + 'error' => 'Invalid staging path.' + )); + die(); + } + + $home_path = untrailingslashit(ABSPATH); + if ($home_path != $site_path) { + if (!class_exists('WP_Filesystem_Base')) include_once(ABSPATH . '/wp-admin/includes/class-wp-filesystem-base.php'); if (!class_exists('WP_Filesystem_Direct')) include_once(ABSPATH . '/wp-admin/includes/class-wp-filesystem-direct.php'); @@ -1311,6 +1440,11 @@ } } + $normalized_path = $this->wpvivid_normalize_delete_path($site_path, false); + if ($normalized_path !== false) { + delete_transient('wpvivid_cancel_staging_cleanup_' . md5($normalized_path)); + } + $ret['result'] = 'success'; echo wp_json_encode($ret); } @@ -1751,6 +1885,27 @@ $ret['staging_table_prefix']=$value['db_connect']['new_prefix']; } } + + if (!empty($ret['staging_path'])) { + $normalized_cleanup_path = $this->wpvivid_normalize_delete_path($ret['staging_path'], false); + + if ($normalized_cleanup_path !== false) { + set_transient( + 'wpvivid_cancel_staging_cleanup_' . md5($normalized_cleanup_path), + array( + 'staging_path' => $ret['staging_path'], + 'staging_table_prefix' => isset($ret['staging_table_prefix']) ? $ret['staging_table_prefix'] : '', + 'staging_additional_db' => isset($ret['staging_additional_db']) ? $ret['staging_additional_db'] : 0, + 'staging_additional_db_user' => isset($ret['staging_additional_db_user']) ? $ret['staging_additional_db_user'] : '', + 'staging_additional_db_pass' => isset($ret['staging_additional_db_pass']) ? $ret['staging_additional_db_pass'] : '', + 'staging_additional_db_name' => isset($ret['staging_additional_db_name']) ? $ret['staging_additional_db_name'] : '', + 'staging_additional_db_host' => isset($ret['staging_additional_db_host']) ? $ret['staging_additional_db_host'] : '' + ), + 10 * MINUTE_IN_SECONDS + ); + } + } + update_option('wpvivid_staging_task_cancel', false, 'no'); $b_delete=true; }
Exploit Outline
The exploit requires an authenticated user with Administrator privileges. 1. The attacker obtains a valid WordPress nonce for the 'wpvivid_ajax' action, typically found localized in the plugin's staging settings page. 2. The attacker triggers the 'wp_ajax_wpvivid_delete_cancel_staging_site' AJAX action. 3. In vulnerable versions, the plugin attempts to clean up staging sites by deleting the folder specified in the staging site configuration or transient data. 4. By manipulating the staging site path information (either through a previous configuration step or by influencing the input variables), the attacker can specify an arbitrary directory path (e.g., '/wp-content/plugins/'). 5. The plugin recursively deletes the target directory because it only checks if the path is strictly different from the site's home path (ABSPATH), allowing for a full loss of data for that directory.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.