
Analisi della causa principale, verifica passiva della versione e PoC di laboratorio per CVE-2026-18322, un'escalation di privilegi non autenticata nel plugin WordPress Smart Popup di Supsystic.
Ricerca di sicurezza su CVE-2026-18322, una vulnerabilità di privilege-escalation
non autenticata nel plugin WordPress Smart Popup by Supsystic
(popup-by-supsystic). Questo repository contiene un'analisi della causa radice derivata dal
diff del codice sorgente upstream, le patch estratte, uno strumento di rilevamento (fingerprinter passivo di versione) per identificare le installazioni affette e un laboratorio per il PoC di sfruttamento.
La vulnerabilità è pubblica e corretta. Questo lavoro è pubblicato per uso difensivo: aiutare gli operatori a trovare e correggere gli host affetti.
| Intervallo di versioni | Stato |
|---|---|
< 1.13.0 | Vulnerabile |
>= 1.13.0 | Corretta |
La 1.13.0 (rilasciata il 31.07.2026) è la prima release corretta; ripara tutti e tre gli anelli della catena descritta di seguito. Vedi §1 dell'analisi.
Tre debolezze indipendenti si compongono nella creazione non autenticata di un amministratore:
1. Una collisione nella mappa dei permessi — havePermissions() combinava due mappe di permessi con
array_merge(). Entrambe usano la chiave stringa PPS_USERLEVELS ('userlevels'), e
array_merge() sovrascrive le chiavi stringa, quindi la lista breve predefinita del controller di base
sostituiva in blocco la lista del modulo popup — rimuovendo silenziosamente la restrizione
di amministratore da save e da altri nove metodi:
$permissions = $mod->getController()->getPermissions(); // [... 'save' ...]
$permissionsBase = $mod->getController()->getBasePermissions(); // ['getListForTbl','removeGroup','clear']
$permissions = array_merge($permissions, $permissionsBase); // base vince — 'save' è sparito
Il controllo fallisce aperto: un'azione assente dalla mappa non viene mai negata.
2. Un nonce riutilizzabile inviato a estranei — save richiedeva comunque un pps_nonce, ma l'email
di conferma dell'iscrizione incorporava esattamente quell'azione nonce. Per gli utenti non autenticati un
nonce di WordPress è legato a uid=0, quindi il token generato per un iscritto anonimo
risulta valido per qualsiasi attaccante anonimo.
3. Nessuna allowlist di ruoli lato server — createWpSubscriber() passava il ruolo configurato
direttamente a WP_User::set_role(). La lista di ruoli sicuri del plugin (che esclude
administrator) era applicata solo durante il rendering del menu a tendina di amministrazione — un controllo solo di UI.
Concatenate: iscriviti per ottenere un nonce → riproducilo contro popup::save per impostare
sub_wp_create_user_role=administrator → attiva il flusso di iscrizione → account amministratore
persistente.
Procedura completa con riferimenti a file/righe: docs/ANALYSIS.md.
poc/cve_2026_18322_check.py determina se un sito esegue una versione affetta. Non tenta mai
lo sfruttamento: emette semplici GET HTTP e legge la versione che l'installazione
pubblica su se stessa.
git clone <this-repo> && cd CVE-2026-18322
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
requests è consigliato ma opzionale — lo strumento ripiega sulla libreria standard.
# Singolo target
python3 poc/cve_2026_18322_check.py https://example.com
# Con evidenze di supporto
python3 poc/cve_2026_18322_check.py https://example.com -v
# Molti target, in concorrenza, esportando i risultati
python3 poc/cve_2026_18322_check.py -f targets.txt -t 20 --json results.json
python3 poc/cve_2026_18322_check.py -f targets.txt --csv results.csv --only-vulnerable
# Attraverso un proxy, ignorando gli errori TLS (uso in laboratorio)
python3 poc/cve_2026_18322_check.py https://staging.internal -k --proxy http://127.0.0.1:8080
I target possono essere forniti come argomenti o tramite -f (- legge da stdin). Un hostname nudo è
assunto come https://.
$ python3 poc/cve_2026_18322_check.py https://example.com -v
[VULNERABLE] https://example.com/ (Smart Popup by Supsystic 1.11.2) < 1.13.0 affected by CVE-2026-18322; upgrade to 1.13.0+
- asset-version: plugin asset enqueued with ?ver=1.11.2 (https://example.com/)
- homepage-reference: references /plugins/popup-by-supsystic/ (https://example.com/)
- readme: plugin readme.txt is publicly readable (https://example.com/wp-content/plugins/popup-by-supsystic/readme.txt)
- readme-stable-tag: Stable tag: 1.11.2 (.../readme.txt)
Codici di uscita: 0 = nessun target vulnerabile, 1 = almeno uno vulnerabile, 2 = errore di utilizzo.
Due segnali passivi, entrambi semplici GET HTTP di risorse pubbliche:
?ver=PPS_VERSION
(classes/frame.php:410,473), quindi la homepage spesso rivela la versione esatta.readme.txt — Stable tag: è autorevole e ha la precedenza su un asset in cache
potenzialmente obsoleto; il changelog funge da fallback.La scansione della homepage scopre anche directory wp-content rinominate (es. /app/ di Bedrock)
così la ricerca del readme segue il layout reale del sito.
Lo strumento di rilevamento non tenta lo sfruttamento. Nessun exploit weaponizzato per questa CVE è pubblicato qui: l'unico codice che esegue la catena completa è rigidamente vincolato al laboratorio locale (vedi sotto).
Il checker passivo esegue solo il fingerprinting della versione. Il suo HttpClient non espone nessun
verbo HTTP diverso da GET — imposto per costruzione e verificato nella suite di test, così
non può emettere una richiesta che modifica lo stato nemmeno per errore. Non chiama mai popup::save,
non invia un parametro sub_wp_create_user_role, non invia un modulo di iscrizione, non attiva un'email
di conferma, né crea o modifica alcun utente, popup o impostazione. Legge due risorse pubbliche — la homepage e readme.txt — e nient'altro.
Questa sicurezza ha un costo, dichiarato apertamente: il verdetto vale solo quanto i metadati di versione che l'host pubblica. Compromessi e modalità di fallimento: §9.
lab/exploit_full_chain.py è l'unico pezzo di codice qui che esegue l'attacco reale,
e crea un amministratore WordPress. È pubblicato affinché l'analisi possa essere
riprodotta anziché presa per fede — un'affermazione su un bug di autorizzazione che non può essere
dimostrata è un'asserzione, non una scoperta. È scritto per lo stack usa e getta in
lab/ e per nient'altro:
--lab-confirm è obbligatorio. Entrambi i controlli vengono eseguiti in assert_lab_target() prima della
prima richiesta HTTP, quindi un'invocazione rifiutata non tocca affatto il target.pps_nonce riutilizzabile dall'email
di conferma dell'iscrizione tramite l'API Mailpit del laboratorio
(http://localhost:8025/api/v1/…). Non esiste un percorso di codice per recuperare quell'email da
qualsiasi altro luogo, quindi la catena non ha un primo passo contro un host al di fuori del laboratorio.lab/docker-compose.yml si lega a 127.0.0.1.Così com'è distribuito, quindi, non può essere puntato a un sito reale — si ferma a
REFUSING TO RUN prima di emettere una richiesta:
$ python3 exploit_full_chain.py https://example.com --lab-confirm
REFUSING TO RUN: example.com resolves to non-local address(es) 104.20.23.154, ...
This script creates a WordPress administrator and is for the local lab only.
To test a real site, use the non-destructive checker in ../poc/.
Queste protezioni sono portanti anziché decorative: lo script è la catena reale, e
ciò che lo mantiene una dimostrazione è che non verrà eseguito al di fuori di uno stack locale usa e getta.
Si prega di non rimuoverle, e vedi SECURITY.md — modifiche che
le indeboliscono, o un exploit autonomo pensato per essere eseguito al di fuori del laboratorio, sono fuori ambito per
questo repository. Per scoprire se un sito reale è affetto, usa poc/ — il
checker risponde alla stessa domanda senza toccare nulla.
Per una risposta autorevole su un host che controlli:
grep -n "array_merge(\$permissions, \$permissionsBase)" \
wp-content/plugins/popup-by-supsystic/classes/frame.php
Qualsiasi output significa che l'host è vulnerabile — quella riga è precisamente ciò che la 1.13.0 ha sostituito.
Usare solo contro sistemi di tua proprietà o per cui sei esplicitamente autorizzato a testare. Vedi SECURITY.md.
lab/ contiene uno stack Docker usa e getta che esegue la vulnerabilità end-to-end, così
l'analisi può essere verificata anziché presa per fede:
cd lab
./setup.sh # WordPress + plugin 1.12.0
../.venv/bin/python exploit_full_chain.py http://localhost:8080 --lab-confirm
./verify.sh # confirm from the database
./teardown.sh
Il suo output più utile è il confronto delle versioni — richieste identiche contro tre release:
./setup.sh --plugin-version 1.11.2 # exploit succeeds
./setup.sh --plugin-version 1.12.0 # exploit succeeds
./setup.sh --plugin-version 1.13.0 # exploit fails at phase 2
Il laboratorio è intenzionalmente vulnerabile e il suo script di exploit crea un amministratore WordPress. Tutto si lega a
127.0.0.1, e lo script non verrà eseguito contro altro che un laboratorio locale — vedi l'exploit del laboratorio viene eseguito solo nel laboratorio. Non è uno scanner; per controllare un sito reale, usapoc/.
sub_wp_create_user_role impostato a un ruolo privilegiato, e richieste POST a
admin-ajax.php che trasportano pl=pps&mod=popup&action=save.Query e grep sui log: §10.
.
├── README.md
├── SECURITY.md Scope, authorized-use policy, disclosure
├── LICENSE MIT
├── requirements.txt
├── docs/
│ └── ANALYSIS.md Full root-cause analysis and fix rationale
├── patches/
│ ├── 01-frame-permission-merge.diff array_merge() → _mergePermissions()
│ ├── 02-subscribe-role-allowlist-and-nonce.diff Role allowlist + dedicated nonce
│ └── 03-v1.12.0-confirmation-page-hardening.diff Unrelated hardening added in 1.12.0
├── poc/
│ └── cve_2026_18322_check.py Passive version fingerprint (GET only)
├── lab/ Disposable Docker lab — INTENTIONALLY VULNERABLE
│ ├── docker-compose.yml MariaDB + WordPress 1.12.0 + Mailpit (loopback only)
│ ├── setup.sh / teardown.sh Bring the lab up / destroy it
│ ├── exploit_full_chain.py Full attack chain — CREATES AN ADMIN; lab-gated
│ └── verify.sh Independent confirmation from the database
├── scripts/
│ └── fetch_versions.sh Export plugin versions from SVN and regenerate diffs
└── tests/
└── test_check.py Offline unit tests for the passive checker
pip install -r requirements.txt pytest
python3 -m pytest tests/ -v
La suite è completamente offline — usa fixture registrate, mai target live. Copre il triage delle versioni attraverso il confine della 1.13.0, la normalizzazione dei target, e le regole di evidenza che decidono ogni stato.
Le proprietà di sicurezza sono verificate, non solo documentate: che una scansione completa non emetta
richieste che modificano lo stato, e che HttpClient non esponga alcun verbo HTTP diverso da GET.
Due regressioni trovate durante lo sviluppo sono bloccate: un readme.txt con CRLF che vanifica
il parsing ancorato alle righe, e una pagina di errore HTTP conteggiata erroneamente come evidenza del plugin.
Per riprodurre l'analisi dalle fonti upstream:
./scripts/fetch_versions.sh # exports 1.11.2 / 1.12.0 / 1.13.0 and regenerates patches/
MIT — vedi LICENSE. Il plugin analizzato è GPLv2-or-later e non è
ridistribuito qui; scripts/fetch_versions.sh lo recupera dal repository SVN ufficiale di WordPress.
| Stato | Significato |
|---|
VULNERABLE | Plugin rilevato a < 1.13.0 |
NOT_VULNERABLE | Plugin rilevato a >= 1.13.0 |
PLUGIN_DETECTED_VERSION_UNKNOWN | Plugin presente, versione non determinabile — verificare manualmente |
PLUGIN_NOT_DETECTED | Nessuna evidenza del plugin (non prova dell'assenza) |
ERROR | Target irraggiungibile o malformato |