Lauscher an der Wand (Linux-Magazin, Mai 2019)

Klappt's mal nicht so ganz beim Entwickeln einer Web-Applikation, stellt sich sofort die Frage, welche Daten Browser und Webserver eigentlich austauschen. Hierzu eignen sich Tools zum Schnüffeln im Netzwerk wie Wireshark, aber auch Proxies, die zwischen Client und Server klemmen, dabei Anfragen und Antworten durchleiten, und nebenbei fleißig mitschreiben. Mitmproxy ("Man-in-the-middle Proxy") ist der Platzhirsch in dieser Kategorie, denn er macht das Unmögliche möglich, in dem er selbst verschlüsselte https-Requests mitprotokolliert.

Aber zunächst zum einfachsten, unverschlüsselten Fall, für den sich meine olle noch mit blankem unverschlüsseltem http arbeitende Webseite perlmeister.com eignet. Abbildung 1 zeigt, wie der Browser den HTML-Text der Seite, sowie einige Bildchen und JavaScript-Schnipsel vom Server einholt. Die Terminal-UI von mitmproxy, das als Binary auf [4] zum Download bereitsteht, zeigt links vor dem aktuell ausgewählten "Flow" genannten Request einen Doppelpfeil. Drückt der User die "Enter"-Taste, kommen, wie in Abbildung 2 gezeigt, die detaillierten Request-Daten hoch. Mit den Cursor- oder den vi-typischen hjkl-Tasten fährt der User anschließend nach rechts oder links und springt zwischen "Request", "Response" und "Details" hin und her. Zurück zur nächsthöheren Ebene geht's stets mit "q". Auch die Auswahl eines bestimmten Requests auf der höchsten Navigationsebene geht über die Cursortasten (bzw. "j" für runter und "k" für rauf). "d" löscht den mit ">>" bezeichneten Flow, und Hämmern auf "d" fegt den Bildschirm leer.

Abbildung 1: Der Proxy-Server protokolliert mit, welche HTTP-Requests der Browser beim Aufruf einer Webseite absetzt.

Abbildung 2: Auf Tastendruck (Enter) zeigt der Proxy detaillierte Request/Response-Daten.

Damit der Proxy anfängt, zwischen Client und Server zu lauschen, bietet mitmproxy mehrere Ansätze, die einfachste Lösung ist die Konfiguration auf der Seite des Clients, der typischerweise Optionen für HTTP- oder SOCKS-Proxies bereithält. Der Aufruf mitmproxy startet den Proxy und färbt das laufende Terminalfenster schwarz, die Konsole ist nun betriebsbereit.

Abbildung 3: Auf Ubuntu verweigert Chromium die Proxy-Einstellung im Settings-Menü.

Wer jetzt allerdings versucht, Chromium auf Ubuntu zum Überstülpen eines Proxies zu überreden, offiziell unter Settings->Advanced->Security->Open Proxy Configuration, erlebt sein blaues Wunder, denn die Desktop-Applikation weigert sich mit einer Fehlermeldung (Abbildung 3). Zum Glück akzeptiert das Programm aber die Kommandozeilenoption

    $ chromium-browser --proxy-server="localhost:8080"

und auf diesem Host ("localhost") und diesem Port (8080) lauscht mitmproxy standardmäßig. Wichtig ist auch noch, dass Chromium als Einzelinstanz auf dem Ubuntu-Desktop läuft, wer vorher schon eine laufen hatte, rauft sich die Haare, warum's nicht funktioniert. Ruft der User nach erfolgreicher Konfiguration dann URLs im Browser ab, erscheinen die Daten in der Proxy-UI zur weiteren Analyse.

Verschlüsselung knacken

So weit, so einfach, doch was passiert, falls der Browser einen HTTPS-Request absetzt? Das Protokoll wehrt entwurfsgemäß Lauschangriffe von Mittelsmännern ab, und in der Tat meldet ein mit mitmproxy konfigurierter Chromium-Browser sofort die laufende Attacke (Abbildung 4). Klickt der User allerdings unter Advanced den Link "Proceed (unsafe)", öffnet Chromium die Schranke und lässt den User auf eigene Verantwortung weitermachen, worauf Mitmproxy die Verbindungsdaten eifrig mitprotokolliert. Aber, mal dumm gefragt, wie ist das überhaupt möglich, wo doch Client und Server eine verschlüsselte Verbindung aufbauen?

Abbildung 4: Lauscht mitmproxy auf einer https-Verbindung, merkt der Browser dies am gefälschten Zertifikat.

Abbildung 5: Mitmproxy hängt sich zwischen Client und Server und präsentiert sich dem Client als Server, und dem Server als Client (Lizenz der Abbildung prüfen, gegebenenfalls eigenes Diagramm erstellen?).

