Erfahrungen · Lessons Learned
Der Fehlbefund, der keiner war
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:
- Inventar der PHP-Landschaft: ausschließlich
fpmundcliinstalliert — keinmod_php, keinphp-cgi, kein Apache. - Exponierte Endpunkte: genau eine PHP-Anwendung über den Proxy (die Nextcloud) plus ein LAN-only-Admin-Port; daneben eine verwaiste Adminer-Site, deren PHP-Socket seit Wochen nicht mehr existierte.
- Kein „irgendwo noch PHP“: statische Sites ohne FastCGI-Block sind keine PHP-Angriffsfläche.
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üfung | Ergebnis | Was sie belegt |
|---|---|---|
| Paketversion | 8.5.4-0ubuntu1.5 (Ubuntu-Fix, USN-8948-1/-2) | Der Fix ist installiert |
| Installationszeitpunkt | 2026-10-22 06:12 (automatische Updates) | Wann der Fix ankam |
| FPM-Neustart | 2026-10-22 06:13, Master seither durchgehend aktiv | Der Fix läuft |
| Dateiintegrität | Paketprüfung ohne Abweichung | Keine 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:
| Ebene | Beantwortet | Blindstelle |
|---|---|---|
| Proxy-Zugriffslog | Welche 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-Log | Worker-Abstürze, Speichererschöpfung, Fatal Errors | Nur PHP-Ebene, kein HTTP-Kontext |
| Journald / Kernel | OOM-Kills, Segfaults, Coredumps | Keine 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:
- 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.
- 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.
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:
- 18.244 HTTP-Anfragen über zehn Tage; die PHP-Anwendung beantwortete keine einzige Anfrage mit 5xx.
- Größter Request-Body: 25 KB. Kein Body ≥ 1 MB — der für eine Speicher-Erschöpfung typische Pfad fehlte im beobachteten Traffic vollständig.
- Anwendungslog: 14.518 Einträge, 0 Fatal Errors, ein einziger Fehler (App-Store nicht erreichbar), keine fehlgeschlagenen Logins, keine App-Installationen.
- Indikator-Suche über 789.528 Zeilen (typische Webshell-Funktionen,
/tmp/-Pfade): je null Treffer. Treffer auf bekannte Shell-Namen entpuppten sich als Zufallsfolgen in Request-IDs. - FPM und Kernel: keine Fatal Errors, keine OOM-Kills, keine Segfaults.
- Web-Root: seit Beginn des Beobachtungszeitraums keine neue PHP-Datei, keine unerwartete Änderung.
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:
- Proxy-Logs: 5 × 10 MB Rotation — rund zehn Tage Rückblick. Der Beobachtungszeitraum war vollständig abgedeckt, mehr aber nicht.
- 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.
(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
- Patch-Nachweis doppelt geführt (Paketversion und Laufzeit) — die entscheidende Aussage war damit nicht von Logs abhängig.
- Negativnachweise statt Vermutungen: keine neue PHP-Datei, keine 5xx, keine OOM-Kills, keine Fatal Errors.
- Angriff gegen totes Backend erkannt — der 502-Befund verhinderte eine dramatisierte Fehlbewertung.
- Entscheidung begründet, nicht behauptet: Eine Meldepflicht wurde geprüft und mit Begründung verneint, nicht stillschweigend übergangen.
Was wir beim nächsten Mal anders machen
- Suchraum vor der Suche dokumentieren. „Nicht gefunden“ nur schreiben, wenn die durchsuchten Pfade und Tiefen genannt sind.
- Jede Config-Aussage mit einer Gegenprobe versehen. Logpfad: nicht nur lesen, sondern prüfen, ob die Datei existiert, wächst und rotiert.
- Rotation aktiv testen (Größe künstlich überschreiten, Rotation im Log nachweisen) — nicht annehmen, dass sie läuft.
- Firewall als Ganzes lesen (Default-Policy, pauschale Subnetz-Regeln, Reihenfolge).
- Verwaiste Sites sofort abbauen. Eine tote PHP-Site ist kein Risiko, aber ein ewiger Störfaktor im Log und ein Blindfleck in der Bewertung.
- Evidenz sichern, bevor rotiert wird — mit Eigentümer, Rechten und Prüfsumme.
- 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
- PHP-Projekt: Sicherheitshinweise zu
CVE-2026-109618undCVE-2026-109619. - CERT-EU, Security Advisory 2026-018 (Mitigationen und Beobachtungslage).
- Ubuntu Security Notices USN-8948-1/-2 (Paketfix für PHP 8.5).
- Herstellerhinweis zum Perimeter-Gerät: dort wird kein PHP eingesetzt.