Illustration: Ein Patch-Nachweis, vier Log-Ebenen und eine Rotation, die nicht lief

Dieser Erfahrungsbericht ist anonymisiert. Er beschreibt einen realen Vorfall, ohne Hostnamen, Adressen oder Kundendaten zu nennen. Im Mittelpunkt steht nicht die Schwachstelle, sondern die Beweisführung: Was prüft man, wenn innerhalb von Stunden zu entscheiden ist, ob ein System kompromittiert wurde — und wie leicht man dabei ein Ergebnis falsch liest?

Ausgangslage

Innerhalb eines Tages veröffentlichte das PHP-Projekt zwei kritische, unauthentifiziert ausnutzbare Schwachstellen (CVE-2026-109618 und CVE-2026-109619, je CVSS 9.2, zusammen als „PHP2Shell“ bezeichnet). Betroffen sind PHP-Installationen, die Webanfragen bedienen. Auf dem betroffenen Host liefen ein Reverse-Proxy, eine Nextcloud-Instanz mit PHP-FPM sowie eine Reihe statischer Sites. Die Frage des Betreibers war denkbar einfach:

Sind wir betroffen? Ist der Patch aktiv? Und gibt es in den Logs einen Hinweis darauf, dass jemand die Lücke ausgenutzt hat?

Drei Fragen, drei sehr unterschiedliche Beweisprobleme. Genau darin lag die Schwierigkeit.

Schritt 1 — Betroffenheit belegen, nicht vermuten

Der erste Reflex („wir setzen PHP ein, also sind wir betroffen“) ist zu grob. Entscheidend ist die SAPI: Nur Installationen, die Webanfragen verarbeiten, sind exponiert. Der Nachweis war entsprechend konkret:

Schritt 2 — Patch-Nachweis: Paket und Laufzeit

Ein installiertes Sicherheitsupdate ist noch kein aktiver Patch. Zwischen „Paket liegt auf der Platte“ und „der laufende Prozess nutzt es“ liegt ein Neustart des FPM-Masters. Deshalb wurde doppelt geprüft:

PrüfungErgebnisWas sie belegt
Paketversion8.5.4-0ubuntu1.5 (Ubuntu-Fix, USN-8948-1/-2)Der Fix ist installiert
Installationszeitpunkt2026-10-22 06:12 (automatische Updates)Wann der Fix ankam
FPM-Neustart2026-10-22 06:13, Master seither durchgehend aktivDer Fix läuft
DateiintegritätPaketprüfung ohne AbweichungKeine manipulierten Binaries

Diese vier Zeilen sind der eigentliche Patch-Nachweis. Alles Weitere ist Interpretation der Logs.

Schritt 3 — Vier Log-Ebenen, vier verschiedene Fragen

Ein einzelnes Log kann einen Incident nicht beweisen. Jede Ebene beantwortet eine andere Frage — und hat eine eigene Blindstelle:

EbeneBeantwortetBlindstelle
Proxy-ZugriffslogWelche Anfragen kamen an, mit welchem Status und welcher Körpergröße?Keine Inhalte; Rotation begrenzt den Rückblick
Anwendungslog (Nextcloud)Was hat die Anwendung intern getan — Fehler, Warnungen, Logins?Bei Info-Level nur Ereignisse, keine Zugriffe
FPM-LogWorker-Abstürze, Speichererschöpfung, Fatal ErrorsNur PHP-Ebene, kein HTTP-Kontext
Journald / KernelOOM-Kills, Segfaults, CoredumpsKeine Anwendungs-Semantik

Ergänzend: Fail2ban (SSH-Bruteforce) und der Paketmanager (Patch-Zeitpunkt). Erst die Kombination trägt eine Aussage — und nur, wenn man die Blindstellen benennt.

Der Fehlbefund — „die Anwendung schreibt kein Log“

Die Suche nach dem Anwendungslog lief über die üblichen Verzeichnisse (/srv, /var, /home, begrenzte Tiefe) und fand nichts. Der Schluss lag nahe: Es gibt kein Log. Er war falsch.

Zwei Dinge waren übersehen worden:

  1. Das Datenverzeichnis lag außerhalb des Suchraums. Die Instanz speicherte ihre Daten nicht im Programmverzeichnis, sondern auf einem eigenen Volume. Die Suche war gründlich — nur nicht dort.
  2. Der Logger hatte einen Fallback. War die konfigurierte Logdatei nicht vorhanden und ihr Verzeichnis nicht beschreibbar, schrieb die Anwendung in eine Ersatzdatei im Datenverzeichnis. Das ist korrektes Verhalten — aber es verdeckt eine Fehlkonfiguration.

Der eigentliche Defekt steckte eine Ebene tiefer: Der Rotationsjob liest den Logpfad aus der Konfiguration. Da dieser Pfad nicht existierte, verglich er die Größe einer nicht vorhandenen Datei — und rotierte nie. Ergebnis: eine einzige Datei mit 438 MB statt stündlicher Rotation.

