Die Hintertür hieß „auto-bootstrap“: Wie alte Gastkonten meinen Blog zur Spam-Schleuder machten

Die Hintertür hieß „auto-bootstrap“: Wie alte Gastkonten meinen Blog zur Spam-Schleuder machten

Ein Blick in die Beitragsliste, und da standen sie: über tausend Casino-Beiträge, eingereicht von zwei Gastautoren, die seit Jahren nichts mehr geschrieben hatten. Der Weg hinein führte nicht übers Login-Formular, sondern über WordPress-Anwendungspasswörter. Hier ist, wie ich den Vorfall untersucht und die Tür geschlossen habe.

Du kennst das: Du willst eigentlich nur kurz einen Beitrag korrigieren, die Beitragsliste lädt – und oben steht „Ausstehend (1.026)“. Bei mir war das auf meinem zweiten Blog so, einem Gesellschaftsblog mit vielen Gastautoren seit 2010. Ein Klick, und die Liste war voller Casino-Beiträge. Auf Deutsch, Norwegisch, Französisch. Eingereicht von zwei Gastautoren, die seit über zehn Jahren nichts mehr geschrieben hatten.

Der Weg hinein führte nicht über das Login-Formular, sondern über WordPress Anwendungspasswörter – eine Funktion, die seit WordPress 5.6 fest eingebaut ist und die die meisten nie bewusst eingeschaltet haben. Hier der Ablauf vom ersten Schreck bis zur geschlossenen Tür. Damit du (oder dein zukünftiges Ich) das nicht erst bei Tausend Beiträgen merkst.

Was ich vorgefunden habe

Der Blog ist über 15 Jahre alt und hatte 130 Benutzerkonten. Fast 70 davon waren alte Gastautoren mit der Rolle Mitarbeiter – die dürfen Beiträge einreichen, aber nicht veröffentlichen. Dazu 5 Redakteure und 5 Administratoren. Die meisten Konten hatten sich seit 2015 bis 2018 nicht mehr angemeldet.

Die Bestandsaufnahme mit WP-CLI. Auf einem Plesk-Server mit WP Toolkit sieht der Aufruf so aus (die Instanz-ID zeigt plesk ext wp-toolkit --list):

alias wp='plesk ext wp-toolkit --wp-cli -instance-id N --'
wp post list --post_status=pending --format=count
wp db query "SELECT post_author, COUNT(*) AS anzahl FROM $(wp db prefix)posts
             WHERE post_status='pending' GROUP BY post_author ORDER BY anzahl DESC;"

Terminal: 1026 ausstehende Beiträge in der Warteschlange

Das Ergebnis: 1.026 ausstehende Beiträge von genau zwei Konten. Eines reichte seit Januar 2025 ein (26 Beiträge), das andere seit März 2026 (1.000 Stück). Beide Konten stammten von 2012.

Und dann war da noch ein unscheinbarer Entwurf namens „test123“ – Inhalt: eine einzelne Zeichenkette und ein Zeitstempel. Angelegt ein paar Tage, bevor die Flut losging. Das war kein vertippter Gastautor, das war ein Funktionstest: Klappt das Einreichen über die Schnittstelle? Ja. Dann los.

Welche Seiten der Spam bewerben sollte, verrät der Inhalt:

wp post list --post_status=pending --field=post_content \
  | grep -oE 'href="https?://[^/"]+' | sed 's/href="//' \
  | sort | uniq -c | sort -rn | head -20

Mehrere hundert verschiedene Casino- und Wettseiten. Klassischer SEO-Spam: Es geht um Links von einer alten, vertrauenswürdigen Domain.

Der Weg hinein: WordPress Anwendungspasswörter

Seit WordPress 5.6 kann jeder Benutzer im Profil Anwendungspasswörter anlegen. Damit melden sich Programme an der REST-API an – ohne Login-Formular, ohne Zwei-Faktor-Abfrage. Praktisch für Apps und Automatisierung. Und genau deshalb ideal für Angreifer.

wp user application-password list <konto> --fields=name,created,last_used,last_ip

Eines der beiden Konten hatte fünf Anwendungspasswörter, mit Namen wie „auto-bootstrap“ und „auto-job“. So nennt kein Gastautor von 2012 etwas – die hat ein Skript angelegt. Zuletzt benutzt wenige Tage vor meiner Entdeckung. Dazu kamen offene Sitzungen von ständig wechselnden IP-Adressen rund um den Globus:

wp user meta get <konto> session_tokens --format=json

Dort steht pro Sitzung Anmeldezeit, IP-Adresse und User-Agent. Bei einem echten Gastautor erwartest du einen, vielleicht zwei Einträge. Hier war es ein bunter Reiseplan.

