
Exploit proof-of-concept e analisi tecnica per CVE-2023-6553, una vulnerabilità di inclusione di file PHP non autenticata che consente l'esecuzione remota di codice nel plugin WordPress Backup Migration <=1.3.7.
Plugin: Backup Migration (backup-backup) ≤ 1.3.7
CVSS: 9.8 (Critico)
CWE: CWE-98 — Controllo improprio del nome file per l'istruzione Include/Require
Requisito di autenticazione: Nessuno
Impatto: Esecuzione di codice in remoto
Backup Migration è un plugin WordPress abbastanza popolare (~90.000+ installazioni attive) che aiuta gli utenti a creare copie di backup. Durante il processo di backup, il plugin ha un file chiamato backup-heart.php che viene eseguito in background: riceve le informazioni di configurazione tramite gli header HTTP per sapere quale directory deve essere sottoposta a backup, dove si trova il file di configurazione, ecc.
Il problema sta nel fatto che questo file si fida completamente degli header HTTP inviati dal client, prende il valore dell'header e lo inserisce direttamente nel percorso del file, quindi usa require_once() per caricare il file da quel percorso. Un attaccante deve solo inviare l'header Content-Dir che punta a una directory contenente codice PHP dannoso → il server lo include ed esegue automaticamente.
È importante notare che il file backup-heart.php non richiede autenticazione — controlla solo se il metodo della richiesta è POST, senza verificare alcun nonce o privilegi utente. Chiunque su Internet può inviargli una richiesta.
⇒ Questa è una vulnerabilità zero-click.
| Attributo | Valore |
|---|---|
| CVE ID | CVE-2023-6553 |
| Punteggio CVSS | 9.8 (Critico) |
| Plugin | backup-backup (Backup Migration) ≤ 1.3.7 |
| Autenticazione | Non richiesta |
| Interazione utente | Nessuna (zero-click) |
| Corretto | Versione 1.3.8 |
PHP ha funzioni come include(), require(), require_once() usate per includere altri file PHP nel programma in esecuzione. Quando il percorso del file passato a queste funzioni proviene dall'input utente senza validazione, un attaccante può costringere il server a includere qualsiasi file desideri:
allow_url_include=On (di solito disabilitato per impostazione predefinita).Questa CVE rientra nella categoria LFI — l'attaccante controlla il percorso passato a require_once() che punta a un file PHP che l'attaccante è riuscito a scrivere sul server.
Molti sviluppatori pensano che gli header HTTP siano metadati "interni" noti solo al server e al client. In realtà, gli attaccanti controllano il 100% del contenuto degli header — possono impostare qualsiasi nome e valore di header. Fidarsi degli header è come fidarsi dell'input dei moduli — deve essere validato.
define() e le costanti PHPdefine('NAME', $value) crea una costante usata in tutta l'applicazione. Una volta definita, il valore non può essere modificato. Se $value proviene da un attaccante, ogni punto in cui viene usata quella costante è compromesso.
Ho iniziato usando grep per cercare tutte le istruzioni require e include nel plugin:
grep -rn "require\|include" includes/

I risultati della ricerca hanno restituito molte chiamate require/include. Esaminandole, la maggior parte erano chiamate include_once in banner/misc.php e banner/views/index.php — appartengono al codice di rendering dell'interfaccia admin con percorsi hardcoded, quindi non sfruttabili.
Tuttavia, 2 righe in backup-heart.php hanno attirato la mia attenzione:
includes/backup-heart.php:64: define('BMI_INCLUDES', BMI_ROOT_DIR . 'includes');
includes/backup-heart.php:118: require_once BMI_INCLUDES . '/bypasser.php';
La riga 118 usa require_once con la costante BMI_INCLUDES — se questa costante fosse hardcoded, sarebbe sicura. Ma guardando alla riga 64, ho visto che BMI_INCLUDES è costruita da un'altra costante, BMI_ROOT_DIR. Quindi dobbiamo risalire: dove viene assegnato il valore a BMI_ROOT_DIR?
Ho usato di nuovo grep per tracciarla:
grep -n "BMI_ROOT_DIR" includes/backup-heart.php
Risultati:

Line 62: define('BMI_ROOT_DIR', $fields['content-dir']);
Line 64: define('BMI_INCLUDES', BMI_ROOT_DIR . 'includes');
La riga 62 mostra che BMI_ROOT_DIR prende il suo valore da $fields['content-dir']. Questa è una variabile, non un valore fisso — dobbiamo aprire il file e vedere cosa contiene $fields.
Ho aperto backup-heart.php in VS Code alla riga 62:
// Line 62
define('BMI_ROOT_DIR', $fields['content-dir']);
// Line 64
define('BMI_INCLUDES', BMI_ROOT_DIR. 'includes');

È chiaramente visibile che $fields['content-dir'] finisce direttamente in define(). Ora dobbiamo determinare dove viene assegnata la variabile $fields:
// Lines 7-9: Only checks POST method
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit;
}
// Lines 30-33: Reads ALL HTTP headers from the request
if (isFunctionEnabled('getallheaders')) {
$fields= getallheaders();
}
// Lines 42-46: Lowercases header names
foreach ($fieldsas $key=> $value) {
$buffer= $value;
unset($fields[$key]);
$fields[strtolower($key)] = $value;
}


A questo punto, la causa principale diventa chiarissima: $fields contiene tutti gli header HTTP recuperati tramite getallheaders() — completamente controllati dal client. Non c'è wp_verify_nonce(), non c'è current_user_can(), nessun controllo del percorso valido — verifica semplicemente il metodo POST e legge gli header direttamente.
Attacker sends POST request with header Content-Dir: /path/to/attacker/
↓
getallheaders() reads raw headers → $fields['content-dir'] = "/path/to/attacker/"
↓
define('BMI_ROOT_DIR', "/path/to/attacker/") ← no validation
↓
define('BMI_INCLUDES', "/path/to/attacker/includes")
↓
require_once "/path/to/attacker/includes/bypasser.php" ← executes PHP
↓
Attacker code runs with www-data privileges → RCE
Riepilogo della causa principale: Header HTTP → define() → require_once(), senza alcun passaggio di validazione in mezzo.
Ho usato Xdebug + VS Code per confermare visivamente il flusso di attacco. Ho impostato 2 breakpoint alla riga 62 e alla riga 118 in backup-heart.php, poi ho inviato la richiesta exploit usando curl.
Breakpoint 1 — Riga 62:
Il debugger si è fermato proprio su define('BMI_ROOT_DIR', $fields['content-dir']). Espandendo la variabile $fields nel pannello Variables è emerso un array di 22 elementi — contenente tutti gli header HTTP inviati dal client. Nello specifico: