Skip to content
100,000 WordPress Sites Exposed to Remote Code Execution via PHP Object ...

100,000 WordPress Sites Exposed to Remote Code Execution via PHP Object ...

Wordfence September 17, 2026

Our mission is to secure WordPress through defense in depth, which is why we are investing in proactive vulnerability research of this kind alongside our Bug Bounty Program . We are committed to making the WordPress ecosystem more secure through the detection and prevention of vulnerabilities, which is a critical element to our multi-layered approach to security.

Wordfence Premium , Wordfence Care , and Wordfence Response users received a firewall rule to protect against known exploits targeting this vulnerability on August 25, 2026. Sites using the free version of Wordfence will receive the same protection 30 days later, on September 24, 2026.

We sent full disclosure details to the Themeum team through our Wordfence Vulnerability Management Portal on August 23, 2026, the same day we received and validated the report. The Themeum team acknowledged the vulnerability on August 24, 2026, and released a fully patched version, 4.0.8, on September 10, 2026. We would like to commend the Themeum team for their prompt response and timely patch.

We urge users to update their sites with the latest patched version of Tutor LMS, version 4.0.8 at the time of this publication, as soon as possible.

Vulnerability Summary from Wordfence Intelligence

Unfortunately, insecure implementation of the plugin’s withdraw account management feature allows for PHP Object Injection leading to remote code execution. The vulnerability spans four components across the plugin, exploits an obscure interaction inside WordPress core’s database abstraction layer, and terminates in a destructor method of a bundled third-party library.

The entry point is the tutor_save_withdraw_account AJAX handler, registered in classes/Withdraw.php . As shown on line 44 of Withdraw.php , the handler is registered using the wp_ajax_ hook only — the authenticated hook — with no corresponding capability check anywhere in the handler body:

Attacker-controlled data flows in through $_POST['withdraw_method_field'][$method] , retrieved on line 204 via tutor_utils()->avalue_dot() . The keys of this associative array, the $input_name values, come entirely from POST field names and are never sanitized or constrained. The values are then processed on line 213:

The root cause is the misuse of esc_sql() on a value destined for update_user_meta() rather than a raw database query string. esc_sql() internally calls wpdb::add_placeholder_escape() , which replaces every literal % character with a 66-byte HMAC-keyed placeholder token. This is a WordPress internal safety mechanism designed to prevent % characters from being misinterpreted as printf-style format specifiers in $wpdb->prepare() . When a value like AAAA%z passes through esc_sql() , it becomes a 71-character string in memory. update_user_meta() then calls maybe_serialize() on the entire $saved_data array, faithfully serializing this inflated string as s:71:"AAAA{token}z" .

Here is the twist that turns this into a length desync. WordPress strips those placeholder tokens back out of every query immediately before execution, wpdb registers remove_placeholder_escape() on the query filter at priority 0, so it runs on the UPDATE statement that writes the user meta. As the row is written, the 66-byte token collapses back to a single % character in place, inside the already-serialized string. What actually lands in the database is s:71:"AAAA%z" , a serialized string token that declares a length of 71 bytes but whose actual content is only 6 bytes. The corruption is baked into storage on write; the payload detonates the time that blob is unserialized on read.

When maybe_unserialize() later hands this string to PHP’s unserialize() , the parser reads 71 bytes starting from the opening quote, over-reading 65 bytes past the end of AAAA%z and into the array element. Because the keys of $saved_data come from unescaped POST field names that the attacker controls entirely, the attacker can craft a second array key whose content is a well-formed serialized object stream. After the 65-byte over-read consumes the attacker’s padding, the parser resumes execution on the injected payload.

Detonation requires only that the corrupted blob be unserialized, and sending the same request a second time is sufficient to cause it. On the first request, the corrupted user meta is written to the database. On the second request, the plugin’s update_user_meta() call invokes WordPress core’s update_metadata() , which to determine whether the value has actually changed, reads the previously-stored value via get_metadata_raw() , and get_metadata_raw() runs maybe_unserialize() on it.

That core-internal maybe_unserialize() on the corrupted blob materializes the attacker’s injected objects. The corrupted meta is equally a latent landmine for any other reader: WithdrawModel::get_user_withdraw_method() runs get_user_meta( $user_id, '_tutor_withdraw_method_data', true ) → maybe_unserialize() , and it is invoked both by the tutor_make_an_withdraw handler and whenever the withdrawal dashboard or account settings page is rendered, so simply viewing the withdrawal page also detonates the payload.

The injected payload is a two-element array. The first element references the class name ecommerce\PaymentGateways\Paypal\vendor\autoload , not a real class, but a name crafted so that when unserialize() tries to resolve it, the plugin’s registered autoloader maps the name to a filesystem path and includes, via require_once (), the corresponding file. The plugin registers a custom spl_autoload_register callback in classes/RestAPI.php :

This loader translates the class name ecommerce\PaymentGateways\Paypal\vendor\autoload into a filesystem path and, because the file exists in the plugin directory, it includes, via require_once (), the bundled PayPal Composer autoloader at ecommerce/PaymentGateways/Paypal/vendor/autoload.php . That Composer autoloader in turn registers the GuzzleHttp\* class hierarchy, making the second injected object, a genuine GuzzleHttp\Cookie\FileCookieJar , fully materializable.

The Themeum team patched this vulnerability in version 4.0.8 with a set of changes to tutor_save_withdraw_account() in classes/Withdraw.php that address multiple layers of the attack chain simultaneously. The most important addition is a proper capability check immediately after the nonce verification, ensuring the handler is only reachable by instructors:

Critically, the patch introduces a strict whitelist for array keys: the loop now iterates over $form_fields (the plugin’s own declared field definitions for the chosen withdrawal method) rather than over the attacker-supplied $method_data , and only accepts keys present in both. This prevents an attacker from injecting arbitrary keys into the serialized payload regardless of the value-handling path. Finally, the method name is validated against the plugin’s list of available withdrawal methods before any field processing occurs:

Together, these changes address the vulnerability at four independent points: the authentication gate, the serialization corruption source, the key injection vector, and the method validation bypass. Any one of the first three changes alone would have been sufficient to block exploitation; shipping all of them in a single patch demonstrates thorough remediation. A key highlight of defense in depth applied as a patch.

We encourage all WordPress site owners running Tutor LMS to update to version 4.0.8 or later as soon as possible. Given that Tutor LMS sites commonly enable open student registration as a core part of their function, the effective authentication bar for exploitation is low on many affected installations, making this a high-priority update.

Wordfence Premium , Wordfence Care , and Wordfence Response users received a firewall rule to protect against any exploits targeting this vulnerability on August 25, 2026. Sites using the free version of Wordfence will receive the same protection 30 days later, on September 24, 2026.

If you know someone who uses this plugin on their site, we recommend sharing this advisory with them to ensure their site remains secure, as this vulnerability poses a significant risk.

Breaking WordPress Security Research in your inbox as it happens.

Extracted Entities

Companies (2)

Domains (1)

Platforms (1)

Vulnerabilities (1)