Der revolutionäre Trick von mitmproxy ist nun, dass er sich gegenüber dem Client als Server und gegenüber dem Server als Client ausgibt (Abbildung 5). Statt der wirklichen Server-Antwort schickt er dem Client ein gefälschtes Zertifikat, worauf der Client (wenn er das Zertifikat nicht prüft, oder zulässt, dass der User trotz Warnung weitermachen will) eine verschlüsselte Verbindung mit dem Proxy (und nicht mit dem Server) aufbaut. Der Proxy kann deswegen die Daten natürlich problemlos entschlüsseln, schreibt sie mit, und verschlüsselt sie anschließend wieder zur Weiterleitung an den Server, dem gegenüber er sich als Client ausgibt. Die klassische Man-in-the-Middle-Attacke ([2]) also.

Abbildung 6: Scheinbar mühelos lauscht mitmproxy auf verschlüsselten https-Verbindungen mit.

Härtefälle

Erlaubt der Client auch auf Nachfrage keinerlei Kommunikation über offensichtlich gehackte Kanäle, schiebt ihm der Nutzer ein gefälschtes Certificate-Authority-(CA)-Zertifikat unter, das mitmproxy gleich mitliefert. Viele Clients (zum Beispiel Mobiltelefone) erlauben keine Proxy-Konfiguration oder ignorieren sie schlicht. Für diese Fälle bietet mitmproxy die Möglichkeit des transparenten Proxy-Modus, bei dem die Daten über Netzwerk-Tools wie iptables an den Proxy statt an den Server geleitet werden. Auf [3] finden sich auch Lösungen für diese hartnäckigeren Fälle.

Listing 1: client.go

    01 package main
    02 
    03 import (
    04   "fmt"
    05   "io/ioutil"
    06   "net/http"
    07 )
    08 
    09 func main() {
    10   resp, err := http.Get("https://amazon.com")
    11   if err != nil {
    12     panic(err)
    13   }
    14   defer resp.Body.Close()
    15   body, err := ioutil.ReadAll(resp.Body)
    16   fmt.Println(string(body))
    17 }

Selbstgeschriebene Programme in Go verlassen sich auf den CA-Store des Systems. Dieser kommt auf Ubuntu im Paket ca-certificates daher und enthält das Fälscher-CA-Zertifikat von mitmproxy von Haus aus natürlich nicht. Als Beispiel setzt Listing 1 einen https-Request ab, um die TLS-gesicherte Webseite eines bekannten Online-Riesen einzuholen. Bei laufendem mitmproxy zeigt sich folgende Fehlermeldung, falls der Client wie gezeigt mittels HTTP_PROXY auf den Proxyserver eingenordet ist:

    $ go build client.go
    $ HTTP_PROXY=localhost:8080 ./client 
    panic: Get https://usarundbrief.com: x509: 
    certificate signed by unknown authority

Statt Amazons Zertifikat hat mitmproxy dem Client ein Selbstunterschriebenes untergejubelt, was der Client bemängelt. Damit der Client das Zertifikat dennoch (nur zu Testzwecken natürlich) akzeptiert, kopiert es die Befehlsfolge in Abbildung 7 in die Sammlung des Ubuntu-Systems. Nachfolgende Aufrufe von

    $ HTTP_PROXY=localhost:8080 ./client

spucken klaglos die TLS-gesicherte Webseite von Amazon aus, während mitmproxy als Mittelsmann fleißig als mitschreibt.

Abbildung 7: Diese Befehlsfolge jubelt dem Ubuntu-System das Fälscher-CA-Zertifikat von mitmproxy unter.

Übrigens nutzt Chrome auf Ubuntu nicht den System-Store für vertrauenswürdige Certificate Authorities (CAs), sondern bringt seine eigene Sammlung mit. Um diese um das Fälscherzertifikat zu erweitern, bedarf es einiger Kniffe, die mitmproxy erklärt, wenn der User im Browser die magische Domain mitm.it bei konfiguriertem Proxy ansteuert und das gewünschte Betriebssystem auswählt (Abbildung 8). Firefox bietet ebenfalls ein Konfigurationsmenü, um ein neues Zertifikat einzuhängen (Firefox->Options->Privacy&Security->View Certificates->Authorities->Import).

Abbildung 8: Für Instruktionen, um Linux ein Mitmproxy-konformes CA-Zertifikat unterzujubeln, ist hier "Other" zu drücken.

Hausgemachte Erweiterung

Doch damit nicht genug, als wahrer Tausendsassa akzeptiert mitmproxy auch selbstgeschriebene Python-Skripts als Erweiterungen. Als Beispiel zeigt Listing 2 ein Skript im von mitmproxy verlangten Python3, das die URLs aller eintreffenden Antworten aufschnappt, und mitsamt der Länge der zugehörigen Webantwort in Bytes in der Datei "dump.log" ablegt.

