CVE-2026-8760

Login with OTP <= 1.6 - Unauthenticated Authentication Bypass via OTP Brute Force

criticalImproper Restriction of Excessive Authentication Attempts
9.8
CVSS Score
9.8
CVSS Score
critical
Severity
Unpatched
Patched in
N/A
Time to patch

Description

The Login with OTP plugin for WordPress is vulnerable to authentication bypass in all versions up to, and including, 1.6. This is due to an incomplete fix for CVE-2024-11178: the rate-limit/lockout check added to `otpl_login_action()` was placed only inside the OTP-generation branch and is never evaluated on the OTP-validation branch, and the generated 6-digit OTP additionally has no expiration. This makes it possible for unauthenticated attackers to brute-force the 900,000-value OTP space for any user account (including administrators) and obtain a valid `wp_set_auth_cookie()` session, leading to full site compromise.

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<=1.6
PublishedMay 26, 2026
Last updatedMay 27, 2026
Affected pluginotp-login
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-8760 (Login with OTP Brute Force) ## 1. Vulnerability Summary The **Login with OTP** plugin (<= 1.6) contains a critical authentication bypass vulnerability due to improper rate limiting. While the plugin implemented a lockout mechanism in version 1.6 to addre…

Show full research plan

Exploitation Research Plan: CVE-2026-8760 (Login with OTP Brute Force)

1. Vulnerability Summary

The Login with OTP plugin (<= 1.6) contains a critical authentication bypass vulnerability due to improper rate limiting. While the plugin implemented a lockout mechanism in version 1.6 to address CVE-2024-11178, the check was incorrectly placed within the OTP generation logic rather than the validation logic. Furthermore, the 6-digit OTPs do not expire, allowing an unauthenticated attacker to brute-force the ~1,000,000 possible combinations to successfully authenticate as any user, including administrators.

2. Attack Vector Analysis

  • Endpoint: Likely admin-ajax.php (via wp_ajax_nopriv_otpl_login) or a custom handler on init.
  • Action/Mode: The function otpl_login_action() likely handles both OTP requests and OTP submissions.
  • Parameters:
    • action: (Inferred) otpl_login or otpl_login_action.
    • username or user_id: Target user.
    • otp: The 6-digit brute-force payload.
    • nonce: (Inferred) Required for AJAX requests.
  • Authentication: Unauthenticated.
  • Precondition: The attacker must first trigger the generation of an OTP for the target user to populate the database with a valid code.

3. Code Flow (Inferred from Description)

  1. Entry Point: An unauthenticated user sends a POST request to admin-ajax.php with action=otpl_login.
  2. Dispatch: WordPress calls otpl_login_action().
  3. Branching:
    • Branch A (Request OTP): The code checks for a "request" flag. It performs a rate-limit check (the "fix") and generates a 6-digit OTP, saving it to user meta (e.g., _otpl_code).
    • Branch B (Verify OTP): The code checks for the presence of an otp parameter. CRITICAL FLAW: This branch lacks the rate-limit/lockout check.
  4. Validation: The code compares the user-supplied otp with the stored meta value.
  5. Sink: Upon a match, the code executes wp_set_auth_cookie($user_id) and returns a success response.

4. Nonce Acquisition Strategy

The plugin likely localizes a nonce for its AJAX login form.

  1. Identify Shortcode: Search for add_shortcode in the plugin (likely [otp_login_form]).
  2. Setup Page:
    wp post create --post_type=page --post_status=publish --post_title="Login" --post_content='[otp_login_form]'
    
  3. Extract Nonce:
    • Navigate to the newly created page.
    • Use browser_eval to find the localized object. Look for otpl_ajax or similar in the source.
    • Command: browser_eval("window.otpl_settings?.nonce") (Verify variable name in source).

5. Exploitation Strategy

Phase 1: Trigger OTP Generation

Send a request to generate the OTP for the admin user. This ensures a code exists in the database to be guessed.

  • Method: POST
  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Body (URL-encoded):
    action=otpl_login&username=admin&mode=generate&nonce=[EXTRACTED_NONCE]
    
    (Note: Parameter names like mode or step should be verified via grep in otpl_login_action).

Phase 2: Brute Force (Simulated for PoC)

Since a full 1,000,000 attempt brute force is impractical for a PoC, the agent should:

  1. Identify the OTP: Use wp user meta get 1 _otpl_code (or the actual meta key found via grep) to retrieve the "secret" code.
  2. Execute Validation Request: Send the matching code to prove the bypass.
  • Method: POST
  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Body:
    action=otpl_login&username=admin&otp=[RETRIEVED_CODE]&nonce=[EXTRACTED_NONCE]
    
  1. Verify Rate Limit Absence: Send 10-20 incorrect OTP requests and observe that the plugin continues to respond with "Invalid OTP" (or similar) instead of a 429/Lockout response.

6. Test Data Setup

  1. Target User: Ensure an admin user exists (default admin).
  2. Plugin Config: Install "Login with OTP" 1.6.
  3. Page Creation: Create a page with the login shortcode to facilitate nonce extraction.
  4. Logging: Monitor wp-content/debug.log if WP_DEBUG is enabled.

7. Expected Results

  • Phase 1 Response: JSON success message indicating an OTP was sent.
  • Brute Force Response: A response containing Set-Cookie headers for wordpress_logged_in_*.
  • Vulnerability Confirmation: The server must NOT return a "Too many attempts" error during the incorrect OTP submissions in the validation branch.

8. Verification Steps

  1. Cookie Check: Capture the Set-Cookie header from the successful "guess."
  2. Auth Verification: Use the captured cookie to request /wp-admin/ via http_request.
    // Example http_request config
    {
      url: "http://localhost:8080/wp-admin/",
      headers: { "Cookie": "captured_cookies_here" }
    }
    
  3. Database Check: wp user meta get 1 _otpl_code to confirm the code used matches the one in the DB.

9. Alternative Approaches

  • REST API: Check if the plugin registers any routes via register_rest_route. If so, the brute force may be possible via the REST API, which might lack the nonce requirement.
  • Parameter variations: If username doesn't work, try user_id or email.
  • Direct Login Page: Check if otpl_login_action is hooked to init and processes standard $_POST data from the login page, bypassing AJAX entirely.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Login with OTP plugin for WordPress (versions 1.6 and below) is vulnerable to an authentication bypass due to an incomplete rate-limiting fix. While a lockout mechanism was implemented for OTP generation, it was omitted from the validation logic, allowing unauthenticated attackers to brute-force the 6-digit OTP space for any user account.

Security Fix

--- a/otp-login/otp-login.php
+++ b/otp-login/otp-login.php
@@ -115,6 +115,10 @@
 	public function otpl_login_action() {
 		if ( isset( $_POST['otp'] ) ) {
+			if ( $this->is_locked_out( $user_id ) ) {
+				wp_send_json_error( array( 'message' => __( 'Too many failed attempts.', 'otp-login' ) ) );
+			}
+	
 			if ( $this->verify_otp( $user_id, $_POST['otp'] ) ) {
 				wp_set_auth_cookie( $user_id );
 				wp_send_json_success();

Exploit Outline

The exploit involves two phases. First, an unauthenticated attacker sends a request to the `otpl_login` AJAX action to trigger the generation of an OTP for a target username (such as 'admin'). Second, the attacker systematically brute-forces the 6-digit OTP by sending POST requests to the validation endpoint. Because the validation branch lacks the rate-limiting checks found in the generation branch and the OTPs do not expire, the attacker can eventually identify the correct 6-digit code, which triggers `wp_set_auth_cookie()` and grants full administrative access.

Check if your site is affected.

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