The Exploit
An unauthenticated attacker can inject a serialized PHP object into the plugin's search-replace functionality by crafting a malicious staging site creation request. When the administrator triggers a search-replace operation on the staging database, the plugin deserializes the attacker's payload without class restrictions, instantiating arbitrary objects from a POP chain and achieving file deletion, data exfiltration, or remote code execution.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target-wordpress.local
Content-Type: application/x-www-form-urlencoded
action=bm_backup_migration_create_staging&
data=O:8:"GadgetClass":1:{s:4:"file";s:20:"/var/www/html/wp-config.php";}
When this request is processed and an administrator creates or manages the staging site, the recursive_unserialize_replace() function executes the attacker's serialized object. The response will either execute the gadget chain silently (if the POP chain is present in the WordPress installation), or an error log entry will reveal object instantiation. The side effect is immediate: a file specified in the gadget's property is deleted, or sensitive files are read and exfiltrated via error messages or timing channels.
What the Patch Did
Before
if (is_string($data) && is_serialized($data) && ($unserialized = @maybe_unserialize($data)) !== false) {
After
if (is_string($data) && is_serialized($data) && ($unserialized = @unserialize($data, ['allowed_classes' => ['stdClass']])) !== false) {
The patch adds an explicit allowed_classes constraint to the unserialize() call, restricting instantiation to only stdClass. The original code used maybe_unserialize(), which internally calls unserialize() with no class restrictions, allowing any serialized object in the input to be reconstructed in PHP memory. By explicitly passing ['allowed_classes' => ['stdClass']], the patched code ensures that even if an attacker supplies a serialized object of type WP_Hook, SplFileObject, or any other gadget-bearing class, PHP will reject the deserialization and return false. This implements a whitelist-based deserialization policy, a standard defense against object injection.
Root Cause
CWE-502: Deserialization of Untrusted Data.
The attacker-controlled serialized data enters the action=bm_backup_migration_create_staging AJAX handler (or similar staging-creation endpoint) as a POST parameter or cookie. This data flows directly into recursive_unserialize_replace() at line 46 without prior validation or sanitization. The function crosses a critical trust boundary by calling maybe_unserialize(), which reconstructs arbitrary PHP objects from the attacker's byte string. If a dangerous gadget chain exists in the WordPress codebase or an active plugin, the object's __wakeup() or __destruct() magic methods will execute attacker-supplied operations when the object is destroyed or reassigned. The vulnerability is amplified by the admin-context requirement: an administrator must visit the staging site creation or edit page, triggering the deserialization in their authenticated session.
Why It Works
The load-bearing line in the patch is 'allowed_classes' => ['stdClass']. Removing this constraint—even while keeping the explicit unserialize() call—would restore exploitability, because PHP would again reconstruct any serialized object. stdClass cannot define magic methods or carry executable code; it is a passive data container. The engineer included the full unserialize(..., [...]) syntax to make the intent explicit and future-proof the code against accidental removals of the options array. The check is_serialized($data) remains in the original form because it acts as a quick pre-filter to avoid calling unserialize() on obviously non-serialized strings, a minor performance optimization that does not affect security. The real defense is the class whitelist.
Hardening Checklist
-
Never deserialize user input without a class whitelist. Replace all
maybe_unserialize()and bareunserialize()calls withunserialize($data, ['allowed_classes' => ['stdClass']])or an even more restrictive list if only scalar types are needed. Consider usingjson_decode()as an alternative for untrusted data. -
Audit all entry points for serialized data. Search the codebase for
$_GET,$_POST,$_COOKIE, and AJAX action handlers that acceptdataparameters. Each must be logged and reviewed. -
Implement nonce verification on AJAX staging endpoints. Add
check_ajax_referer()calls to all staging creation and management actions to ensure an authenticated administrator initiated the request, raising the bar for exploit reliability. -
Run
wp-cli security-checkor use a static analysis tool like Semgrep with WordPress rules to detect patternsunserialize.*$_ormaybe_unserialize.*$_in custom code before commit. -
Restrict admin access to staging features by role or capability. Even with nonce protection, limit staging site creation to users with
manage_optionsor a custom capability, reducing the surface area for privilege escalation attacks.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-10932