Listing 2: URLDumper.py

    01 #!/usr/bin/env python3
    02 from mitmproxy import ctx
    03 import re
    04 
    05 class URLDumper:
    06   def __init__(self):
    07     ctx.log.warn( "URLDumper ready" )
    08 
    09   def request(self, flow):
    10     ctx.log.warn( "URLDumper request" )
    11     flow.request.anticache()
    12 
    13   def append_to_dump(self, url, content):
    14     with open("dump.log", "a") as f:
    15       f.write("%s (%d bytes)\n" %
    16               (url, len(content)))
    17 
    18   def response(self, flow):
    19     url = flow.request.url
    20     self.append_to_dump(url,
    21             flow.response.content)
    22 
    23 addons = [
    24     URLDumper()
    25 ]

Hierzu importiert es in Zeile 2 für spätere Aufrufe der Konsolen-Logging-Funktion in Zeile 2 das Kontext-Objekt ctx aus dem Paket mitmproxy, welches das Skript automatisch findet, sobald mitmproxy es einbindet, ohne dass der User es irgendwo installieren müsste. Der Proxy kann sogar alle ausgehenden Webanfragen so umschreiben, dass kein Caching stattfindet. Hierzu springt der Proxy vor jedem Request die Methode request in Zeile 9 an, die in der Variable flow ein Objekt auf den aktuellen Webvorgang mitbekommt, und damit die Methode anticache() des Request-Objekts aufruft, die Header wie "If-modified-since" einfach verwirft. Trotzdem findet in Browsern manchmal ein Caching statt, weil der Browser einfach gar nicht beim Server nachfragt, ob sich eine Abbildung geändert hat, aber ein "Shift-Reload" zwingt ihn dazu.

Bei jeder eintrudelnden Antwort springt der Proxy die Methode response() in Zeile 17 an. Dort extrahiert Listing 2 erst den URL der Anfrage, holt dann mit dem Attribut content den Inhalt der eingeholten Web-Resource als String ein, und gibt beides zum Mitprotokollieren an die Methode append_to_dump() ab Zeile 13. Sie öffnet die Datei dump.log im Append-Modus, und hängt den URL sowie die Länge des Content-Strings in lesbarem Format an. Der Aufruf

    $ mitmproxy --mode regular -s URLDumper.py \
      console_eventlog_verbosity="warn"

gibt dem Proxy das neue Python-Skript mit und zeigt in der Fußzeile des Proxyfensters kurz nach dem Start die Meldung "URLDumper ready", zur Bestätigung dafür, dass die Initialisierungsfunktion der Erweiterung erfolgreich durchlaufen wurde. Der Proxy-Prozess überwacht übrigens laufend das Python-Skript, ändert es sich zur Laufzeit, merkt der Proxy das und initialisiert das Skript praktischerweise gleich neu. Nach einer Browser-Session, die "localhost:8080" als Proxy eingestellt hatte und die Startseite des Linux-Magazin besuchte, fanden sich die in Abbildung 9 gezeigten Einträge in dump.log.

Abbildung 9: Mit der Erweiterung in Listing 2 hat der Proxy in dump.log mitprotokolliert, welche URLs der Browser beim Besuch auf linux-magazin.de angefahren hat.

Wer Fehler suchet ...

Tritt im Python-Skript ein Fehler auf, entweder beim Kompilieren oder zur Laufzeit, zeigt das Konsolenfenster eine ominöse Nachricht an, nämlich dass Details dazu "im Eventlog" lägen. Einiges Stöbern im Netz brachte die Antwort zutage: Der User tippt dazu

    :console.view.eventlog

in das Konsolenfenster, dann wechselt letzteres zu der gesuchten Fehlermeldung, die anzeigt, welche Zeile Python zu bemeckern hatte.

Eine weitere Killeranwendung von mitmproxy ist das Mitprotokollieren von Batterien von Request-Sequenzen, um sie später zu Testzwecken wieder auf den Server loszulassen. Die Manualseiten, sowohl zur Bedienung der Konsole als auch die Möglichkeiten der API, findet der Hilfesuchende auf mitmproxy.org. Ihr Inhalt sieht etwas lieblos zusammengeschustert aus, aber der geduldige Forscher findet meist, was er braucht. Oh, und bevor ich's vergesse: Wer auf seinem System nach dem Testen mit mitmproxy keine klaffende Securitylücke hinterlassen will, sollte das zu Testzwecken hinzugefügte CA-Zertifikat wieder entfernen. Sicher ist sicher.

Infos

[1]

Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2019/05/snapshot/

[2]

"Man-in-the-middle-attack", Wikipedia, https://en.wikipedia.org/wiki/Man-in-the-middle_attack

[3]

https://docs.mitmproxy.org/stable/concepts-howmitmproxyworks/

[4]

Linux-Binaries für Mitmproxy: https://mitmproxy.org/downloads/#4.0.4/

[5]

Michael Schilli, "Titel": Linux-Magazin 12/16, S.104, <U>http://www.linux-magazin.de/Ausgaben/2016/12/Perl-Snapshot<U>

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 5:

Unknown directive: =desc