Wie kamen die Angreifer an die Passwörter? Am wahrscheinlichsten über Credential Stuffing: Passwörter aus fremden Datenlecks, die die Gastautoren vor über zehn Jahren auch bei mir benutzt hatten. Fünf weitere alte Konten – auch Abonnenten – hatten in den Wochen davor ebenfalls frische Anmeldungen von wechselnden Adressen. Eingereicht hatten die nichts. Noch nicht.

Warum Fail2Ban nichts gemerkt hat

Natürlich lief auf dem Server Fail2Ban, mit einer eigenen Jail für WordPress-Anmeldungen:

fail2ban-client status plesk-wordpress

Über hundert gesperrte IPs – die Jail arbeitet. Nur hilft sie gegen dieses Muster nicht: Fail2Ban reagiert auf fehlgeschlagene Anmeldungen. Die Angreifer hatten aber die richtigen Passwörter. Und Anwendungspasswörter laufen sowieso nicht über das Anmeldeformular.

Glück im Unglück: die Rolle „Mitarbeiter“

Dass von den 1.026 Beiträgen keiner online ging, lag an einer einzigen Einstellung von vor über zehn Jahren: Gastautoren bekamen damals die Rolle Mitarbeiter. Die dürfen einreichen, aber nicht veröffentlichen. Alles landete in „Ausstehend“ und wartete auf eine Freigabe, die nie kam.

Wäre ein Redakteurskonto betroffen gewesen, wäre der Spam sofort live gegangen. Mit allem, was dazugehört: Glücksspiel-Links auf einer seriösen Domain, Abstrafung bei Google, wochenlanges Aufräumen.

Ein Fehlalarm – sauber ausgeschlossen

Mitten in der Untersuchung fiel mir ein Administrator mit einem zufälligen Namen aus zehn Zeichen auf, Anzeigename „admin“. Sieht aus wie ein klassisches Angreifer-Konto. Bevor ich es gelöscht habe, habe ich nachgesehen: Sitzungen, Bearbeitungen, Meta-Daten.

Ergebnis: Das war das alte Ur-Konto „admin“, vom WP Toolkit aus Sicherheitsgründen umbenannt. Die letzte Anmeldung kam von meinem eigenen iPhone – über iCloud Privat-Relay, deshalb mit einer fremden Cloudflare-Adresse. Kleines Detail am Rand: Safari meldet ab iOS 26 im User-Agent eingefroren „iPhone OS 18_7“, die echte Version steht weiter hinten hinter „Version/26.x“. Wer nur auf den Anfang schaut, sieht ein veraltetes System, wo keins ist.

Lehre: Erst prüfen, dann löschen. Sonst sperrst du dich im Eifer des Gefechts selbst aus.

So habe ich die Tür geschlossen

Die Reihenfolge war mir wichtig: erst sichern, dann die aktiven Zugänge kappen, dann aufräumen, dann vorbeugen.

1) Backup vor jedem Schritt

wp db export ~/backup-vor-spam-aufraeumen-$(date +%Y%m%d-%H%M).sql

2) Die übernommenen Konten stilllegen

wp user application-password delete <konto> --all
wp user session destroy <konto> --all
wp user update <konto> --user_pass="$(openssl rand -base64 24)"
wp user set-role <konto> subscriber
wp user meta update <konto> sicherheitsnotiz "2026-11: übernommen, Anwendungspasswörter gelöscht, auf Abonnent gesetzt"

Die Notiz am Konto ist für später: Wer in einem Jahr über das Konto stolpert, weiß sofort, was passiert ist.

3) Spam in den Papierkorb – nicht endgültig löschen

wp post list --post_status=pending --author=<id> --format=ids | xargs -n 100 wp post delete

Ohne --force landen die Beiträge im Papierkorb. Endgültig löschen mache ich erst nach einer Sichtprüfung, ob kein echter Beitrag dazwischen gerutscht ist.

Stolperstelle: Der WP-CLI-Aufruf über Plesk bricht nach 60 Sekunden ab. Für tausend Beiträge reicht das nicht. Die Lösung war, das echte WP-CLI direkt als Systembenutzer der Website laufen zu lassen:

sudo -u <site-benutzer> wp --path=/var/www/vhosts/example.org/httpdocs post list --post_status=pending --format=count

4) Anwendungspasswörter nur noch für Administratoren

Ein kleines Must-Use-Plugin unter wp-content/mu-plugins/sicherheit-api.php – es lässt sich im Backend nicht versehentlich deaktivieren:

<?php
/**
 * Plugin Name: Sicherheit: API-Zugänge begrenzen
 * Description: Anwendungspasswörter nur für Administratoren, XML-RPC aus.
 */
add_filter( 'wp_is_application_passwords_available_for_user', function ( $available, $user ) {
    return $available && user_can( $user, 'manage_options' );
}, 10, 2 );

