CVE-2026-59518

Directorist: AI-Powered Business Directory, Listings & Classified Ads <= 8.8.2 - Authenticated (Subscriber+) PHP Object Injection

highDeserialization of Untrusted Data
7.5
CVSS Score
7.5
CVSS Score
high
Severity
8.8.3
Patched in
6d
Time to patch

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:H
Attack Vector
Network
Attack Complexity
High
Privileges Required
Low
User Interaction
None
Scope
Unchanged
High
Confidentiality
High
Integrity
High
Availability

Technical Details

Affected versions<=8.8.2
PublishedJuly 9, 2026
Last updatedJuly 14, 2026
Affected plugindirectorist

What Changed in the Fix

Changes introduced in v8.8.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

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 - *…

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_Handler class.
  • Sink: maybe_unserialize() (which calls unserialize()).
  • 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 args parameter that is directly passed to maybe_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:

  1. 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' ] );
    
  2. Method Execution: The atbdp_get_listing_type_fields method is called.
  3. Nonce Check: It verifies the nonce:
    check_ajax_referer( 'atbdp_nonce', 'atbdp_nonce' );
    
  4. Vulnerable Sink: It retrieves the args POST parameter and passes it to maybe_unserialize():
    $args = isset( $_POST['args'] ) ? maybe_unserialize( stripslashes( $_POST['args'] ) ) : array();
    
  5. Deserialization: Since maybe_unserialize detects the serialized string format (e.g., O:8:"stdClass":0:{}), it executes unserialize(), triggering any __wakeup or __destruct magic 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:

  1. Login: Authenticate as a Subscriber.
  2. Page Navigation: Navigate to a page where Directorist enqueues its common scripts. The "Add Listing" page is the most reliable source.
  3. Shortcode Setup: If no such page exists, create one with the [directorist_add_listing] shortcode.
  4. Extraction: Use browser_eval to extract the nonce from the global window object.
    • JS Object: window.atbdp_common
    • Nonce Key: atbdp_nonce

5. Exploitation Strategy

  1. Setup: Create a Subscriber user and ensure a page with the Directorist shortcode exists.
  2. Identify Nonce:
    • Access the page with the shortcode.
    • Extract atbdp_common.atbdp_nonce.
  3. 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).
  4. 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>
      

6. Test Data Setup

  1. User: wp user create attacker attacker@example.com --role=subscriber --user_pass=password
  2. Page: wp post create --post_type=page --post_title="Submit Listing" --post_status=publish --post_content='[directorist_add_listing]'
  3. 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 the unserialize() call was reached.

8. Verification Steps (Post-Exploit)

  1. Check Logs: Monitor wp-content/debug.log for deserialization errors.
  2. 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).
  3. Code Audit: Use wp-cli to 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 (parameter args)
  • atbdp_search_result_save (parameter search_data)
  • atbdp_tax_listing_fields (parameter args)

Always use the atbdp_nonce found in the localized script data for these actions.

Research Findings
Static analysis — not yet PoC-verified

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

diff -ru directorist/8.8.2/templates/archive/custom-fields/button.php directorist/8.8.3/templates/archive/custom-fields/button.php
--- directorist/8.8.2/templates/archive/custom-fields/button.php
+++ directorist/8.8.3/templates/archive/custom-fields/button.php
@@ -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'] : '';
 
diff -ru directorist/8.8.2/templates/single/action-section.php directorist/8.8.3/templates/single/action-section.php
--- directorist/8.8.2/templates/single/action-section.php
+++ directorist/8.8.3/templates/single/action-section.php
@@ -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'] ?? '';
 
diff -ru directorist/8.8.2/templates/single/custom-fields/button.php directorist/8.8.3/templates/single/custom-fields/button.php
--- directorist/8.8.2/templates/single/custom-fields/button.php
+++ directorist/8.8.3/templates/single/custom-fields/button.php
@@ -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.