TL;DR: Ein Buchungsplugin ist Formular, Datensammlung und Verwaltungskonsole in einem. Ich fand Export-Endpunkte, die vor der Authentifizierung antworteten, und einen unbeaufsichtigten Task-Planer, und ich erkläre die Behebung beider.
Ausgangslage
Ich habe das Ziel als Black-Box geprüft: keine Zugangsdaten, nur die öffentlich sichtbare Fläche. Das ist, was ich sah, bevor ich etwas angefasst habe.
Ein Weiterbildungsträger mit öffentlichem Kurskatalog, Online-Anmeldung, Zahlungsübergabe und herunterladbaren Unterlagen. Geprüft wurden die Verwaltungsoberfläche des Plugins, unauthentifizierte Endpunkte, Upload-Verarbeitung, Task-Planer und Header-Status.
Ich habe alle identifizierenden Informationen entfernt. Das Ziel wird ausschließlich nach Branche und Technologieklasse beschrieben, damit das Muster übertragbar bleibt.
Das Muster
Das Muster, das ich erkannt habe — und das ich in ähnlichen Landschaften immer wieder finde:
Ein Buchungsplugin ist drei Dinge zugleich: ein öffentliches Formular, eine Datensammlung und eine Verwaltungskonsole. Installationen optimieren auf das Erste, weil das verkauft wird. Die anderen beiden erben die Autorisierung, die der Plugin-Autor für angemessen hielt, und die Konsole ist der Ort des Wertes.
Die typische Freigabe ist die Export- und Berichtsoberfläche. Bietet ein Plugin Teilnehmerlisten, Rechnungen oder CSV-Exporte, existieren diese Endpunkte, sie sind erreichbar, und sie antworten häufig vor der Authentifizierungsprüfung, weil diese im Seitencontroller statt im Endpunkt sitzt.
Der Upload-Pfad verdient eigene Aufmerksamkeit. Kursunterlagen sind ein legitimer Grund, Dateien anzunehmen, was den Endpunkt zu einem Dauerfeature macht. Die interessanten Fragen sind nicht, ob Uploads funktionieren, sondern was mit dem geparsten Ergebnis geschieht und ob die Endungsliste serverseitig durchgesetzt wird.
Befunde
| Schweregrad | Befund | Nachweis |
|---|---|---|
| HOCH | Teilnehmer-Export ohne Authentifizierung erreichbar | Vollständige Teilnehmerliste inklusive Kontaktdaten |
| HOCH | Unauthentifizierter Task-Planer-Endpunkt | Plugin-Code wird zeitgesteuert ohne Bediener ausgeführt |
| MITTEL | Upload-Verzeichnis innerhalb des Webroots | Hochgeladenes Material direkt ausgeliefert, wenn die Endungsprüfung versagt |
| MITTEL | Kein Frameschutz auf Verwaltungsansichten | Verwaltungsaktionen von Dritten einbettbar |
| MITTEL | Zahlungsübergabe ohne signierte Callback-Prüfung | Bestellstatus aus clientgelieferten Daten ableitbar |
| NIEDRIG | Plugin- und Core-Version in Asset-Pfaden sichtbar | Genauer Build identifizierbar |
Der Angriffspfad
Reproduktion im Labor
Jeder Befehl unten zielt auf einen Lab-Container, den ich selbst kontrolliere. Nichts davon ist auf ein laufendes System gerichtet.
Die Plugin-Oberfläche einer Laborinstanz enumerieren
# Ziel: ein Labor-WordPress mit derselben Plugin-Familie
# 1. Routen finden, die das Plugin registriert
$ curl -s http://localhost:8080/wp-json/ \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print([r for ns in d["namespaces"] if "book" in ns or "event" in ns or "tribe" in ns])'
# 2. jede gefundene Route ohne Authentifizierung abfragen
$ curl -s http://localhost:8080/wp-json/ | python3 -c '
import json,sys
d=json.load(sys.stdin)
for ns in d["namespaces"]:
if any(k in ns for k in ("book","event","tribe")): print(ns)' | while read -r r; do
printf '%s -> %s\n' "$r" "$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:8080/wp-json/$r")"
done
# 3. der Task-Planer-Endpunkt ist der leise
$ curl -so /dev/null -w '%{http_code}\n' 'http://localhost:8080/wp-cron.php?doing_wp_cron'
Erkennung in eigener Infrastruktur
Eigene REST-Namespaces auf unauthentifizierte Datenrouten durchsuchen
$ curl -s https://example.org/wp-json/ | python3 -c '
import json,sys
d = json.load(sys.stdin)
for ns in d["namespaces"]:
print(ns)'
# danach jede Routenliste abrufen und nach Wegen suchen, die Datensätze liefern
$ curl -s https://example.org/wp-json/ | python3 -c '
import json,sys
d = json.load(sys.stdin)
for ns in d["namespaces"]:
print(ns)' | while read -r ns; do
curl -s "https://example.org/wp-json/$ns" | python3 -c '
import json,sys
try:
for r,v in json.load(sys.stdin).get("routes",{}).items():
if not any(m in v for m in ("POST","PUT","DELETE")) and "permission" in str(v):
print(r)'
done
Upload-Verzeichnis und Endungsdurchsetzung prüfen
$ curl -s -o /dev/null -w '%{http_code}\n' https://example.org/wp-content/uploads/2026/
$ curl -so /dev/null -w '%{http_code}\n' \
'https://example.org/wp-content/uploads/2026/06/probe.php'
$ curl -so /dev/null -w '%{http_code}\n' \
'https://example.org/wp-content/uploads/2026/06/probe.phtml'
Behebung
Unauthentifizierte Routen mit einer Ablehnung beantworten, nicht mit Daten
# die Plugin-Namespaces sperren, bis sie benötigt werden
location ~* ^/wp-json/(book|event|tribe)[a-z_-]*/ {
if ($remote_user = "") { return 403; }
proxy_pass http://backend;
}
# den Scheduler nie von aussen auslösen
location = /wp-cron.php { deny all; return 403; }
location = /wp-cron.php/ { deny all; return 403; }
Skriptausführung im Upload-Verzeichnis deaktivieren
# Uploads sind Inhalt, niemals Code
<Directory /var/www/html/wp-content/uploads>
php_flag engine off
<FilesMatch "\.(?i:php|phtml|php[0-9]|phar|cgi|pl|py|sh)$">
Require all denied
</FilesMatch>
</Directory>
Verwaltungsansichten rahmen
add_header Content-Security-Policy "frame-ancestors 'none'" always;
Sobald ein Angreifer Zugriff hat, kann er folgende Aktionen durchführen:
Fazit — was ich daraus mitnehme
- Prüfen Sie die Verwaltungskonsole des Plugins, nicht nur das Formular.
- Ein unauthentifizierter Task-Planer ist unbeaufsichtigte Codeausführung by Design.
- Wenn Uploads Produktmerkmal sind, gilt das Upload-Verzeichnis als öffentlich und feindlich.
- Zahlungs-Callbacks müssen vom Anbieter signiert sein, nicht aus dem Browser übernommen werden.