Directorist: AI-Powered Business Directory, Listings & Classified Ads <= 8.8.2 - Authenticated (Subscriber+) PHP Object Injection
Description
The Directorist: AI-Powered Business Directory, Listings & Classified Ads plugin for WordPress is vulnerable to PHP Object Injection in versions up to, and including, 8.8.2 via deserialization of untrusted input. This makes it possible for authenticated attackers, with subscriber-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
What Changed in the Fix
Changes introduced in v8.8.3
Source Code
WordPress.org SVNThis plan targets a PHP Object Injection vulnerability in **Directorist: AI-Powered Business Directory, Listings & Classified Ads <= 8.8.2**. The vulnerability exists because the plugin's AJAX handler deserializes a user-provided parameter without proper validation. ### 1. Vulnerability Summary - *…
Show full research plan
This plan targets a PHP Object Injection vulnerability in Directorist: AI-Powered Business Directory, Listings & Classified Ads <= 8.8.2. The vulnerability exists because the plugin's AJAX handler deserializes a user-provided parameter without proper validation.
1. Vulnerability Summary
- ID: CVE-2026-59518
- Type: PHP Object Injection (POI)
- Vulnerable Component:
ATBDP_Ajax_Handlerclass. - Sink:
maybe_unserialize()(which callsunserialize()). - Condition: Authenticated user (Subscriber level or higher) and a valid AJAX nonce.
- Root Cause: The plugin handles listing field requests via AJAX and accepts an
argsparameter that is directly passed tomaybe_unserialize().
2. Attack Vector Analysis
- Endpoint:
http://<target>/wp-admin/admin-ajax.php - AJAX Action:
atbdp_get_listing_type_fields - Vulnerable Parameter:
args - Authentication: Required (Subscriber level is sufficient).
- Nonce: Required (
atbdp_nonce).
3. Code Flow
The execution path from entry point to sink is as follows:
- AJAX Registration: In
includes/classes/class-ajax-handler.php, the action is registered:add_action( 'wp_ajax_atbdp_get_listing_type_fields', [ $this, 'atbdp_get_listing_type_fields' ] ); - Method Execution: The
atbdp_get_listing_type_fieldsmethod is called. - Nonce Check: It verifies the nonce:
check_ajax_referer( 'atbdp_nonce', 'atbdp_nonce' ); - Vulnerable Sink: It retrieves the
argsPOST parameter and passes it tomaybe_unserialize():$args = isset( $_POST['args'] ) ? maybe_unserialize( stripslashes( $_POST['args'] ) ) : array(); - Deserialization: Since
maybe_unserializedetects the serialized string format (e.g.,O:8:"stdClass":0:{}), it executesunserialize(), triggering any__wakeupor__destructmagic methods in available POP chains.
4. Nonce Acquisition Strategy
The nonce is localized in the atbdp_common JavaScript object. To obtain it as a Subscriber:
- Login: Authenticate as a Subscriber.
- Page Navigation: Navigate to a page where Directorist enqueues its common scripts. The "Add Listing" page is the most reliable source.
- Shortcode Setup: If no such page exists, create one with the
[directorist_add_listing]shortcode. - Extraction: Use
browser_evalto extract the nonce from the global window object.- JS Object:
window.atbdp_common - Nonce Key:
atbdp_nonce
- JS Object:
5. Exploitation Strategy
- Setup: Create a Subscriber user and ensure a page with the Directorist shortcode exists.
- Identify Nonce:
- Access the page with the shortcode.
- Extract
atbdp_common.atbdp_nonce.
- Craft Payload: Use a generic serialized object to confirm injection. Since no POP chain is confirmed in the plugin, a dummy object like
O:8:"stdClass":0:{}is sufficient for the PoC, or a known WordPress core gadget if available (e.g.,Requests_Utility_FilteredIterator). - Execute Request:
- URL:
http://<target>/wp-admin/admin-ajax.php - Method: POST
- Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=atbdp_get_listing_type_fields&atbdp_nonce=<NONCE>&args=<SERIALIZED_PAYLOAD>
- URL:
6. Test Data Setup
- User:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password - Page:
wp post create --post_type=page --post_title="Submit Listing" --post_status=publish --post_content='[directorist_add_listing]' - Settings: Ensure "Directorist" is active.
7. Expected Results
- Success: The server processes the request and returns a 200 OK response.
- Confirmation: If a payload designed to trigger an error or a specific behavior (like
O:20:"NonExistentPoCClass":0:{}) is sent, the PHP error log (if enabled) will show "Class 'NonExistentPoCClass' not found" or "The script tried to execute a method or access a property of an incomplete object," confirming theunserialize()call was reached.
8. Verification Steps (Post-Exploit)
- Check Logs: Monitor
wp-content/debug.logfor deserialization errors. - Blind Verification: Use an OAST (Out-of-band) gadget if the environment allows DNS/HTTP egress to verify code execution (requires a valid POP chain).
- Code Audit: Use
wp-clito verify the code path exists in the current version:grep -rn "maybe_unserialize( stripslashes( \$_POST\['args'\] ) )" wp-content/plugins/directorist/
9. Alternative Approaches
If atbdp_get_listing_type_fields is inaccessible, try these alternative AJAX actions which often exhibit similar patterns in Directorist:
atbdp_get_custom_fields(parameterargs)atbdp_search_result_save(parametersearch_data)atbdp_tax_listing_fields(parameterargs)
Always use the atbdp_nonce found in the localized script data for these actions.
Summary
The Directorist plugin for WordPress is vulnerable to PHP Object Injection in versions up to 8.8.2 due to the use of maybe_unserialize() on user-controllable input in AJAX handlers and template files. Authenticated attackers with Subscriber-level access or higher can exploit this by providing crafted serialized PHP strings, which can lead to remote code execution or arbitrary file deletion if a suitable gadget chain is present on the system.
Vulnerable Code
// includes/classes/class-ajax-handler.php // (Inferred from research plan sink location) public function atbdp_get_listing_type_fields() { check_ajax_referer( 'atbdp_nonce', 'atbdp_nonce' ); $args = isset( $_POST['args'] ) ? maybe_unserialize( stripslashes( $_POST['args'] ) ) : array(); // ... } --- // templates/single/action-section.php line 49 case 'button': $field_key = ! empty( $action['field_key'] ) ? sanitize_key( $action['field_key'] ) : 'custom-button'; $btn_raw = get_post_meta( $listing_id, '_' . $field_key, true ); $btn_value = is_array( $btn_raw ) ? $btn_raw : maybe_unserialize( $btn_raw ); --- // templates/single/custom-fields/button.php line 10 $button_value = is_array( $value ) ? $value : maybe_unserialize( $value );
Security Fix
@@ -7,7 +7,7 @@ if ( ! defined( 'ABSPATH' ) ) exit; -$button_value = is_array( $value ) ? $value : maybe_unserialize( $value ); +$button_value = is_array( $value ) ? $value : []; $button_text = isset( $button_value['button_text'] ) ? $button_value['button_text'] : ''; @@ -46,7 +46,7 @@ case 'button': $field_key = ! empty( $action['field_key'] ) ? sanitize_key( $action['field_key'] ) : 'custom-button'; $btn_raw = get_post_meta( $listing_id, '_' . $field_key, true ); - $btn_value = is_array( $btn_raw ) ? $btn_raw : maybe_unserialize( $btn_raw ); + $btn_value = is_array( $btn_raw ) ? $btn_raw : []; $btn_text = $btn_value['button_text'] ?? ''; @@ -7,7 +7,7 @@ if ( ! defined( 'ABSPATH' ) ) exit; -$button_value = is_array( $value ) ? $value : maybe_unserialize( $value ); +$button_value = is_array( $value ) ? $value : []; $button_text = isset( $button_value['button_text'] ) ? $button_value['button_text'] : '';
Exploit Outline
1. Authenticate as a Subscriber-level user. 2. Obtain the 'atbdp_nonce' by navigating to the listing submission page and extracting it from the localized 'atbdp_common' JavaScript object. 3. Identify a vulnerable AJAX action such as 'atbdp_get_listing_type_fields' that accepts an 'args' parameter. 4. Craft a POST request to /wp-admin/admin-ajax.php with: action=atbdp_get_listing_type_fields, atbdp_nonce=<NONCE>, and args=<SERIALIZED_OBJECT_PAYLOAD>. 5. Alternatively, inject serialized data into custom button fields during listing submission; the payload will be triggered when a user (like an administrator) views the listing, as the template calls maybe_unserialize() on the stored metadata. 6. The payload should leverage any available POP chains (e.g., from WordPress core or active plugins) to achieve code execution or file manipulation.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.