Sploitus

Exploit for CVE-2026-32475

githubexploit Β· 2026-09-05

Exploit Code

README103 lines
## https://sploitus.com/exploit?id=95A2A3D3-9B4B-54F6-9DF5-31D0EBF77BB9
# CVE-2026-32475 β€” Elementor Pro Forms unauthenticated arbitrary file upload β†’ RCE

A/B Docker lab that reproduces **CVE-2026-32475** (CVSS 9.x, Patchstack; Elementor Pro
Forms File Upload field, affected ≀ 4.2.1, fixed 4.2.2 on 2026-08-19; reported by
Tin Pham / TF1T). Unauthenticated, no user interaction.

> **Authorized testing / education only.** This lab runs entirely on `127.0.0.1` in disposable
> Docker containers. Do not point the PoC at systems you are not explicitly authorized to test.
> `build.sh` fetches the Elementor Pro source from a public GPL mirror; no premium code is
> redistributed in this repository.

## Root cause β€” a validation/move loop desync

`modules/forms/fields/upload.php`. Two loops iterate the **same** reshaped
`$_FILES['form_fields'][$id]` but treat an empty part (`UPLOAD_ERR_NO_FILE`) differently:

```php
// validation()  β€” line ~269
foreach ( $_FILES['form_fields'][ $id ] as $index => $file ) {
    if ( ! $field['required'] && UPLOAD_ERR_NO_FILE === $file['error'] ) {
        return;                         // is_file_type_valid( $field, $file ) ) {           // extension allow/deny list
        $ajax_handler->add_error( $id, 'This file type is not allowed.' );
    }
}

// process_field() β€” line ~418
foreach ( $_FILES['form_fields'][ $id ] as $index => $file ) {
    if ( UPLOAD_ERR_NO_FILE === $file['error'] ) {
        continue;                       // .php`. The `get_blacklist_file_ext()` blocklist
(`php,phtml,pht,shtml,…`) only runs inside `validation()`, so it is completely bypassed.

Fully unauth: `wp_ajax_nopriv_elementor_pro_forms_send_form` (ajax-handler.php:295).

### Why it is RCE, not just a write
`get_ensure_upload_dir()` drops an `.htaccess` in the forms dir containing only
`Options -Indexes` + `Header set Content-Disposition attachment`. That is a *browser download*
hint β€” on Apache + mod_php the `.php` still **executes server-side**; the attacker (curl) simply
receives the command output with a download header. The forms module contains **no `unlink`
cleanup**, so the shell **persists**.

### The one precondition
A published page with an Elementor Pro **Form** widget that has a File Upload field whose
**Required = No** (the `! $field['required']` branch). Field config is read server-side from
`_elementor_data`, so the attacker cannot toggle it β€” but "optional attachment" fields are common.

### Filename recovery (fully-remote, no filesystem access)
Stored name = `uniqid()` = `%08x%05x` = *(unix-second)(microsecond)*. The second is leaked
**exactly** by the HTTP `Date` response header; only the microsecond (0–999999) is unknown β†’
a bounded ≀10⁢ online GET brute. `poc.py --recover` anchors the microsecond on the response
arrival time (co-located / NTP-synced β‡’ seconds–minutes; here: ~26k requests) and turns the
write into remote code execution.

## Lab layout
| Service | Port | Elementor Pro |
|---|---|---|
| `wp-vuln`    | http://127.0.0.1:8975 | **3.6.4 β€” vulnerable** (authentic tree; `validation()` desync identical to ≀4.2.1) |
| `wp-patched` | http://127.0.0.1:8976 | 3.6.4 with the 4.2.2-equivalent fix (`return` β†’ `continue`, aligning the loops) |

Stack: `wordpress:php7.4-apache` (mod_php) + MariaDB 10.6 + Elementor (free) 3.6.8 + Elementor Pro 3.6.4.
Each variant has an admin (`admin`/`labpass`) and a published page **"CVE-2026-32475 Lab"**
(`post_id=5`, `form_id=frm00001`, upload field `field_cv`, Required=No).

> **Version note.** No clean 4.2.1/4.2.2 source is publicly redistributable, so the lab runs the
> authentic **3.6.4** tree, whose `validation()`/`process_field()` desync is byte-identical to the
> code CVE-2026-32475 describes (the bug is long-latent; 4.2.2 aligned the two loops). The A/B
> "patched" build applies exactly that alignment.

## Build & run
Requires Docker + Docker Compose and outbound network (to pull images, Elementor free, and the
Elementor Pro source mirror). `build.sh` clones the authentic Elementor Pro tree automatically.
```bash
git clone https://github.com/dinosn/cve-2026-32475-elementor-pro-lab
cd cve-2026-32475-elementor-pro-lab
bash build.sh          # clone plugin + compose up + install WP/Elementor + create form page (both variants)
bash verify.sh         # A/B proof: vuln writes+executes a shell; patched rejects. Cleans up.
```

## Exploit
```bash
# fully-remote unauthenticated RCE (auto-extracts post_id/form_id/field_id from the page):
python3 poc.py -t http://127.0.0.1:8975 --page-id 5 --recover -c "id; uname -a"

# just prove the blocklist bypass (leave the shell for inspection):
python3 poc.py -t http://127.0.0.1:8975 --post-id 5 --form-id frm00001 --field-id field_cv
```

## Files
- `docker-compose.yml` β€” 2Γ— (WordPress + MariaDB), vuln:8975 / patched:8976
- `elementor-pro-vuln/` β€” authentic Elementor Pro 3.6.4 (vulnerable); **generated** by `build.sh`, git-ignored
- `elementor-pro-patched/` β€” same tree, `validation()` `return`β†’`continue`; **generated** by `patch_pro.sh`, git-ignored
- `evidence/verify_transcript.txt` β€” a captured passing `verify.sh` run
- `install_wp.sh` β€” wp-cli install of WP + Elementor free + activate Pro + create form page + fix uploads perms
- `setup_page.php` β€” builds the Elementor form page with an **optional** upload field
- `patch_pro.sh` β€” derives the patched tree + prints the one-line diff
- `poc.py` β€” unauth desync upload + `uniqid()` filename recovery + RCE
- `verify.sh` β€” A/B proof with an **independent** filesystem oracle (proves execution, not just write)
- `build.sh` β€” one-shot build

## Cleanup / teardown
```bash
cd /root/cve-2026-32475-lab && docker compose down -v
```