Vor ein paar Wochen entschied ich diesen Blog meinem Heimserver-Setup hinzuzufügen, um eine Art “Ventil” für Themen zu haben, über die ich gerade nachdenke. Mehr dazu und warum ich mich entschlossen habe, diesen Blog einzurichten, in einem späteren Beitrag.
Eines der Systeme, die ich der Website hinzufügen wollte, war eine Lösung, um einen ind die allgemeine Nutzung zu erhalten. Ich entschied mich hierbei für Google Analytics, da es am einfachsten zu integrieren schien. Auch ohne Analytics konfiguriert zu haben, hatte ich die Datenschutzerklärung dafür bereits vorbereitet. Nur für den Fall der Fälle und damit ich sie nicht vergesse, falls ich letztendlich integriere.
Österreichs Datenschutzbehörde war da anderer Meinung
Gerade als ich vorhatte, mich über die Integration von Analytics zu belesen, veröffentliche heise.de einen Artikel, der besagte, dass [die Platform nicht mit dem europäischen Datenschutzgesetz zu vereinbaren ist] (https://www.heise.de/news/Oesterreichs-Datenschutzbehoerde-Google-Analytics-verstoesst-gegen-die-DSGVO-6326506.html).
Okay, also musste ich eine neue Lösung finden und ich fing an, in den Tiefen des Internets nach etwas zu durchsuchen, das meinen Bedürfnissen entsprach. Dabei fand eine nette, kuratierte Liste von Analytics-Lösungen auf github: awesome-analytics.
Dank der letzten Minuten etwas schlauer, bekam eines mein Hauptziele bei der Einrichtung des Heimservers, jedes System selbst bereit zu stellen und so viele Open-Source-Lösungen wie möglich zu verwenden. Das bedeutete, dass eine Reihe der, in der Liste angebotenen Tools mindestens eine dieser beiden Anforderungen nicht erfüllte. Auch der Datenschutz wurde dank des heise-Artikels zu einem wichtigen Aspekt meiner Entscheidung, und so wendete ich mich dem Abschnitt
"Privacy focused analytics"
zu, um einige der dort angebotenen Tools anzuschauen.
Da ich von Natur aus faul bin, entschied ich mich für die Lösung, die am einfachsten aussah und in einem Container laufen konnte, vorzugsweise auf Kubernetes, Plausible Analytics.
Installieren von Plausible und die ersten komischen Verhalten
Da die offizielle Dokumentation nur eine Installationsanleitung für docker-swarm enthält, prüfte ich ob nicht schon jemand vor mir Plausible in einer Kubernetes-Umgebung installiert hat.
Was ich fand, war ein vielversprechend aussehendes github Projekt mit einigen Manifest-Dateien. Es war einfach genug, um den Server in wenigen Schritten zum Laufen zu bringen und erforderte nicht allzu viele Anpassungen in den Konfigurationsdateien. In der Tat mussten nur drei Dateien geändert werden.
Ich fing also an alle notwendigen Kennwörter zu definieren. Da ich davon ausging, dass ich auch ohne Definition eines Mail-Servers einen ersten Eindruck gewinnen könne, ließ ich diese Settings leer. Meine fertige secret.yaml
sah am Ende in etwa so aus:
apiVersion: v1
kind: Secret
metadata:
name: plausible
type: Opaque
stringData:
SECRET_KEY_BASE: my-secret-password # a randomly generated string. the longer the better
ADMIN_USER_NAME: my-admin-username # your login user name
ADMIN_USER_EMAIL: mail@mail.mail # your login email
ADMIN_USER_PWD: some-admin-password # 100% up to you
# see postgres.yaml for setting the database password
DATABASE_URL: postgres://postgres:password@postgres-service:5432/postgres
CLICKHOUSE_DATABASE_URL: http://clickhouse-service:8123/plausible
BASE_URL: https://analytics.my-devbox.de # same address you used in ingress.yaml
MAILER_EMAIL: <todo:replaceme> # email@example.com
SMTP_HOST_ADDR: <todo:replaceme> # smtp.example.com
SMTP_HOST_PORT: "465"
SMTP_HOST_SSL_ENABLED: "true"
SMTP_USER_NAME: <todo:replaceme> # email@example.com
SMTP_USER_PWD: <todo:replaceme>
DISABLE_REGISTRATION: "true"
Alles lief prima und ich hatte schnell alle notwendigen Pods stabil laufen
$ kubectl get po
NAME READY STATUS RESTARTS
clickhouse-statefulset-0 1/1 Running 0
plausible-5f5ffd7497-nfl66 1/1 Running 0
postgres-statefulset-0 1/1 Running 0
Ich loggte mich also auf dem Server ein und definierte die zu analysierende Website. Als ich den Prozess abschloss erschien die unten dargestellte Fehlermeldung (die ich im Laufe der Ermittlungen noch öfter zu Gesicht bekommen werde)
Nach einem Blick in die Logs wurde klar, dass das fehlende Mail-Server Setup verantwortlich war. Das war auch so lange kein Problem, bis ich mein User-Kennwort ändern wollte. Per Design ist eine Rücksetzung des Kennworts nur per Mail möglich. Ich musste also die SMTP-Einstellungen doch definieren - kein Problem.
Die einzelnen Iterationen meiner Versuche habe ich mit meinem vorher definierten Benutzer getestet, was jedes mal in der mir bekannten Fehlermeldung endete. So weit so gut, ist ja erstmal nicht weiter dramatisch. Weil es einfacher zu schreiben war, wechselte ich irgendwann zu einem Pseudo-Benutzer test@test.de
und bekam folgende Meldung:
Mein erster Gedanke war “Endlich, alles fertig”, also versuchte ich mein reales Benutzer-Kennwort zurück zu setzen. Dabei wurde ich wieder von der bereits bekannten “Error 500”-Meldung überrascht.
MOMENT! Das bedeutet doch, dass die Rücksetzung bekannter und unbekannter Mail-Adressen unterschiedlich gehandhabt werden?! Stellt das nicht eine Sicherheitsproblem dar?
Die Erfolgsmeldung für meine falsche Mail besagte eindeutig "[...] if it exists in our database." , was mir sagte, dass der Hinweis in beiden Fällen angezeigt werden soll. Obwohl mir bewusst war, dass das eigentliche, zugrundeliegende Problem für die Fehlermeldung mein SMTP-Setup war, wusste ich, dass ich etwas gefunden hatte. Die folgenden paar Minuten versuchte ich mein System in einen stabilen und lauffähigen Zustand zu bekommen, damit ich eine genauere Analyse starten konnte. Irgendwann hatte ich dann eine E-Mail in meinem Postfach und war bereit für ein bisschen Pen Testing.
Ich hatte noch im Hinterkopf, dass ich letzten Sommer auf Youtube ein bis zwei Video über so genannte “Timing Attacks” gesehen habe.
Timing attacks sind einfach gesagt, Angriffsvektoren auf IT Systeme, bei denen der Angreifer durch Messen und Vergleichen der Antwortzeiten öffentlich erreichbarer Ressourcen (z.B. Web Services), Einblick in kritische Daten des Ziels bekommen kann.
Die bisherigen Ergebnisse deuteten auf ein synchrones Verhalten beim Mail-Versand hin. Das System wartet also auf die Antwort eines Mail-Servers, bevor eine Antwort an den Benutzer zurückgeliefert wird.
In meinen ersten Angriffsversuchen verglich ich die Antwortzeiten meines Servers noch “per Hand”, bzw. durch die Entwickler-Werkzeuge meines Browsers. Das war, für den Anfang, ausreichend um ein erstes Gefühl zu bekommen, wie schlecht (zumindest in meinem System) die Lage wirklich war.
Für eine unbekannte Mail antwortete mir der Server nach ca. 80ms, die Zeiten für eine bekannte Adresse waren signifikant höher: 1200ms.
Ein Datenpunkt macht allerdings noch keine Statistik und ist daher wenig aussagekräftig. Schließlich könnten auch externe Faktoren wie Netzwerkschwankungen für die Unterschiede verantwortlich sein.
Mehr Informationen durch Automatisierung des Pen Tests
Um die Zeiten in “großem Stil” zu messen, waren der Browser und die Entwicklungswerkzeuge keine Option - ich wollte automatisieren. Also startete ich mein Tool der Wahl, Postman, und machte mich an die Arbeit.
Der notwendige Endpunkt war schnell durch meine vorherigen Experimente gefunden. Was ich nicht bedacht hatte war die Notwendigkeit eines
CSRF-Tokens
an der Nachricht, welches ich vorher erst einmal auslesen musste. Ein paar kleinere Scripts später war alles fertig und ich war bereit die Tests durchzuführen.
Der Server erlaubt nur fünf konsekutive Anfragen zur Rücksetzung eines Passworts. Das war aber genug für ein Proof of Concept und um das zugrunde liegende Sicherheitsproblem aufzuzeigen.
Für unbekannte Mail-Adressen erhielt ich eine durchschnittliche Antwortzeit von 73ms. Bei Anfragen für valide Adressen bekam ich im Schnitt nach 1793ms eine Antwort (jeweils mit 5 Stichproben erhoben - nicht viel, aber mehr als ich vorher hatte). Zusätzlich war keine der Anfragen auf bekannten Mails schneller als die langsamste Antwort einer unbekannten Adresse.
Jetzt wurde ich leicht nervös.
Habe ich gerade wirklich eine potentiell ausnutzbare Sicherheitslücke gefunden?
Sollte es durch das Verhalten nicht möglich sein zu prüfen, ob eine bestimmte Mail-Adresse auf dem Server bekannt ist? Durch lediglich ein paar wenige Versuche pro Adresse...
Um den Vektor effektiv ausnutzen zu können, würde eine ausreichend umfangreiche Liste von bekannten Mail-Adressen benötigt - aber die gibt es bereits durch vorherige Datenlecks. Außerdem neigen Menschen dazu Mail-Adressen mehrfach zu verwenden. Klar, das Problem war kein Zero-Day, in meinen Augen aber kritisch genug um mich zu fragen
Was mache ich jetzt?
Prüfung des Quellcodes
Mein nächster Schritt bestand darin, den Quellcode von Plausible zu prüfen. Gut, dass ich mich dafür entschieden hatte ein Open-Source-Projekt zu verwenden. Dadurch konnte ich meine Funde im Code bestätigen - vorausgesetzt ich kann ihn lesen. Ich fand den Schuldigen in der Methode password_reset_request()
.
Die Entwickler waren nett genug, den Klassen und Methoden sprechende Namen zu geben. Dadurch war ich schnell und einfach in der Lage den Code nachzuvollziehen, auch wenn ich die genutzte Programmiersprache nicht kannte.
def password_reset_request(conn, %{"email" => email} = params) do
if PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do
user = Repo.get_by(Plausible.Auth.User, email: email)
if user do
token = Auth.Token.sign_password_reset(email)
url = PlausibleWeb.Endpoint.url() <> "/password/reset?token=#{token}"
Logger.debug("PASSWORD RESET LINK: " <> url)
email_template = PlausibleWeb.Email.password_reset_email(email, url)
Plausible.Mailer.deliver_now!(email_template)
render(conn, "password_reset_request_success.html",
email: email,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
else
render(conn, "password_reset_request_success.html",
email: email,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
end
else
render(conn, "password_reset_request_form.html",
error: "Please complete the captcha to reset your password",
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
end
end
Ich hatte Recht. Der Code sendet für bekannte User tatsächlich eine Mail, wartet auf die Antwort und erzeugt erst danach die Erfolgs-Meldung. Als nächstes musste also ein Bug gemeldet werden. Plausibles Entwickler haben eine eigene Seite erstellt, auf der erklärt wird, wie bei der Offenlegung von Security Problemen umzugehen ist. Dort wurde eine Mail Adresse genannt, an welche man sich zu wenden hatte um Probleme aufzuzeigen.
Verantwortungsvolle Offenlegung (responsible disclosure)
Nachdem ich alle Informationen und Daten gesammelt hatte, schrieb ich eine Mail mit meinen Befunden an die genannte Adresse.
Hi Plausible Team,
[…]
When setting up the Server, I misconfigured my mail-configurations and found some strange behavior: I got a success message when trying ro recover the password for an unknown user, but a way longer roundtrip and a failure-message when doing so for the correct user.
I had a look into your code für the password_reset_request found, that you configured the mail-delivery as a synchronous call before rendering the success page https://github.com/plausible/analytics/blob/9022234aa6546a146929556b3ef3811b6d42b5a3/lib/plausible_web/controllers/auth_controller.ex#L250
This could create ( together with a sufficient big dictionary of leaked mail-addresses ) a potential timing attack vector to check for the addresses in your database. After fixing my environment, I ran a test against it to check for response times (both based on 5 consecutive requests):
Trying to recover for wrong mail: 73ms roundtrip on average Trying to recover for existing mail: 1793ms roundtrip on average
Since you are returning a success in both cases, wouldn’t it be more secure to invoke the mail-delivery after rendering the success message?
Feel free to contact me at any time, if you have further questions.
KR Daniel
Natürlich kannte ich das Konzept verantwortungsvoller Offenlegung. Allerdings habe ich darüber gar nicht nachgedacht, bis ich eine Antwort von Uku, einem der Entwickler, erhielt.
Hey Daniel,
You are absolutely correct. We explicitly protect against timing attacks on the login page to prevent leaking email addresses. The same precaution must be taken on the password reset page as well but it slipped my mind.
Thank you for disclosing this privately and responsibly.
[…]
Thanks again, Uku
Zu diesem Zeitpunkt realisierte ich, dass ein Sprechen über das Thema vermutlich unter eine nicht ausgesprochene Verschwiegensheitsvereinbarung fällt. Zumindest bis das Problem behoben wurde.
Aber da Uku innerhalb weniger Stunden antwortete, war ich mir sicher, dass eine Behebung bald auf dem Weg sein würde. Nichtsdestotrotz fragte ich an, ob ich, als ersten Beitrag, hier über die Funde berichten dürfe. Ich erhielt die Erlaubnis, so lange ich mit der Veröffentlichung warten würde, bis eine Lösung ausgeliefert wurde.
Nach ein paar Tagen Warten erhielt ich eine Mail, in der ich von Uku darum gebeten wurde die frisch veröffentlichte Version, inklusive einer Lösung für mein Problem, zu prüfen.
Sobald ich nach Hause kam, installierte ich ein System mit der neuen Version. Spätestens jetzt war ich froh mich für eine Container-basierte Lösung entschieden zu haben, da die Neukonfiguration der Kubernetes-Manifeste und der Start des Servers lediglich ein paar Sekunden in Anspruch nahm.
Eine erste manuelle Prüfung des Fixes sah vielversprechend aus, also ließ ich ein paar automatisierte Tests laufen.
Zuerst mit meinem initialen Use Case, so wie ich das Problem ursprünglich fand - mit falsch konfigurierten SMTP-Einstellungen. Die Zeiten sahen gut aus: im Durchschnitt, über jeweils 5 Versuche, 79ms für unbekannte und 80,8ms für bekannte Mail-Adressen. Zusätzlich wurde jetzt in beiden Fällen die Erfolgsmeldung generiert. Ein Datenleck konnte also auch bei falscher Konfiguration nicht mehr erfolgen.
Als nächstes wollte das Problem in einer produktionsnahmen Umgebung geprüft werden - seien wir mal ehrlich, die SMTP-Einstellungen sollten in öffentlichen Live-Systemen korrekt konfiguriert sein.
Um sicher zu gehen, prüfte ich 10 Datenpunkte je Szenario und erhielt durchschnittliche Antwortzeiten von 78,9ms für unbekannte, sowie 83,5ms für bekannte Mail-Adressen. Meiner Meinung nach sahen die Zahlen “ähnlich genug” aus.
Der Statistik-Nerd in mir sagte jedoch, dass ~5% Abweichung immer noch
als statistisch signifikant anzusehen sind.
Hier musste ich ihn jedoch zum Schweigen bringen.
Nachdem ich geprüft hatte, dass der Mail-Verstand immer noch funktionierte, bestätigte ich die erfolgreiche Behebung bei Uku
Hi Uku,
great to hear this. […] In my opinion this looks good and similar enough to not get any significant difference from just a few tries per mail address. When having the smtp-server configured correctly the mails were also send correctly (so no functional error was introduced by this fix)
Thank you for your support.
Do you need any more information from me, to close this issue?
Thanks Daniel
Nachdem wir noch ein paar zusätzliche Nachrichten ausgetauscht hatten, sah ich das Problem als behoben an und begann mit der Planung dieses Artikels.
Was habe ich aus der ganzen Sache gelernt?
Das gesamte Erlebnis war für mich ziemlich spannend, da der gesamte Prozess für mich neu war. Meine anfänglichen Bedenken erwiesen sich jedoch schnell als unbegründet.
Uku hat das Problem sofort anerkannt, was mir einem enormen Vertrauensschub verschaffte. Die paar Tage haben mir wieder einmal gezeigt, wie toll die Open-Source-Gemeinschaft ist. Das Hauptziel besteht stets darin ein cooles und und großartiges Softwareprodukt zu entwickeln.
Persönlich habe ich hierdurch die Erfahrung gemacht, dass auch ein kleines bisschen Wissen außerhalb meiner Komfortzone mir erstaunliche Möglichkeiten und Erfahrungen eröffnen kann. Auch ohne zu wissen, wie Timing-Attacks in der Praxis ablaufen und umgesetzt werden, hat mir die Kenntnis über das Konzept geholfen, das Problem zu identifizieren und einzuordnen. Ein Grundverständnis für Quellcode half bei der Analyse und bei der Suche nach der Ursache.
Natürlich habe ich meinen eigenen Plausible Server sofort aktualisiert und freue mich in den nächsten Tagen hoffentlich etwas Bewegung in den Graphen zu sehen. Und da dies nun meine genutzte Site-Analytics-Lösung ist, habe ich die Datenschutzerklärung entsprechend angepasst.
Letztendlich hoffe ich, dass ich diese Gelegenheit für einen ersten Beitrag als gutes Zeichen für das kommende Jahr bzw. die kommenden Jahre deuten kann. 😃