Wer darf hier eigentlich klingeln?
Jede Anwendung, die eine Anmeldung kennt, kennt auch Ausnahmen davon. Das muss auch so sein: Die Anmeldeseite selbst muss ohne Anmeldung erreichbar sein, die Passwort-vergessen-Funktion auch, das Impressum sowieso. Dazu kommen die unauffälligeren Fälle – Schnittstellen, an denen nicht ein Mensch anklopft, sondern ein anderer Server.
Dort wird es interessant. Wenn ein fremdes System eine Nachricht schickt, kann es sich nicht wie ein Benutzer anmelden; es ist keiner. Also wird für diesen Weg die Anmeldepflicht ausgeschaltet. Die Entscheidung ist richtig, und sie hat eine Eigenschaft, die man ihr nicht ansieht: Sie gilt für alles, was in diesem Kontext läuft – auch für Code, der Jahre später dazukommt und von dieser Vorgeschichte nichts weiß.
Für einen Test ist das eine dankbare Ausgangsfrage, und sie lässt sich am Quelltext beantworten statt am Browser: Welche Einstiegspunkte nimmt die Initialisierung von der Anmeldepflicht aus – und was passiert dahinter? Die Liste ist kurz, endlich und steht an einer Stelle.
Jede Ausnahme von der Anmeldepflicht ist eine Stelle, an der die üblichen Annahmen nicht mehr gelten. Man sollte sie kennen, bevor jemand anderes sie kennt.
Die Suche
Der Ausgangspunkt war diesmal eine Perspektive, keine Technik: Ich habe mir ILIAS konsequent aus der Sicht von jemandem angesehen, der sich nicht anmelden kann. Die Frage lautet dann nicht mehr „was darf ein Teilnehmer, was eine Kursleitung?“. Sie lautet: Was läuft überhaupt, bevor die Anwendung weiß, wer da anfragt?
Diese Sicht wird beim Testen seltener eingenommen, als man meinen sollte. Sobald ein Konto im Spiel ist, dreht sich alles um Rollen und Rechte – wer darf was sehen, wer darf was ändern. Vor der Anmeldung gibt es diese Frage nicht. Es gibt nur die andere: Wer wird hier überhaupt gefragt? Die Fläche ist kleiner und lässt sich deshalb vollständig durchgehen.
Praktisch heißt das: die Liste der Kontexte durchsehen, die die Initialisierung von der Anmeldepflicht ausnimmt, und zu jedem Eintrag nachlesen, was dahinter passiert. Diese Liste ist kurz. Was dahinter passiert, ist es nicht.
Bei einem der Einträge blieb ich hängen – wegen einer Bauform, die aus derselben
Familie stammt wie die zuvor gefundene Zeichenkettenverkettung: Wo wird
gespeicherter Zustand wieder zu einem Objekt? Also unserialize(),
und dazu die Anschlussfrage, ob eingeschränkt wird, welche Klassen dabei entstehen
dürfen.
Der Treffer lag in der Shibboleth-Komponente, in einem Rückkanal namens
shib_logout.php. Über diesen Weg teilt ein Identity-Provider der Anwendung
mit, dass eine Anmeldung anderswo beendet wurde. Die Anwendung muss daraufhin
herausfinden, welche ihrer eigenen Sitzungen gemeint ist – und dabei ging sie
großzügig vor: Sie las alle noch laufenden Sitzungen aus der Datenbank und schickte
deren gespeicherten Inhalt durch eine selbstgeschriebene Zerlegefunktion. Die rief
unserialize() auf, ohne den Parameter zu setzen, mit dem sich die
zulässigen Klassen beschränken lassen.
Warum das gefährlich ist, obwohl doch nur gelesen wird
unserialize() liest keine Daten – es baut Objekte. Diese Objekte
gehören zu echten Klassen der Anwendung, und ihre Lebenszyklus-Methoden laufen, auch
wenn das Programm sie nie benutzt: Spätestens beim Verwerfen wird aufgeräumt. Ein
gewachsenes Projekt bringt über seine Abhängigkeiten fast immer eine Klasse mit, die
dabei etwas Folgenreiches tut – eine Datei schreiben, einen Befehl absetzen, eine
Verbindung öffnen. Der Angreifer schreibt also keinen Code. Er setzt vorhandene
Bausteine so zusammen, dass ihr ganz normales Verhalten das Gewünschte bewirkt.
Damit war die Senke gefunden. Was fehlte, war der Weg dorthin: Wie bekommt jemand, der nicht angemeldet ist, überhaupt etwas in eine Sitzung dieser Instanz?
Die Kette, Glied für Glied
Wie bei der zuvor untersuchten SQL-Injection gilt: Ein Fund im Quelltext ist eine Vermutung. Belastbar wird er erst, wenn sich jedes Glied einzeln nachvollziehen lässt – und wenn zu jedem die Frage gestellt wurde, ob es die Kette nicht doch unterbricht.
- Der Endpunkt ist offen Der Shibboleth-Rückkanal läuft in einem Kontext, den die zentrale Initialisierung von der Anmeldepflicht ausnimmt. Kein Konto, kein Token, keine Sitzung nötig.
- Er liest alle Sitzungen, nicht nur eine Um die passende Sitzung zu bestimmen, wird die gesamte Tabelle der noch gültigen Sitzungen durchgegangen. Es genügt also, dass irgendwo eine präparierte Sitzung existiert – sie muss dem Aufrufer nicht gehören.
- Der Inhalt wird ohne Klassenbeschränkung deserialisiert Aus gespeichertem Text werden Objekte beliebiger Klassen, die die Anwendung kennt – einschließlich derer aus mitgelieferten Fremdbibliotheken.
- Der Inhalt kam ohne Anmeldung dorthin Das fehlende Stück lag an einer ganz anderen Ecke: Der Einstiegspunkt der LTI-Anbindung legt übermittelte Werte in der Sitzung ab – und ist von derselben Initialisierung ebenfalls von der Anmeldepflicht ausgenommen. Eine anonyme Sitzung mit selbst gewähltem Inhalt ist damit eine einzige Anfrage entfernt.
- Beim Aufräumen wird geschrieben Unter den mitgelieferten Bibliotheken findet sich eine Klasse, deren normales Verhalten beim Verworfenwerden das Schreiben einer Datei ist – an einen Pfad, der aus einer ihrer eigenen Eigenschaften stammt.
Fünf Glieder, von denen keines für sich ein Fehler ist. Die Ausnahme von der Anmeldepflicht ist richtig. Der Rückkanal muss Sitzungen finden können. Die LTI-Anbindung muss Werte zwischenspeichern. Die Bibliotheksklasse tut genau das, wofür sie gebaut wurde. Erst die Verbindung ergibt den Befund – und macht aus fünf vertretbaren Entscheidungen eine Codeausführung ohne Anmeldung.
Drei Linien, drei Antworten
Hier zahlt sich das Testlabor aus. Auf der VM stehen alle aktiv gepflegten Versionsstände nebeneinander, und die Frage „betrifft das alle?“ ist keine Schätzung, sondern ein Nachmittag Arbeit. Die Antwort war in diesem Fall bemerkenswerter als erwartet.
Der verwundbare Code – die Deserialisierung ohne Klassenbeschränkung – steckt in allen drei Linien, und zwar in seiner Substanz identisch. Ob der Endpunkt ihn im ausgelieferten Zustand aber überhaupt erreicht, unterscheidet sich:
| Linie | Verwundbarer Code | Erreichbar |
|---|---|---|
| ILIAS 9 | vorhanden | Nein. Der zuständige Zweig hängt an einer Superglobalen, die PHP 7 entfernt hat. Er wird nie betreten. |
| ILIAS 10 | vorhanden | Ja – im Rahmen der Meldung nachgewiesen. |
| ILIAS 11 | vorhanden | Nein. Der Endpunkt greift auf den Dienst-Container zu, bevor dieser gebaut wird, und bricht bei jeder Anfrage vorher ab. |
Man muss sich klarmachen, was in den Zeilen eins und drei steht: Der Shibboleth-Rückkanal war dort gar nicht funktionsfähig. Nicht abgesichert – kaputt. In der einen Linie, weil er sich auf eine PHP-Variable stützt, die es seit über zehn Jahren nicht mehr gibt. In der anderen, weil eine Zeile auf etwas zugreift, das an dieser Stelle noch nicht existiert, sodass jede Anfrage sofort abbricht.
Wäre der Shibboleth-Logout dort je benutzt worden, hätte es jemand gemerkt. Es hat niemand gemerkt. Über Jahre.
Kein Grund zur Entwarnung
Das ist ausdrücklich kein Argument, ein Update aufzuschieben. Was den Endpunkt in diesen Linien blockiert, sind Nebenumstände – eine Sprachänderung und ein Programmierfehler –, keine Schutzmaßnahmen. Ein Patch, ein Plugin oder eine angepasste Installation kann sie aufheben, ohne dass das jemandem auffällt. Der verwundbare Code ist überall vorhanden, und deshalb hat der Hersteller ihn auch überall entfernt.
Was hier nicht steht
Bei der SQL-Injection konnte ich den Quelltext zeigen und den Weg des Werts Glied für Glied nachzeichnen. Hier mache ich das nicht, und das ist eine bewusste Entscheidung.
Der Unterschied liegt nicht am Fund selbst. Er liegt an dem, was er kostet. Für die SQL-Injection war ein angemeldetes Konto mit Schreibrecht nötig – jemand musste also erst einmal irgendwo Kursleitung sein. Eine Codeausführung ohne Anmeldung lässt sich dagegen automatisiert gegen jede erreichbare Instanz fahren, und nach einem Patch dauert es Wochen bis Monate, bis die Mehrheit der Installationen aktualisiert ist. Eine Schritt-für-Schritt-Darstellung wäre in dieser Zeit eine Bauanleitung.
Deshalb steht hier kein Quelltext, kein Name der als Werkzeug geeigneten Bibliotheksklasse, keine Parameter und keine Abfolge konkreter Anfragen. Was hier steht, reicht aus, um die eigene Betroffenheit zu beurteilen, die Dringlichkeit richtig einzuschätzen und Protokolldaten sinnvoll auszuwerten. Mehr steht hier nicht. Wer den Code lesen möchte, findet ihn über den im Advisory SIT-2026-002 verlinkten Commit des Herstellers.
Diese Abwägung fällt nicht bei jedem Befund gleich aus. Sie hängt daran, wie leicht sich etwas ausnutzen lässt, wie viele Systeme betroffen sind und wie lange ein Update braucht, bis es in der Fläche ankommt.
Die Korrektur
Der Hersteller hat die Abmelde-Behandlung des Endpunkts nicht abgesichert, sondern entfernt. Die Funktion war an eine Betriebsart gebunden, die nur ein Teil der Instanzen nutzt, und ihre Aufgabe – aus einer fremden Mitteilung die passende lokale Sitzung heraussuchen – lässt sich ohne das Durchsehen aller Sitzungsdaten kaum sinnvoll lösen. Die formalen Angaben stehen im Advisory SIT-2026-002.
Fazit / Ausblick
Der zuvor untersuchte Befund war ein vergessener Abgleich. Dieser hier ist etwas anderes: eine Funktion, die niemand mehr benutzt, an einem Eingang, den niemand mehr prüft. Sie war in zwei von drei Linien kaputt, und es ist über Jahre niemandem aufgefallen.
Eine Funktion, deren Ausfall niemandem auffällt, ist keine Funktion mehr – sie ist nur noch Angriffsfläche. Sie wird nicht benutzt, also nicht getestet; sie wird nicht getestet, also fällt nicht auf, dass sie kaputt ist; und weil sie niemand vermisst, sieht sich auch niemand ihren Code an. Sie liegt einfach da und wartet.
Zwei Punkte nehme ich mit, und beide gelten weit über ILIAS hinaus:
- Ausnahmen von der Anmeldepflicht altern schlecht. Sie werden aus einem guten Grund eingerichtet und gelten danach für alles, was in diesem Kontext läuft. Eine Liste dieser Ausnahmen zu führen und regelmäßig durchzugehen, ist eine der lohnendsten Übungen überhaupt.
- Sitzungsdaten sind keine eigenen Daten. Sobald irgendein anmeldefreier Pfad hineinschreiben kann, ist ihr Inhalt Fremdeingabe – und die Frage „wer kann hier eigentlich etwas ablegen?“ gehört an jede Stelle, die gespeicherte Zustände wieder einliest.
Die Perspektive dieses Writeups – die Anwendung von außen ansehen, ohne Konto – hat mehr hergegeben als diesen einen Befund. Weitere Beiträge dazu bleiben vorerst zurückgehalten.