Login No Captcha reCAPTCHA <= 1.8.0 - Unauthenticated Stored Cross-Site Scripting via PHP_SELF
Description
The Login No Captcha reCAPTCHA plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the `$_SERVER['PHP_SELF']` superglobal in all versions up to, and including, 1.8.0. This is due to the `authenticate()` function storing the unsanitized output of `basename($_SERVER['PHP_SELF'])` in the `login_nocaptcha_error` WordPress option when a login attempt is made from a non-standard login page (e.g., xmlrpc.php). The `admin_notices()` function then echoes this stored value directly into the admin dashboard HTML without escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts that execute when an administrator with a whitelisted IP address visits the WordPress dashboard within 30 seconds of the attack.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:NTechnical Details
What Changed in the Fix
Changes introduced in v1.8.1
Source Code
WordPress.org SVNThis research plan outlines the steps required to demonstrate the Unauthenticated Stored Cross-Site Scripting (XSS) vulnerability in the **Login No Captcha reCAPTCHA** plugin (CVE-2026-2374). ### 1. Vulnerability Summary The `Login No Captcha reCAPTCHA` plugin (up to version 1.8.0) is vulnerable to…
Show full research plan
This research plan outlines the steps required to demonstrate the Unauthenticated Stored Cross-Site Scripting (XSS) vulnerability in the Login No Captcha reCAPTCHA plugin (CVE-2026-2374).
1. Vulnerability Summary
The Login No Captcha reCAPTCHA plugin (up to version 1.8.0) is vulnerable to Stored XSS because it trusts the $_SERVER['PHP_SELF'] value during authentication attempts on non-standard login pages (like xmlrpc.php). The LoginNocaptcha::authenticate() function retrieves the current script's name using basename($_SERVER['PHP_SELF']) and stores it into the login_nocaptcha_error WordPress option when it identifies a login attempt that bypasses the standard wp-login.php flow. This stored value is later rendered in the admin dashboard via the admin_notices hook without any output escaping, allowing an unauthenticated attacker to inject malicious scripts into the administrator's view.
2. Attack Vector Analysis
- Vulnerable Endpoint:
/xmlrpc.php/(or any path that triggers theauthenticatefilter). - Vulnerable Parameter: The URL path component of
PHP_SELF. - Authentication: None (Unauthenticated).
- Preconditions:
- The plugin must be configured with a valid-length (40 characters) Site Key and Secret Key to activate the
authenticatefilter. - An administrator must visit the dashboard (
/wp-admin/) to trigger the payload. - The payload must be triggered within the time window before the notice is cleared (the description suggests a 30-second window, likely due to the plugin's transient handling of error notices).
- The plugin must be configured with a valid-length (40 characters) Site Key and Secret Key to activate the
3. Code Flow
- Initialization:
LoginNocaptcha::init()(inlogin-nocaptcha.php) checks iflogin_nocaptcha_keyandlogin_nocaptcha_secretare set usingvalid_key_secret(). - Hook Registration: If keys are valid, it registers the
authenticatefilter:add_filter('authenticate', array('LoginNocaptcha', 'authenticate'), 30, 3). - Source (Input): An attacker requests
/xmlrpc.php/<img src=x onerror=alert(1)>. WordPress populates$_SERVER['PHP_SELF']with the path including the payload. - Processing: The
authenticate()function is called during the XML-RPC authentication attempt. It checks the current script:basename($_SERVER['PHP_SELF']). - Storage: The function identifies the script is not
wp-login.phpand callsupdate_option('login_nocaptcha_error', ...)containing the malicious basename. - Sink (Output): When an admin loads any admin page,
LoginNocaptcha::admin_notices()is called. It retrieves thelogin_nocaptcha_erroroption andechoes it directly to the page without usingesc_html()orwp_kses().
4. Nonce Acquisition Strategy
This vulnerability is unauthenticated and occurs during the authentication process itself (the authenticate filter). Therefore, no WordPress nonce is required to inject the payload.
5. Exploitation Strategy
The goal is to store an XSS payload in the login_nocaptcha_error option by making a failed login attempt via XML-RPC.
Step 1: Configure the Plugin
The plugin only hooks the vulnerable filter if it believes it is correctly configured.
# Set dummy keys of 40 characters (required by valid_key_secret check)
wp option update login_nocaptcha_key "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
wp option update login_nocaptcha_secret "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
Step 2: Inject the Payload
Send an XML-RPC request to a URL path containing the XSS payload. We use a standard XML-RPC system call to trigger the authentication flow.
- URL:
/xmlrpc.php/foo<img src=x onerror=alert(document.domain)> - Method:
POST - Body: A standard
wp.getUsersBlogsXML-RPC call with dummy credentials.
Step 3: Trigger the XSS
Log in as an administrator and navigate to the dashboard.
6. Test Data Setup
- Plugin Status: Ensure
login-recaptchais installed and active. - Admin User: A standard administrator account (e.g.,
admin/password). - Keys: Keys must be 40 chars long as per
LoginNocaptcha::valid_key_secret.
7. Expected Results
- The
POSTrequest to/xmlrpc.php/...will return an XML response (likely an authentication failure). - The WordPress option
login_nocaptcha_errorwill now contain the stringfoo<img src=x onerror=alert(document.domain)>. - When the admin navigates to
/wp-admin/, an alert box displaying the document domain will appear.
8. Verification Steps
After sending the injection request, verify the database state using WP-CLI:
wp option get login_nocaptcha_error
Expected Output: The string should contain your HTML payload unencoded.
To verify the XSS via browser:
- Use
browser_navigateto/wp-admin/. - Observe the execution of the JavaScript payload.
9. Alternative Approaches
If /xmlrpc.php/ does not correctly populate PHP_SELF due to server configuration (e.g., Nginx without proper path info support), try:
- Query String Injection: Some environments might include the query string in
PHP_SELFif not properly handled, thoughbasename()usually mitigates this. - Direct Path: If the site uses a custom login page provided by another plugin that triggers
authenticate(), use that path. - Different Payload: If
<img ...>is blocked by a WAF, try<details open ontoggle=...>or<svg onload=...>to avoid thescriptorimgtags.
Payload for http_request (Step 2):
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
<methodName>wp.getUsersBlogs</methodName>
<params>
<param><value><string>attacker</string></value></param>
<param><value><string>password</string></value></param>
</params>
</methodCall>
Request URL: http://localhost:8080/xmlrpc.php/x%3Cimg%20src=x%20onerror=alert(1)%3E
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.