add_filter( 'xmlrpc_enabled', '__return_false' );

XML-RPC war bei mir über den Webserver ohnehin schon gesperrt – doppelt hält hier besser.

5) Alle inaktiven Konten auf Abonnent – mit Gedächtnis

Der wichtigste Schritt. Jeder, der nicht aktiv schreibt, braucht keine Rolle über Abonnent. Damit ich einen Gastautor bei Bedarf mit einem Befehl wieder freischalten kann, merke ich mir die alte Rolle als Benutzer-Meta:

for id in $(wp user list --role__in=contributor,author,editor --field=ID); do
  rolle=$(wp user get "$id" --field=roles)
  wp user meta update "$id" alte_rolle "$rolle"
  wp user set-role "$id" subscriber
done

Den einzigen aktiven Autor habe ich vorher aus der Liste genommen, die übrigen Administratoren außer meinem eigenen Konto ebenso behandelt. Zurück geht es so:

wp user set-role <id> "$(wp user meta get <id> alte_rolle)"

Terminal: Rollenverteilung nach dem Aufräumen – ein Administrator, ein Autor, 128 Abonnenten

Ergebnis: ein Administrator, ein aktiver Autor, 128 Abonnenten.

6) Konten mit fremden Anmeldungen: neues Passwort

Die fünf Konten mit den verdächtigen Anmeldungen bekamen ebenfalls ein Zufallspasswort. Wer davon wirklich noch schreiben will, nutzt „Passwort vergessen“.

7) Zwei-Faktor für das Admin-Konto

wp plugin install two-factor --activate

Das offizielle Plugin „Two Factor“ kann TOTP (Authenticator-App) und Backup-Codes. Stolperstelle: Die Backup-Codes werden zwar erzeugt, gelten aber erst, wenn du sie im Profil auch als Methode anhakst. Sonst stehst du beim Handy-Verlust mit zehn wertlosen Codes da.

8) Integrationen nicht vergessen

Google Site Kit hing noch am alten Admin-Konto. Also unter dem eigenen Konto neu verbunden – und erst danach das alte Konto zurückgestuft. Andersherum reißt die Verbindung ab.

So prüfst du es

wp post list --post_status=pending --format=count            # sollte klein sein
wp user list --field=roles | sort | uniq -c                  # wer hat welche Rolle?
for u in $(wp user list --field=user_login); do
  n=$(wp user application-password list "$u" --format=count 2>/dev/null)
  [ "${n:-0}" -gt 0 ] && echo "$u: $n Anwendungspasswörter"
done

Die letzte Schleife sollte nur noch dein eigenes Admin-Konto zeigen – oder gar nichts.

Rückweg

  • Gastautor wieder freischalten: alte Rolle aus der Meta zurückholen (siehe Schritt 5).
  • Anwendungspasswörter für alle wieder erlauben: das Must-Use-Plugin löschen.
  • Versehentlich Gelöschtes: liegt im Papierkorb oder im Datenbank-Backup.

Stolperstellen aus der Praxis

  • Alte Gastkonten sind Altlasten mit Schlüssel. Wer seit Jahren nichts schreibt, braucht keine Schreibrechte.
  • Anwendungspasswörter sind leise: kein Login-Formular, keine Zwei-Faktor-Abfrage, Fail2Ban sieht nichts. Wer sie nicht braucht, schaltet sie für Nicht-Admins ab.
  • „Ausstehend“ ist ein Frühwarnsystem – aber nur, wenn man reinschaut.
  • Ein Entwurf namens „test123“ ist kein Tippfehler.
  • Nicht jeder verdächtige Admin ist ein Angreifer. Erst Sitzungen und Bearbeitungen prüfen, dann handeln.
  • Plesk-WP-CLI hat ein Zeitlimit von 60 Sekunden. Für Massenaktionen direkt als Site-Benutzer arbeiten.

Fazit

Der ganze Spuk hat mich einen Vormittag gekostet. Wäre ein Redakteurskonto betroffen gewesen, hätte er mich wahrscheinlich das Google-Ranking eines 15 Jahre alten Blogs gekostet. Der eigentliche Fehler lag nicht in WordPress, sondern in der Annahme, dass alte Konten harmlos sind, nur weil sie still sind. WordPress Anwendungspasswörter sind ein nützliches Werkzeug – aber eben auch eine Tür, die man kennen sollte, bevor jemand anderes sie findet.

Total
0
Shares
Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Previous Post
Dein iPhone meldet Söldner-Spyware: Was jetzt zu tun ist – und was du besser lässt

Dein iPhone meldet Söldner-Spyware: Was jetzt zu tun ist – und was du besser lässt

Related Posts
Total
0
Share