
Unabhängiger PoC und reproduzierbares Lab für CVE-2026-97332 — unauthentifizierte Offenlegung privater Dateien in User Private Files < 2.2.0 auf WordPress Multisite.
[!NOTE] Dieses Repository ist eine unabhängige Reproduktion und ein Proof of Concept für CVE-2026-97332.
Ursprüngliche Entdeckung der Schwachstelle: Akshat Parikh (SN1PER). CNA / Advisory: WPScan. Unabhängiger PoC und Reproduktion: Kolya080808.
Unauthentifizierte Offenlegung privater Dateien anderer Benutzer im WordPress-
Plugin User Private Files (wp.org-Slug user-private-files) vor 2.2.0
auf Multisite-Installationen. Das Plugin speichert die privaten Dokumente
jedes Benutzers im einfachen Uploads-Verzeichnisbaum und schützt sie mit einer
einzigen RewriteRule, die an die Root-.htaccess angehängt wird und
Dateianfragen durch eine PHP-Zugriffsprüfung leitet. In einem Multisite-Netzwerk
wird diese Regel nie erreicht: ein einfacher GET — keine Cookies, keine
Header, kein Login — liefert die private Datei eines anderen Benutzers als
statischen Inhalt aus. Broken Access Control, CWE-284, CVSS 3.1 5.3
(AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N).
Dieses Repository enthält einen funktionierenden, abhängigkeitsfreien Python-Exploit und ein Zwei-Container-Docker-Lab (verwundbar 2.1.9 und gepatcht 2.2.0, beide WordPress-Multisite), um den Angriff durchgängig zu reproduzieren.
[!CAUTION] Dieses Material wird ausschließlich für autorisiertes Sicherheitsforschungs- und defensives Testen bereitgestellt (siehe den Haftungsausschluss am Ende). Systeme anzugreifen, die Sie nicht besitzen oder ohne schriftliche Genehmigung betreiben, ist illegal.
Das Plugin speichert private Dateien als gewöhnliche Anhänge in einem flachen,
pro Site angelegten Uploads-Unterverzeichnis — wp-content/uploads/upf-docs/ (Hauptseite) und
wp-content/uploads/sites/N/upf-docs/ (Subsite N) — ohne Isolation pro Benutzer.
Die Zugriffskontrolle liegt nicht im Verzeichnis; sie befindet sich in PHP
(dl-file.php: 403 für anonyme Benutzer, dann eine Autor-/Allow-List-Prüfung) und
wird nur über eine Rewrite-Regel erreicht, die bei der Plugin-Aktivierung in die
Root-.htaccess geschrieben wird:
# appended by 2.1.9 (upvf_actdeact.php), AFTER the WordPress block
RewriteRule ^wp-content/uploads/upf-docs/(.*)$ https://site/?file=$1 [QSA,L]
Bei Multisite wird dies nie ausgelöst, aus zwei unabhängigen Gründen:
Die netzwerkweite .htaccess fängt existierende Dateien zuerst ab. WordPress
Multisite schreibt seine eigene Regelmenge in die .htaccess im Stammverzeichnis:
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
Eine Anfrage, die auf eine physisch existierende Datei abgebildet wird, beendet das Rewriting genau dort — Apache liefert sie als statischen Inhalt aus, und mod_rewrite erreicht den Block des Plugins am Ende der Datei nie. (Single-Site-WordPress verwendet negative Bedingungen !-f/!-d, weshalb dasselbe Plugin dort funktioniert.) Dies legt bereits Dateien der Hauptseite des Netzwerks offen.
Pfade von Subseiten passen überhaupt nicht auf das Muster. Bei Multisite hat jede Seite ihre eigenen Uploads — wp-content/uploads/sites/N/…, plus das URL-Präfix /siteN/ in Subdirectory-Netzwerken — während die Regel fest auf ^wp-content/uploads/upf-docs/ von der Basis-URL der Hauptseite verdrahtet ist. Selbst nachdem das interne Rewriting des Netzwerks das Präfix /siteN/ entfernt hat, passt das Segment sites/N/ immer noch nicht.
Somit ruft für jedes Multisite-Netzwerk (Subdirectory und Subdomain gleichermaßen) ein nicht authentifizierter Angreifer private Dateien direkt ab:
GET /wp-content/uploads/upf-docs/victim.pdf HTTP/1.1 (main site)
GET /site2/wp-content/uploads/sites/2/upf-docs/victim.pdf HTTP/1.1 (subsite)
Die Antwort ist 200 mit dem Dateiinhalt — direkt von Apache aus
uploads/ ausgeliefert, die Zugriffsprüfung des Plugins wird nie ausgeführt.
Nichts an der URL ist geheim. Das Plugin bildet Namen aus der numerischen Benutzer-ID des Eigentümers und der Unix-Sekunde des Uploads:
| Generator (2.1.9) | Name auf der Festplatte |
|---|---|
moderne UI, inc/functions-file.php:73-77 | usr{ID}_{unixtime}_{original} |
klassisches Template, inc/classic-user-functions.php | user_{ID}_doc_{unixtime}.{ext} |
Benutzer-IDs in einem Multisite-Netzwerk sind kleine fortlaufende Ganzzahlen
aus einer gemeinsamen wp_users-Tabelle, sodass ein Angreifer nur eine grobe
Upload-Zeit (ein Fenster von ±2 Minuten entspricht ≈240 Anfragen) und
entweder den ursprünglichen Dateinamen oder nur dessen Erweiterung benötigt.
Kollisions-Suffixe (-1, -2) existieren, sind aber selten.
Version 2.2.0 ersetzte das Umschreiben der Root-Datei durch eine
verzeichnisweite upf-docs/.htaccess — RewriteRule ^(.*)$ {home}/?file=$1 [R=302,QSA,L],
plus Require all denied, wenn mod_rewrite fehlt —, die bei jedem
admin_init neu erstellt wird (selbstheilend, pro Subsite). Eine
verzeichnisbezogene Regel greift für jeden physischen Pfad unter upf-docs/
unabhängig vom URL-Präfix, sodass der direkte GET nun zu einem 302 auf die
Zugriffsprüfung und für anonyme Benutzer zu einem 403 wird. Dateien werden
nicht verschoben; nur der Abfangmechanismus ändert sich. Diese Unterscheidung
zwischen 302 und statischer Auslieferung nutzt der Exploit auch als
verhaltensbasierten Detektor.
.htaccess liest und die Datei bei der Aktivierung beschreibbar war (nginx
benötigt das manuelle Rewrite des Herstellers — siehe Plugin-Readme).exploit.py nutzt ausschließlich die Python-3-Standardbibliothek (http.client), folgt keinen
Weiterleitungen und sendet keine Cookies: