Post Duplicator <= 3.0.10 - Authenticated (Contributor+) PHP Object Injection
Description
The Post Duplicator plugin for WordPress is vulnerable to PHP Object Injection in versions up to, and including, 3.0.10 via deserialization of untrusted input. This makes it possible for authenticated attackers, with contributor-level access and above, to inject a PHP Object. No known POP chain is present in the vulnerable software. If a POP chain is present via an additional plugin or theme installed on the target system, it could allow the attacker to delete arbitrary files, retrieve sensitive data, or execute code.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:HTechnical Details
<=3.0.10What Changed in the Fix
Changes introduced in v3.0.11
Source Code
WordPress.org SVN# Exploit Research Plan - CVE-2026-39474 ## 1. Vulnerability Summary The **Post Duplicator** plugin (<= 3.0.10) for WordPress is vulnerable to **PHP Object Injection** due to the unsafe use of `maybe_unserialize()` on post metadata within its REST API endpoints. The vulnerability exists in `inclu…
Show full research plan
Exploit Research Plan - CVE-2026-39474
1. Vulnerability Summary
The Post Duplicator plugin (<= 3.0.10) for WordPress is vulnerable to PHP Object Injection due to the unsafe use of maybe_unserialize() on post metadata within its REST API endpoints.
The vulnerability exists in includes/api.php within the get_post_data() function. This function iterates through all custom fields (metadata) associated with a post and calls maybe_unserialize() on any value that satisfies is_serialized(). An authenticated user with Contributor level access can create a post, attach a malicious serialized PHP object as metadata, and then trigger the deserialization by accessing the plugin's REST API endpoint for that post.
2. Attack Vector Analysis
- Endpoint:
GET /wp-json/post-duplicator/v1/post-data/(?P<id>\d+) - Vulnerable Parameter: The values of post metadata (retrieved via
get_post_custom()) for the post specified by theidparameter. - Authentication: Authenticated (Contributor+). The user must have permission to duplicate the post, which typically includes any post they have authored.
- Preconditions:
- The attacker must be able to add or edit post metadata (standard for Contributors on their own posts).
- The attacker must obtain a valid
wp_restnonce to interact with the WordPress REST API.
3. Code Flow
- Entry Point: The REST route
post-duplicator/v1/post-data/(?P<id>\d+)is registered inincludes/api.php(line 12) with the callbackMtphr\PostDuplicator\get_post_data. - Permission Check:
get_post_data_permissions(line 80) is called. It verifies the post exists and checksuser_can_duplicate($post). - Data Retrieval:
get_post_data(line 104) is executed. It callsget_post_custom($post_id)(line 143) to retrieve all metadata for the post. - Iteration: The code loops through each meta key and value (lines 146-156).
- Vulnerable Logic:
- At line 163, it checks:
if ( is_serialized( $value ) ). - If true, at line 165, it calls:
$unserialized = maybe_unserialize( $value );.
- At line 163, it checks:
- Sink:
maybe_unserialize()(a wrapper forunserialize()) processes the untrusted string from the database, triggering the PHP Object Injection. - Response: The code then checks
is_object($unserialized)(line 169) and, if true, sets thetypeto'object'and JSON-encodes the object for the response (line 171).
4. Nonce Acquisition Strategy
The REST API endpoint requires a wp_rest nonce for authenticated requests.
- Identify Script Loading: The plugin enqueues its duplication interface scripts on the post list page (
edit.php) for supported post types. - Access Admin Area: Log in as the Contributor and navigate to
/wp-admin/edit.php. - Extract Nonce: Use
browser_evalto extract the WordPress standard REST nonce from thewpApiSettingsobject:- JS Variable:
window.wpApiSettings.nonce - Alternative: If the plugin localizes its own nonce, it would likely be found in a variable like
window.postDuplicatorData.nonce(though standardwp_restis used by theregister_rest_routedefault).
- JS Variable:
5. Exploitation Strategy
- Setup Post: Create a post as the Contributor and add a meta field containing a serialized object.
- Obtain Nonce: Extract the
wp_restnonce from the WordPress dashboard. - Trigger Exploit: Send a GET request to the vulnerable REST endpoint.
Step-by-Step Execution:
- Login: Log in as a Contributor user.
- Inject Metadata: Use WP-CLI to add a serialized object to a post owned by the Contributor:
wp post meta add <POST_ID> "exploit_field" 'O:8:"stdClass":0:{}' - Capture Nonce:
- Navigate to
http://localhost:8080/wp-admin/edit.php. - Run
browser_eval("window.wpApiSettings.nonce").
- Navigate to
- Send Request:
- Use
http_requestto call the endpoint:- URL:
http://localhost:8080/wp-json/post-duplicator/v1/post-data/<POST_ID> - Headers:
X-WP-Nonce: <NONCE_VALUE> - Method:
GET
- URL:
- Use
- Analyze Response: Observe the
customMetaarray in the JSON response.
6. Test Data Setup
- User: Role
contributor, usernameexploit_user. - Post: A standard
postcreated byexploit_user. - Metadata:
- Key:
test_object_injection - Value:
O:8:"stdClass":0:{}(A simple standard class object).
- Key:
7. Expected Results
- Success Indicator: The REST API response (200 OK) will contain a JSON object. Inside the
customMetaarray, an entry fortest_object_injectionwill have:"type": "object""isSerialized": true"value": "{}"
- Verification: The fact that the
typereturned is"object"confirms thatmaybe_unserialize()successfully transformed the string'O:8:"stdClass":0:{}'into a PHP object. If it had failed or not run, the type would be"string".
8. Verification Steps
- Check Logs: Check the PHP error logs for any notices or errors if using a more complex (non-existent) class (e.g.,
O:15:"NonExistentClass":0:{}). - Response Inspection: Verify the JSON response structure:
{ "taxonomies": [...], "customMeta": [ { "key": "test_object_injection", "value": "{}", "type": "object", "isSerialized": true, "originalValue": "O:8:\"stdClass\":0:{}" } ] } - Database Confirmation: Use
wp post meta get <POST_ID> test_object_injectionto confirm the raw string in the database matches the injected payload.
9. Alternative Approaches
If the post-data endpoint is inaccessible, the post-full-data endpoint (also in api.php) follows a similar logic and can be used as a fallback:
- Endpoint:
GET /wp-json/post-duplicator/v1/post-full-data/(?P<id>\d+) - Logic: Calls
get_post_full_data, which likely also processes custom meta to provide a full preview of the post being duplicated.
Summary
The Post Duplicator plugin for WordPress is vulnerable to PHP Object Injection in versions up to 3.0.10. This occurs because the plugin's REST API endpoints unsafeley call `maybe_unserialize()` on post metadata, allowing authenticated attackers with Contributor-level access to trigger deserialization of malicious payloads stored in custom fields.
Vulnerable Code
// includes/api.php lines 143-176 $custom_fields = get_post_custom( $post_id ); $excluded_meta_keys = get_excluded_meta_keys(); foreach ( $custom_fields as $key => $values ) { // ... foreach ( $values as $value ) { // Detect data type $type = 'string'; $is_serialized = false; $original_value = $value; // Check if serialized if ( is_serialized( $value ) ) { $is_serialized = true; $unserialized = maybe_unserialize( $value ); if ( is_array( $unserialized ) ) { $type = 'array'; $value = wp_json_encode( $unserialized, JSON_PRETTY_PRINT ); } elseif ( is_object( $unserialized ) ) { $type = 'object'; $value = wp_json_encode( $unserialized, JSON_PRETTY_PRINT ); } else { $type = 'string'; } } // ...
Security Fix
@@ -162,15 +162,7 @@ // Check if serialized if ( is_serialized( $value ) ) { $is_serialized = true; - $unserialized = maybe_unserialize( $value ); - if ( is_array( $unserialized ) ) { - $type = 'array'; - $value = wp_json_encode( $unserialized, JSON_PRETTY_PRINT ); - } elseif ( is_object( $unserialized ) ) { - $type = 'object'; - $value = wp_json_encode( $unserialized, JSON_PRETTY_PRINT ); - } else { - $type = 'string'; - } + $type = 'serialized'; + $value = $value; } elseif ( is_numeric( $value ) ) {
Exploit Outline
1. An attacker logs into WordPress with at least Contributor-level permissions. 2. The attacker creates a new post or selects one they already own. 3. Using the standard WordPress interface or WP-CLI, the attacker adds a custom meta field (metadata) to the post containing a malicious serialized PHP object. 4. The attacker obtains a valid REST API nonce (standard `wp_rest` nonce) from the WordPress admin dashboard (e.g., from the `wpApiSettings` JavaScript object). 5. The attacker sends a GET request to the REST API endpoint `/wp-json/post-duplicator/v1/post-data/<POST_ID>` (or `/wp-json/post-duplicator/v1/post-full-data/<POST_ID>`), passing the nonce in the `X-WP-Nonce` header. 6. The plugin's `get_post_data` function retrieves the post's metadata and calls `maybe_unserialize()` on the malicious payload, triggering the PHP Object Injection.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.