flowchart TD C["Konfiguration:
logfile = Programmverzeichnis/data"] --> L{"Datei vorhanden?"} L -->|nein| F["Logger nutzt Fallback:
Datenverzeichnis/nextcloud.log"] L -->|ja| W["Logger schreibt konfigurierte Datei"] C --> R["Rotationsjob liest denselben
konfigurierten Pfad"] R --> N{"Datei da?"} N -->|nein| X["Keine Rotation
(Datei wächst unbegrenzt)"] F --> X

Die Lehre ist unbequem, weil sie die eigene Arbeit betrifft: „Nicht gefunden“ ist kein Befund, solange der Suchraum nicht dokumentiert ist. Ein Negativbefund braucht dieselbe Sorgfalt wie ein Positivbefund — inklusive der Angabe, wo man nicht gesucht hat.

Der zweite Fehlbefund — die Firewall

Ein Admin-Port war „nur im LAN erreichbar“. Die Prüfung fand kein portbezogenes Allow für diesen Port und notierte: kein Allow vorhanden, also von außen dicht. Auch das war zu kurz gedacht.

Tatsächlich existierte eine pauschale Regel für das gesamte LAN-Subnetz, die jeden Port erlaubt. Der Port war also im LAN offen — der Schutz kam aus der Netztopologie, nicht aus der Portregel. Das Ergebnis („aus dem Internet nicht erreichbar“) stimmte, die Begründung nicht. Und eine falsche Begründung ist der Anfang der nächsten Fehlentscheidung.

Regel: Eine Firewall liest man als Ganzes — Default-Policy, pauschale Allows, Reihenfolge. Ein Blick auf „fehlt ein Port-Allow?“ genügt nicht.

Was die Logs dann tatsächlich zeigten

Nach der Korrektur des Logpfads war die Auswertung möglich — und das Bild war unauffällig:

Der einzige „heiße“ Treffer war ein Scanner, der vier etwa 1 MB große Anfragen an die verwaiste Adminer-Site schickte — genau das Muster, das ein Ausnutzungsversuch erzeugt. Alle vier endeten mit 502, weil der PHP-Socket nicht existierte. Der Angriff lief gegen ein totes Backend. Auch das ist ein Ergebnis: Der gefährlichste Befund war ein Beweis für die Wirksamkeit einer (unbeabsichtigten) Härtung.

Die Nachweisgrenze — und warum Evidenz gesichert werden muss

Zwei Aufbewahrungsregeln begrenzten die Aussage:

  1. Proxy-Logs: 5 × 10 MB Rotation — rund zehn Tage Rückblick. Der Beobachtungszeitraum war vollständig abgedeckt, mehr aber nicht.
  2. Anwendungslog: Die Rotation hält genau eine Vorgängerdatei. Beim nächsten Lauf wäre der Auswertungszeitraum überschrieben gewesen.

Deshalb wurde der Auswertungszeitraum vor der nächsten Rotation als eigenes Archiv gesichert: komprimiert, Eigentümer root, Rechte 640 (die Datei enthält Nutzernamen und IP-Adressen), mit dokumentierter Prüfsumme. Ein Nachweis, der sich nicht reproduzieren lässt, ist im Audit wenig wert — eine Prüfsumme macht aus einer Datei eine überprüfbare Aussage.

flowchart LR A["Live-Log
(eine rotierte Datei)"] --> B["Fensterauszug
2026-10-15 … 2026-10-25"] B --> C["gzip-Archiv
root:root 640"] C --> D["SHA-256 im Bericht"] D --> E["Reproduktion:
gunzip + wc -l"]

Was gut funktionierte

Was wir beim nächsten Mal anders machen

  1. Suchraum vor der Suche dokumentieren. „Nicht gefunden“ nur schreiben, wenn die durchsuchten Pfade und Tiefen genannt sind.
  2. Jede Config-Aussage mit einer Gegenprobe versehen. Logpfad: nicht nur lesen, sondern prüfen, ob die Datei existiert, wächst und rotiert.
  3. Rotation aktiv testen (Größe künstlich überschreiten, Rotation im Log nachweisen) — nicht annehmen, dass sie läuft.
  4. Firewall als Ganzes lesen (Default-Policy, pauschale Subnetz-Regeln, Reihenfolge).
  5. Verwaiste Sites sofort abbauen. Eine tote PHP-Site ist kein Risiko, aber ein ewiger Störfaktor im Log und ein Blindfleck in der Bewertung.
  6. Evidenz sichern, bevor rotiert wird — mit Eigentümer, Rechten und Prüfsumme.
  7. Fehlbefunde korrigieren, nicht glätten. Der Bericht enthält die Korrektur samt Datum und Ursache. Ein stillschweigend reparierter Fehlbefund zerstört Vertrauen stärker als der Fehler selbst.

Übertragbarkeit

Der Vorfall selbst war unspektakulär: gepatcht, nicht ausgenutzt, keine Meldung erforderlich. Wertvoll war der Nachweisapparat: Betroffenheit über Inventar, Patch über Paket und Laufzeit, Ausnutzung über mehrere Log-Ebenen, Bewertung mit expliziten Nachweisgrenzen, Evidenz mit Prüfsumme — und ein Bericht mit Änderungshistorie, in dem auch die eigene Fehleinschätzung steht. Genau diese Struktur lässt sich in Git versionieren und Jahre später noch prüfen.

Quellen