
Exploit PoC and root-cause analysis for a critical unauthenticated PHP object injection in WordPress Database for Contact Form 7, leading to RCE via arbitrary file deletion.
Plugin: Database for Contact Form 7 (contact-form-entries) ≤ 1.4.3
CVSS: 9.8 (Critical)
CWE: CWE-502 — Deserialization of Untrusted Data
Authentication Requirement: None (Unauthenticated)
Impact: Remote Code Execution
The "Database for Contact Form 7" plugin (slug: contact-form-entries) version 1.4.3 and below contains a PHP Object Injection vulnerability. When a WordPress administrator views a form record (entry) inside the admin panel, the plugin calls the function maybe_unserialize() directly on data submitted by an unauthenticated user via Contact Form 7, without controlling the list of allowed classes to instantiate.
An attacker does not need to log in — they only need to submit a regular contact form while inserting a serialized PHP object into any form field. This data is stored raw in the database. When an admin opens to view that entry, the deserialization function will instantiate an object of the attacker's choice, triggering magic methods like __destruct() or __wakeup() → executing arbitrary behavior depending on the POP gadgets available in the WordPress environment.
Severity Level: With a suitable POP gadget (for example, a class whose
__destruct()method callsunlink()), an attacker can delete thewp-config.phpfile, reverting WordPress back to its initial installation screen → reinstalling with an administrator account controlled by the attacker → installing a plugin containing a webshell → achieving full Remote Code Execution (RCE) on the server.
| Attribute | Value |
|---|---|
| CVE ID | CVE-2025-7384 |
| CVSS Score | 9.8 (Critical) |
| CWE | CWE-502 — Deserialization of Untrusted Data |
| Affected Plugin | contact-form-entries (Database for Contact Form 7) ≤ 1.4.3 |
| Authentication Requirement | None — anyone submitting a CF7 form can inject payload |
| Trigger Condition | Admin views the injected entry in the admin panel |
| Maximum Impact | Unauthenticated Remote Code Execution |
| Patched Version | 1.4.4+ (replaces unserialize with json_decode or allowed_classes: false) |
PHP uses serialize() to convert an object into a structured text string, and unserialize() to restore the object from that string. When unserialize() receives data from an untrusted source (e.g., user input), an attacker can construct an arbitrary object belonging to any class currently loaded in PHP memory at that moment.
Special methods that PHP automatically invokes during an object's lifecycle. Most important in this context:
__wakeup() — invoked immediately when an object is unserialized__destruct() — invoked when an object is destroyed (goes out of scope, or request ends)__toString() — invoked when an object is cast to a stringA technique of chaining multiple magic methods from existing classes within the application to construct a dangerous sequence of behaviors. The attacker does not write new code — they only manipulate the properties of existing objects so that when magic methods execute, they perform actions unintentional to the developers.
maybe_unserialize() in WordPressA WordPress Core wrapper function. It calls is_serialized() to check if a string is serialized data — if true, it calls unserialize() to restore the object. Problem: this function does not pass the allowed_classes parameter (available since PHP 7.0) to limit which classes are permitted to instantiate.
Start by grepping the entire plugin source code to locate deserialization functions — these are the most dangerous functions in PHP as they can lead to Object Injection:
grep -rn "unserialize" wp-src/wp-content/plugins/contact-form-entries/

The output reveals multiple call sites for maybe_unserialize(), most notably inside includes/data.php line 545 within the verify_val() function:

// data.php lines 538-548
public function verify_val($string){
if(in_array(substr(ltrim($string),0,1), array('{','['))
&& in_array(substr(rtrim($string),-1), array('}',']'))
){
$val = json_decode($string, 1);
if(is_array($val)){ $string = $val; }
} else if(is_serialized($string)){ // line 544
$string = maybe_unserialize($string); // ★ line 545 — SINK
}
return $string;
}
Key Question: Where does the $string variable originate? If it comes from user input without filtering → this is a vulnerability.
Find where verify_val() is invoked. Trace backward in the same data.php file:

// data.php lines 520-535
public function get_lead_detail($lead_id){
global $wpdb;
$table = $wpdb->prefix . 'vxcf_leads_detail';
$detail_arr = $wpdb->get_results(
$wpdb->prepare("SELECT * FROM $table WHERE lead_id=%d", $lead_id),
ARRAY_A
);
foreach($detail_arr as $k => $v){
if(!empty($v['value'])){
$detail_arr[$k]['value'] = $this->verify_val($v['value']); // ← calls verify_val
}
}
return $detail_arr;
}
→ $string is precisely $v['value'] — values retrieved from the wp_vxcf_leads_detail database table. This function is called when an admin views the details of a form entry.
Next Question: Where does data inside wp_vxcf_leads_detail come from? Who writes it?
From Step 2, we know data is pulled from the database. Next question: who writes data into it? Search for INSERT queries within data.php:
grep -rn "insert" wp-src/wp-content/plugins/contact-form-entries/includes/data.php

Open the create_lead() function code (lines 85-103) for details:

At lines 98-99, the value $v — which is the content of a form field (e.g., your-message) — is inserted directly into the database via $wpdb->insert(). The plugin hooks into Contact Form 7's wpcf7_before_send_mail event, so whenever a user submits a form, all fields are stored raw.
Additional check: the plugin does use sanitize_text_field() and sanitize_textarea_field() prior to saving, but these two functions only strip HTML tags and special HTML characters — a serialized payload like O:21:"VulnerableFileHandler":2:{...} contains no HTML tags and thus passes through entirely intact.