SSRF in einer C#-Webanwendung mit ASP.NET MVC Core finden und beheben

Das hier ist ein Erlebnisbericht über einen SSRF-Fehler (Server-side request forgery) in einer Webanwendung – dessen Beseitigung bzw. Lösung ich so bislang noch nirgendwo dokumentiert gesehen habe.

Deshalb möchte ich sie hier festhalten.

Der Fehler trat im Rahmen eines Penetrationstests bei einer Anwendung für einen Kunden auf.

Fehlverhalten

Unsere Anwendung wurde von einem Dienstleister des Kunden auf Schwachstellen überprüft – im Rahmen eines „Pentests“. Dabei kam u. a. Burp Suite Professional zum Einsatz.

Dabei trat ein SSRF-Fehler auf.

Dieser äußerte sich so, dass beim Senden manipulierter HTTP-Anfragen an unsere MVC-Endpunkte („Actions“) – etwa mit einem XML-Fragment oder einer URL als Parameter – irgendeine Komponente eine externe Webanfrage an diese bösartig übergebene URL ausführte.

Egal ob per GET oder POST: Sobald eine solche URL übergeben wurde, wurde sie irgendwo im System weiterverwendet, was zu einem HTTP-Web-Request oder zumindest zu einer DNS-Anfrage an die enthaltene Domain führte.

Also ein „klassischer“ SSRF-Fall: URLs wurden vom System übernommen und unkontrolliert verwendet.

Analyse

Das Seltsame: Der Fehler ließ sich nur in wenigen Konstellationen nachstellen.

Er trat ausschließlich auf einem bestimmten System/Umgebung beim Kunden auf.

Er trat nicht auf:

  • lokal auf Entwickler-Maschinen.
  • in unserer eigenen Testumgebung.
  • in anderen Test- und Produktivumgebungen beim Kunden.

Außerdem stellte sich durch Logging heraus, dass der Fehler bereits vor dem Aufruf unserer eigentlichen Action (inkl. URL-Sanitizing) auftrat.

Zunächst hatten wir diverse Custom-ASP.NET-Core-Middleware im Verdacht – z. B. Komponenten von DevExtreme oder ähnlichem –, konnten dort aber nichts Konkretes identifizieren.

Ursache

Nach viel Herumprobieren kam mein Kollege Oliver schließlich – mehr oder weniger zufällig – auf die richtige Spur:

Die Ursache war unsere Fehlerprotokollierung („Logging“).

Detailanalyse

Wir verwenden NLog – wobei das konkrete Logging-Framework hier keine Rolle spielt –, um verschiedenste Informationen in Protokolldateien zu schreiben.

Unter anderem auch Fehler.

Zusätzlich zur lokalen Protokollierung wird bei Fehlern auch eine E-Mail mit einer Übersicht an ein Support-Postfach beim Kunden gesendet.

Und genau das war der Auslöser: Die manipulierten Anfragen der Pentester führten zu einem Fehler, der wiederum eine E-Mail generierte – inklusive der „bösartigen“ URL im Fehlertext.

Nun kommt der Clou: Irgendeine Komponente im Verlauf der E-Mail-Zustellung fühlte sich offenbar dazu genötigt, die enthaltenen URLs zu parsen, zu „analysieren“ und durch DNS-/HTTP-Anfragen weiter zu untersuchen – selbst bei HTML-kodierten URLs.

Welche Komponente das war, ist unklar – möglicherweise:

  • der sendende SMTP-Server,
  • der empfangende SMTP-Server,
  • ein zwischengeschalteter Mail-Scanner.
  • oder irgendeine „Sicherheits“-Software („Snakeoil“) in diesem Ablauf.

Eine ähnliche Problematik hatten wir vor über 10 Jahren bereits einmal, als ein Mail-Server ebenfalls Inhalte aus E-Mails (Download-Links) automatisch aufrief.

Fazit: Die „Intelligenz“ in den Mail-Servern war der Auslöser für das SSRF – nicht unsere Anwendung selbst!

Lösung

Unsere Lösung bestand darin, mittels eines Regulären Ausdrucks („Regex“) die URLs in den Fehlerdetails der E-Mail so zu modifizieren, dass sie nicht mehr als echte URLs erkannt werden.

Konkret: Wir ersetzen Punkte (.) in Domains durch [.], also z. B. example.comexample[.]com.

Damit können SMTP-Server oder Sicherheitsfilter die Links nicht mehr automatisch erkennen und auch nicht mehr aufrufen. Problem gelöst – SSRF behoben!

Der externe Pentest-Dienstleister hat dies im Nachtest auch bestätigt: Der Fehler tritt jetzt nicht mehr auf.

Optional könnten wir die Lösung noch erweitern und z. B. eine Whitelist mit „sicheren“ URLs pflegen. Diese könnten wir dann gezielt nicht verändern.

Technisch würde ich das vermutlich so umsetzen, dass:

  1. Für jede Whitelist-URL eine GUID im Speicher hinterlegt wird.
  2. Die GUID temporär die URL ersetzt.
  3. Der Regex greift nur auf die verbleibenden, nicht-whitelisteten URLs.
  4. Anschließend erfolgt die Rückübersetzung der GUIDs zur Original-URL.

So bleibt der Regex unverändert und die „guten“ URLs bleiben lesbar.

Aftermath

Das war in meiner Erinnerung der schwierigste Fehler seit Langem.

Vor allem, weil er:

  • so unspezifisch war,
  • kaum reproduzierbar,
  • und viele beteiligte Komponenten hatte.

Großartig, dass mein geschätzter Kollege Oliver den Fehler gefunden und behoben hat :blush:

1 „Gefällt mir“