~/blog/event-plugin-booking-attack-surfacePOSTED
von Shady Nathan Tawfik · 13. Juni 2026 · 4 min

Das Buchungsplugin ist das Produkt, und das Produkt ist eine Verwaltungsoberfläche

Ein Weiterbildungsträger mit Buchungsplugin legte mit dem Plugin dessen komplette Verwaltungsoberfläche frei: Exporte, Teilnehmerdaten, Datei-Uploads und ein unauthentifizierter Cron.

Interaktiv[blog-2.0]

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.

TL;DR: Ein Weiterbildungsträger mit Buchungsplugin legte mit dem Plugin dessen komplette Verwaltungsoberfläche frei: Exporte, Teilnehmerdaten, Datei-Uploads und ein unauthentifizierter Cron.

Befunde

SchweregradBefundNachweis
HOCHTeilnehmer-Export ohne Authentifizierung erreichbarVollständige Teilnehmerliste inklusive Kontaktdaten
HOCHUnauthentifizierter Task-Planer-EndpunktPlugin-Code wird zeitgesteuert ohne Bediener ausgeführt
MITTELUpload-Verzeichnis innerhalb des WebrootsHochgeladenes Material direkt ausgeliefert, wenn die Endungsprüfung versagt
MITTELKein Frameschutz auf VerwaltungsansichtenVerwaltungsaktionen von Dritten einbettbar
MITTELZahlungsübergabe ohne signierte Callback-PrüfungBestellstatus aus clientgelieferten Daten ableitbar
NIEDRIGPlugin- und Core-Version in Asset-Pfaden sichtbarGenauer Build identifizierbar

Der Angriffspfad

Public course pageRegistration formScheduled task endpointPlugin code runs unattendedExport and report routinesParticipant records written to diskUpload of course materialFile stored inside the web root

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

~/code/bash bash
# 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

~/code/bash bash
$ 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

~/code/bash bash
$ 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

~/code/nginx nginx
# 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

~/code/apache apache
# 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

~/code/nginx nginx
add_header Content-Security-Policy "frame-ancestors 'none'" always;

Sobald ein Angreifer Zugriff hat, kann er folgende Aktionen durchführen:

Teilnehmer-Export ohne Authentifizierung erreichbar
Unauthentifizierter Task-Planer-Endpunkt
Upload-Verzeichnis innerhalb des Webroots
Kein Frameschutz auf Verwaltungsansichten
Zahlungsübergabe ohne signierte Callback-Prüfung

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.

Nur zu Bildungszwecken. Alle reproduzierbaren Befehle zielen auf meine eigene Lab-Umgebung (localhost), nie auf ein laufendes System. © 2026