#!/usr/bin/env python3
"""
React2Shell Vulnerability Scanner (CVE-2025-55182 / CVE-2025-66478)
Scans for vulnerable React Server Components and Next.js installations
"""

import requests
import re
import json
from urllib.parse import urljoin, urlparse
from typing import Dict, List, Tuple
import sys

class React2ShellScanner:
    def __init__(self, target_url: str, timeout: int = 10):
        self.target_url = target_url.rstrip('/')
        self.timeout = timeout
        self.vulnerabilities = []
        self.info = []
        self.headers = {
            'User-Agent': 'React2Shell-Scanner/1.0 (Security Research)'
        }

    def print_banner(self):
        print("=" * 70)
        print("React2Shell Vulnerability Scanner")
        print("CVE-2025-55182 (React) & CVE-2025-66478 (Next.js)")
        print("=" * 70)
        print(f"Target: {self.target_url}")
        print("=" * 70)
        print()

    def check_rsc_endpoint(self) -> bool:
        """Check for React Server Components endpoint"""
        print("[*] Checking for RSC endpoints...")

        rsc_paths = [
            '/',
            '/_next/data',
            '/api',
            '/__next_data__',
        ]

        vulnerable = False

        for path in rsc_paths:
            url = urljoin(self.target_url, path)

            # Test with RSC-specific headers
            rsc_headers = {
                **self.headers,
                'RSC': '1',
                'Next-Router-State-Tree': '%5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D',
                'Next-Url': '/',
                'Accept': 'text/x-component',
            }

            try:
                response = requests.get(url, headers=rsc_headers, timeout=self.timeout, verify=False)

                # Check for RSC response indicators
                if 'text/x-component' in response.headers.get('Content-Type', ''):
                    self.vulnerabilities.append(f"✗ RSC endpoint found at: {url}")
                    self.vulnerabilities.append(f"  Content-Type: {response.headers.get('Content-Type')}")
                    vulnerable = True

                # Check response content for RSC markers
                if response.text and any(marker in response.text for marker in ['0:', '1:', 'M1:', 'S1:']):
                    self.info.append(f"⚠ Possible RSC response at: {url}")

            except Exception as e:
                pass

        return vulnerable

    def check_nextjs_server_actions(self) -> bool:
        """Check for Next.js Server Actions"""
        print("[*] Checking for Next.js Server Actions...")

        action_headers = {
            **self.headers,
            'Next-Action': 'test',
            'Content-Type': 'text/plain;charset=UTF-8',
        }

        try:
            response = requests.post(
                self.target_url,
                headers=action_headers,
                data='[]',
                timeout=self.timeout,
                verify=False
            )

            if response.status_code in [200, 500] and 'x-action-revalidated' in response.headers:
                self.vulnerabilities.append(f"✗ Next.js Server Actions endpoint detected")
                return True

        except Exception as e:
            pass

        return False

    def detect_framework_version(self) -> Dict[str, str]:
        """Attempt to detect React/Next.js version"""
        print("[*] Detecting framework versions...")

        versions = {}

        # Check common build manifest paths
        manifest_paths = [
            '/_next/static/chunks/webpack.js',
            '/_next/static/chunks/main.js',
            '/static/js/main.js',
            '/_next/BUILD_MANIFEST',
            '/build-manifest.json',
        ]

        for path in manifest_paths:
            url = urljoin(self.target_url, path)
            try:
                response = requests.get(url, headers=self.headers, timeout=self.timeout, verify=False)

                if response.status_code == 200:
                    # Look for version strings
                    react_version = re.search(r'react["\']?\s*:\s*["\']([0-9.]+)', response.text)
                    nextjs_version = re.search(r'next["\']?\s*:\s*["\']([0-9.]+)', response.text)

                    if react_version:
                        versions['react'] = react_version.group(1)
                    if nextjs_version:
                        versions['nextjs'] = nextjs_version.group(1)

            except Exception:
                pass

        # Check HTML source
        try:
            response = requests.get(self.target_url, headers=self.headers, timeout=self.timeout, verify=False)
            if response.status_code == 200:
                # Check for Next.js indicators
                if '_next' in response.text or '__NEXT_DATA__' in response.text:
                    self.info.append("⚠ Next.js framework detected")

                # Try to extract versions from page metadata
                buildId = re.search(r'"buildId":"([^"]+)"', response.text)
                if buildId:
                    self.info.append(f"  Build ID: {buildId.group(1)}")

        except Exception:
            pass

        return versions

    def check_vulnerable_versions(self, versions: Dict[str, str]) -> bool:
        """Check if detected versions are vulnerable"""
        vulnerable = False

        if 'react' in versions:
            react_ver = versions['react']
            print(f"[+] React version detected: {react_ver}")

            # Vulnerable React versions: 19.0.0, 19.1.0, 19.1.1, 19.2.0
            if react_ver.startswith('19.') and react_ver not in ['19.2.1']:
                self.vulnerabilities.append(f"✗ VULNERABLE React version: {react_ver}")
                self.vulnerabilities.append("  Affected: CVE-2025-55182")
                vulnerable = True
            else:
                self.info.append(f"✓ React version appears patched: {react_ver}")

        if 'nextjs' in versions:
            nextjs_ver = versions['nextjs']
            print(f"[+] Next.js version detected: {nextjs_ver}")

            # Parse version
            try:
                major, minor, patch = map(int, nextjs_ver.split('.')[:3])

                # Vulnerable: >=14.3.0-canary.77, >=15, >=16
                # Patched: 16.0.7, 15.5.7, 15.4.8, 15.3.6, 15.2.6, 15.1.9, 15.0.5
                patched_versions = {
                    16: 7,
                    15: [7, 8, 6, 6, 9, 5]  # 15.5.7, 15.4.8, etc
                }

                if major >= 16 and (major, minor, patch) < (16, 0, 7):
                    self.vulnerabilities.append(f"✗ VULNERABLE Next.js version: {nextjs_ver}")
                    self.vulnerabilities.append("  Affected: CVE-2025-66478")
                    vulnerable = True
                elif major == 15:
                    # Check against specific patched versions
                    self.vulnerabilities.append(f"⚠ Next.js 15.x detected: {nextjs_ver} - verify patch level")
                    vulnerable = True

            except Exception:
                self.info.append(f"⚠ Could not parse Next.js version: {nextjs_ver}")

        return vulnerable

    def test_rsc_payload(self) -> bool:
        """Send test RSC payload (non-malicious)"""
        print("[*] Testing RSC payload handling...")

        # Safe test payload - not exploiting, just testing response
        test_payload = '0:["$","div",null,{"children":"test"}]\n'

        rsc_headers = {
            **self.headers,
            'RSC': '1',
            'Content-Type': 'text/x-component',
            'Accept': 'text/x-component',
        }

        try:
            response = requests.post(
                self.target_url,
                headers=rsc_headers,
                data=test_payload,
                timeout=self.timeout,
                verify=False
            )

            # Check if server processes RSC payloads
            if response.status_code == 200 and 'text/x-component' in response.headers.get('Content-Type', ''):
                self.vulnerabilities.append("✗ Server accepts and processes RSC payloads")
                return True

        except Exception as e:
            pass

        return False

    def run_scan(self):
        """Execute complete vulnerability scan"""
        self.print_banner()

        # Step 1: Detect versions
        versions = self.detect_framework_version()

        # Step 2: Check versions
        version_vulnerable = self.check_vulnerable_versions(versions)

        # Step 3: Check for RSC endpoints
        rsc_vulnerable = self.check_rsc_endpoint()

        # Step 4: Check for Server Actions
        actions_vulnerable = self.check_nextjs_server_actions()

        # Step 5: Test RSC payload handling
        payload_vulnerable = self.test_rsc_payload()

        # Print results
        print("\n" + "=" * 70)
        print("SCAN RESULTS")
        print("=" * 70)

        if self.vulnerabilities:
            print("\n🔴 VULNERABILITIES DETECTED:\n")
            for vuln in self.vulnerabilities:
                print(vuln)

        if self.info:
            print("\n🔵 INFORMATION:\n")
            for info in self.info:
                print(info)

        # Final verdict
        print("\n" + "=" * 70)
        if version_vulnerable or rsc_vulnerable or actions_vulnerable or payload_vulnerable:
            print("⚠️  VERDICT: POTENTIALLY VULNERABLE to React2Shell")
            print("\n📋 RECOMMENDATIONS:")
            print("  1. Update React to version 19.2.1 or later")
            print("  2. Update Next.js to patched versions:")
            print("     - 16.0.7+ (for v16.x)")
            print("     - 15.5.7, 15.4.8, 15.3.6, 15.2.6, 15.1.9, 15.0.5 (for v15.x)")
            print("  3. Review and disable unnecessary RSC endpoints")
            print("  4. Implement WAF rules to block malicious RSC payloads")
            print("\n🔗 More info: https://www.wiz.io/blog/critical-vulnerability-in-react-cve-2025-55182")
        else:
            print("✅ VERDICT: No obvious vulnerabilities detected")
            print("\n⚠️  Note: This is a basic scan. Manual verification recommended.")

        print("=" * 70)


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python3 react2shell_scanner.py <target_url>")
        print("Example: python3 react2shell_scanner.py http://example.com:6052")
        sys.exit(1)

    target = sys.argv[1]

    # Disable SSL warnings for testing
    import urllib3
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

    scanner = React2ShellScanner(target)
    scanner.run_scan()
