Login with OTP <= 1.6 - Unauthenticated Authentication Bypass via OTP Brute Force
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:HTechnical Details
# 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(viawp_ajax_nopriv_otpl_login) or a custom handler oninit. - Action/Mode: The function
otpl_login_action()likely handles both OTP requests and OTP submissions. - Parameters:
action: (Inferred)otpl_loginorotpl_login_action.usernameoruser_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)
- Entry Point: An unauthenticated user sends a POST request to
admin-ajax.phpwithaction=otpl_login. - Dispatch: WordPress calls
otpl_login_action(). - 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
otpparameter. CRITICAL FLAW: This branch lacks the rate-limit/lockout check.
- 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.,
- Validation: The code compares the user-supplied
otpwith the stored meta value. - 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.
- Identify Shortcode: Search for
add_shortcodein the plugin (likely[otp_login_form]). - Setup Page:
wp post create --post_type=page --post_status=publish --post_title="Login" --post_content='[otp_login_form]' - Extract Nonce:
- Navigate to the newly created page.
- Use
browser_evalto find the localized object. Look forotpl_ajaxor 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):
(Note: Parameter names likeaction=otpl_login&username=admin&mode=generate&nonce=[EXTRACTED_NONCE]modeorstepshould be verified via grep inotpl_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:
- Identify the OTP: Use
wp user meta get 1 _otpl_code(or the actual meta key found via grep) to retrieve the "secret" code. - 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]
- 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
- Target User: Ensure an admin user exists (default
admin). - Plugin Config: Install "Login with OTP" 1.6.
- Page Creation: Create a page with the login shortcode to facilitate nonce extraction.
- Logging: Monitor
wp-content/debug.logifWP_DEBUGis enabled.
7. Expected Results
- Phase 1 Response: JSON success message indicating an OTP was sent.
- Brute Force Response: A response containing
Set-Cookieheaders forwordpress_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
- Cookie Check: Capture the
Set-Cookieheader from the successful "guess." - Auth Verification: Use the captured cookie to request
/wp-admin/viahttp_request.// Example http_request config { url: "http://localhost:8080/wp-admin/", headers: { "Cookie": "captured_cookies_here" } } - Database Check:
wp user meta get 1 _otpl_codeto 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
usernamedoesn't work, tryuser_idoremail. - Direct Login Page: Check if
otpl_login_actionis hooked toinitand processes standard$_POSTdata from the login page, bypassing AJAX entirely.
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
@@ -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.