
Root-Cause-Analyse, passiver Versionsprüfer und Lab-PoC für CVE-2026-18322, eine unauthentifizierte Rechteausweitung im WordPress-Plugin Smart Popup von Supsystic.
Sicherheitsforschung zu CVE-2026-18322, einer nicht authentifizierten Privilegienausweitungs-
Schwachstelle im WordPress-Plugin Smart Popup by Supsystic
(popup-by-supsystic). Dieses Repository enthält eine Ursachenanalyse, die aus dem
Upstream-Quellcode-Diff abgeleitet wurde, die extrahierten Patches, ein Erkennungswerkzeug (passiver Versions-Fingerprinter) zur Identifizierung betroffener Installationen und ein Labor für den Exploit-PoC.
Die Schwachstelle ist öffentlich und gepatcht. Diese Arbeit wird für den defensiven Einsatz veröffentlicht: um Betreibern zu helfen, betroffene Hosts zu finden und zu beheben.
| Versionsbereich | Status |
|---|---|
< 1.13.0 | Anfällig |
>= 1.13.0 | Behoben |
1.13.0 (veröffentlicht am 31.07.2026) ist die erste gepatchte Version; sie repariert alle drei Glieder der unten beschriebenen Kette. Siehe §1 der Analyse.
Drei unabhängige Schwächen setzen sich zu einer nicht authentifizierten Administrator-Erstellung zusammen:
1. Eine Berechtigungs-Map-Kollision — havePermissions() kombinierte zwei Berechtigungs-Maps mit
array_merge(). Beide verwenden den String-Schlüssel PPS_USERLEVELS ('userlevels'), und
array_merge() überschreibt String-Schlüssel, sodass die kurze Standardliste des Basis-Controllers die Liste des Popup-Moduls vollständig ersetzte — wodurch die Administrator-
Beschränkung stillschweigend aus save und neun weiteren Methoden entfernt wurde:
$permissions = $mod->getController()->getPermissions(); // [... 'save' ...]
$permissionsBase = $mod->getController()->getBasePermissions(); // ['getListForTbl','removeGroup','clear']
$permissions = array_merge($permissions, $permissionsBase); // base wins — 'save' is gone
Die Prüfung schlägt offen fehl: Eine Aktion, die nicht in der Map enthalten ist, wird niemals verweigert.
2. Ein wiederverwendbarer Nonce, der an Fremde gemailt wird — save erforderte weiterhin einen
pps_nonce, aber die Abonnement-Bestätigungs-E-Mail bettete genau diese Nonce-Aktion ein. Für abgemeldete Benutzer ist ein WordPress-Nonce auf uid=0 geschlüsselt, sodass das für einen anonymen Abonnenten erzeugte Token für jeden anonymen Angreifer gültig ist.
3. Keine serverseitige Rollen-Allowlist — createWpSubscriber() übergab die konfigurierte Rolle
direkt an WP_User::set_role(). Die sichere Rollenliste des Plugins (die administrator ausschließt) wurde nur beim Rendern des Admin-Dropdowns angewendet — eine reine UI-Kontrolle.
Verkettet: Abonnieren, um einen Nonce zu erhalten → diesen gegen popup::save wiederverwenden, um
sub_wp_create_user_role=administrator zu setzen → den Abonnement-Ablauf auslösen → persistentes Admin-
Konto.
Vollständige Schritt-für-Schritt-Anleitung mit Datei-/Zeilenangaben: docs/ANALYSIS.md.
poc/cve_2026_18322_check.py ermittelt, ob eine Website eine betroffene Version ausführt. Er versucht
niemals eine Ausnutzung: Er sendet einfache HTTP-GET-Anfragen und liest die Version, die die Installation über sich selbst veröffentlicht.
git clone <this-repo> && cd CVE-2026-18322
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
requests wird empfohlen, ist aber optional — das Tool greift auf die Standardbibliothek zurück.
# Single target
python3 poc/cve_2026_18322_check.py https://example.com
# With supporting evidence
python3 poc/cve_2026_18322_check.py https://example.com -v
# Many targets, concurrently, exporting results
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
# Through a proxy, ignoring TLS errors (lab use)
python3 poc/cve_2026_18322_check.py https://staging.internal -k --proxy http://127.0.0.1:8080
Ziele können als Argumente oder über -f angegeben werden (- liest stdin). Ein bloßer Hostname wird
als https:// angenommen.
$ 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)
Exit-Codes: 0 = keine anfälligen Ziele, 1 = mindestens ein anfälliges, 2 = Verwendungsfehler.
Zwei passive Signale, beide einfache HTTP-GET-Anfragen an öffentliche Ressourcen:
?ver=PPS_VERSION
(classes/frame.php:410,473), sodass die Startseite oft die exakte Version preisgibt.readme.txt — Stable tag: ist maßgeblich und hat Vorrang vor einem
möglicherweise veralteten gecachten Asset; das Changelog dient als Fallback.Der Startseiten-Scan entdeckt auch umbenannte wp-content-Verzeichnisse (z. B. Bedrocks /app/),
sodass die readme-Suche dem tatsächlichen Layout der Website folgt.
Das Erkennungswerkzeug versucht keine Ausnutzung. Hier wird kein weaponisierter Exploit für diese CVE veröffentlicht: Der einzige Code, der die vollständige Kette ausführt, ist fest auf das lokale Labor beschränkt (siehe unten).
Der passive Checker führt nur Versions-Fingerprinting durch. Sein HttpClient bietet kein
anderes HTTP-Verb als GET — konstruktiv erzwungen und in der Testsuite zugesichert, sodass
er selbst versehentlich keine zustandsändernde Anfrage senden kann. Er ruft niemals popup::save auf,
sendet keinen sub_wp_create_user_role-Parameter, übermittelt kein Abonnement-Formular, löst keine
Bestätigungs-E-Mail aus und erstellt oder ändert keinen Benutzer, kein Popup und keine Einstellung. Er liest zwei öffentliche
Ressourcen — die Startseite und readme.txt — und nichts anderes.
Diese Sicherheit hat ihren Preis, offen gesagt: Das Urteil ist nur so gut wie die Versions- Metadaten, die der Host veröffentlicht. Abwägungen und Fehlermodi: §9.
lab/exploit_full_chain.py ist das eine Stück Code hier, das den tatsächlichen Angriff ausführt,
und es erstellt einen WordPress-Administrator. Es wird veröffentlicht, damit die Analyse reproduziert
statt auf Vertrauen hingenommen werden kann — eine Behauptung über einen Autorisierungsfehler, die nicht
demonstriert werden kann, ist eine Behauptung, kein Befund. Es ist für den Wegwerf-Stack in
lab/ geschrieben und für nichts anderes:
--lab-confirm ist obligatorisch. Beide Prüfungen laufen in assert_lab_target() vor der
ersten HTTP-Anfrage, sodass ein abgelehnter Aufruf das Ziel überhaupt nicht berührt.pps_nonce aus der
Abonnement-Bestätigungs-E-Mail über die Mailpit-API des Labors
(http://localhost:8025/api/v1/…). Es gibt keinen Codepfad, um diese E-Mail von
woanders abzurufen, sodass die Kette keinen ersten Schritt gegen einen Host außerhalb des Labors hat.lab/docker-compose.yml bindet an 127.0.0.1.Wie ausgeliefert kann es daher nicht auf eine echte Website gerichtet werden — es stoppt bei
REFUSING TO RUN, bevor eine Anfrage gesendet wird:
$ 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/.
Diese Schutzmaßnahmen sind tragend statt dekorativ: Das Skript ist die echte Kette, und
was es zu einer Demonstration macht, ist, dass es außerhalb eines Wegwerf-Local-Stacks nicht läuft.
Bitte entfernen Sie sie nicht, und siehe SECURITY.md — Änderungen, die
sie abschwächen, oder ein eigenständiger Exploit, der außerhalb des Labors laufen soll, liegen außerhalb des Geltungsbereichs
dieses Repositories. Um herauszufinden, ob eine echte Website betroffen ist, verwenden Sie poc/ — der
Checker beantwortet dieselbe Frage, ohne etwas zu berühren.
Für eine maßgebliche Antwort auf einem Host, den Sie kontrollieren:
grep -n "array_merge(\$permissions, \$permissionsBase)" \
wp-content/plugins/popup-by-supsystic/classes/frame.php
Jede Ausgabe bedeutet, dass der Host anfällig ist — genau diese Zeile hat 1.13.0 ersetzt.
Nur gegen Systeme verwenden, die Sie besitzen oder für deren Test Sie ausdrücklich autorisiert sind. Siehe SECURITY.md.
lab/ enthält einen Wegwerf-Docker-Stack, der die Schwachstelle Ende-zu-Ende ausführt, sodass
die Analyse verifiziert statt auf Vertrauen hingenommen werden kann:
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
Seine nützlichste Ausgabe ist der Versionsvergleich — identische Anfragen gegen drei Releases:
./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
Das Labor ist absichtlich anfällig und sein Exploit-Skript erstellt einen WordPress- Administrator. Alles bindet an
127.0.0.1, und das Skript läuft gegen nichts außer einem lokalen Labor — siehe der Labor-Exploit läuft nur im Labor. Es ist kein Scanner; um eine echte Website zu prüfen, verwenden Siepoc/.
sub_wp_create_user_role auf eine privilegierte Rolle gesetzt, und POST-Anfragen an
admin-ajax.php mit pl=pps&mod=popup&action=save.Abfragen und Log-Greps: §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
Die Suite ist vollständig offline — sie verwendet aufgezeichnete Fixtures, niemals Live-Ziele. Sie deckt Versions-Triage über die 1.13.0-Grenze hinweg, Ziel-Normalisierung und die Evidenzregeln ab, die jeden Status entscheiden.
Sicherheitseigenschaften werden zugesichert, nicht nur dokumentiert: dass ein vollständiger Scan keine
zustandsändernden Anfragen sendet und dass HttpClient kein anderes HTTP-Verb als GET bietet.
Zwei während der Entwicklung gefundene Regressionen sind festgeschrieben: ein CRLF-readme.txt, das
zeilenverankertes Parsing vereitelt, und eine HTTP-Fehlerseite, die fälschlich als Plugin-Evidenz gezählt wird.
Um die Analyse aus Upstream-Quellen zu reproduzieren:
./scripts/fetch_versions.sh # exports 1.11.2 / 1.12.0 / 1.13.0 and regenerates patches/
MIT — siehe LICENSE. Das analysierte Plugin ist GPLv2-or-later und wird hier nicht
weiterverbreitet; scripts/fetch_versions.sh ruft es aus dem offiziellen WordPress-SVN-
Repository ab.
| Status | Bedeutung |
|---|
VULNERABLE | Plugin bei < 1.13.0 erkannt |
NOT_VULNERABLE | Plugin bei >= 1.13.0 erkannt |
PLUGIN_DETECTED_VERSION_UNKNOWN | Plugin vorhanden, Version nicht bestimmbar — manuell überprüfen |
PLUGIN_NOT_DETECTED | Kein Hinweis auf das Plugin (kein Beweis für Abwesenheit) |
ERROR | Ziel nicht erreichbar oder fehlerhaft |