MITMProxy im Docker-Zoo

vorhergehende Artikel in: Linux Docker Virtualisierung
02.04.2023

Es ist wieder mal ein neuer Container in meinem Docker-Zoo eingezogen

Nachdem ich bereits andere Docker-Container zur Unterstützung bei der Softwareentwicklung in meinen Zoo aufgenommen habe, war nun endlich MITMProxy, ein in Python geschriebenes Werkzeug zum Aufbrechen von TLS-verschlüsselten Verbindungen an der Reihe.

Warum würde man solche sicheren Verbindungen aufbrechen wollen? Nun, zum einen kommen da Smartphones in Betracht. Das allein erklärt jemandem, der sich noch nie aktiv mit Datensparsamkeits- und Datenschutzaspekten solcher und ähnlicher "smart" Devices beschäftigt hat, noch nicht viel. Solche Geräte - auch alle smartHome-Geräte oder Sprachassistenz-Systeme - nehmen gerne mal mit dem Mutterschiff Kontakt auf, um Daten dorthin zu übertragen.

Falls man paranoid genug ist, diesen Geräten nicht zu vertrauen, kann man - und muss man, da diese Verbindungen immer mehr oder weniger gut verschlüsselt sind - mit einem solchen MITM-Proxy diese Verschlüsselung einfach aufbrechen und im Klartext mitlesen, worüber sich das Gerät und sein Mutterschiff denn so unterhalten.

Für Entwicklungszwecke kann es hin und wieder ebenfalls ganz nützlich sein, die verschlüsselten Kommunikationsinhalte einer neuen Anwendung im Klartext vor sich zu sehen.

Und natürlich setzen Unternehmen solche Systeme gerne mal ein, um zum Beispiel ihr Firmennetz mittels einer Deep-Packet-Inspection vor Unbill zu schützen.

Technisch funktioniert das so, dass das Betriebssystem oder die Client-Anwendung so konfiguriert wird, dass sie nicht direkt mit dem Server kontakt aufnimmt, sondern dies über einen Proxy tut. Proxies waren früher Systeme, die beispielsweise bei Volumenverträgen oder Verbindungen mit geringer Bandbreite zum Internet dafür sorgten, dass die Menge der übertragenen Daten möglichst gering bleib, indem sie einen intelligenten Cache an der Außenkante des lokalen Netzwerks implementierten.

Diese Funktionalität könnte auch für TLS-Verbindugnen immer noch genutzt werden - in diesem Fall wird die Verschlüsselung nicht gebrochen. Es ist aber auch möglich, dass der Proxy als echter "Man-in-the-Middle" - nichts anderes bedeutet MITM - agiert: der Client sagt dem Proxy in diesem Fall, mit wem er Verbindung aufnehmen möchte. Der Proxy baut dann die TLS-Verbindung zum eigentlichen Kommunikationsziel auf und baut einen zweiten verschlüsselten Kanal zum Client auf, so dass dieser weiterhin eine mittels TLS verschlüsselte Verbindung sieht.

Der Proxy kann sich aber gegenüber dem Client nicht als der eigentliche Server ausgeben - dazu bräuchte er Zugriff auf den privaten Schlüssel des Servers und den hat er nicht. Er erzeugt vielmehr on-the-fly entsprechende Zertifikate für den Server um sie dem Client zu präsentieren - diese Zertifikate sind aber von seiner eigenen CA signiert (und damit eigentlich gefälscht).

Hier würde man sich wünschen, dass mehr Menschen sich den jeweiligen Schlüssel des Zertifikats und am besten aller Zertifikate der Kette anzusehen und mit einer Vorlage, die über einen sicheren Kanal erhalten wurde zu vergleichen...

Und damit haben wir schon die erste Aufgabe: Damit dies alles nämlich funktioniert, muss der Proxy den Client dazu bringen, Zertifikaten zu vertrauen, die dieser ausgestellt hat. MITMProxy benutzt dazu seine eigene on-the-fly generierte Root-CA. Deren Zertifikat kann man kopieren und als vertrauenswürdige CA im eigenen Truststore - global oder spezifisch für den jeweiligen Client - hinterlegen.

Es ist aber auch möglich - fall man sowieso schon eine eigene PKI betreibt - eine spezielle digitale Identität für MTIMProxy zu erstellen, die dieser dann benutzt, um die "gefälschten" Zertifikate auszustellen. Ist das Zertifikat der Root-CA der eigenen PKI bereits im System als vertrauenswürdig eingestuft, muss man also am Client zur Nutzung von MITMProxy nichts mehr ändern.

Ich wollte - wie bereits oben geschrieben - die Lösung in meinem Docker-Zoo einsetzen und daher suchte ich nach einem bereits vorhandenen Dockerfile und einem darauf aufbauenden docker-compose.yml.

Ich fand etwas, auf dem ich aufbauen konnte, musste dies jedoch stark anpassen. Ich musste mich recht früh davon verabschieden, die Weboberfläche über den reverse-Proxy Traefik anzubieten, da MITMProxy derzeit den Zugriff darauf nur per IP-Adresse und nicht per (DNS-)Namen gestattet.

Das Unterschieben einer von mir erstellten digitalen Identität zum Signieren der gefälschten Server-Zertifikate war einfach. Schwieriger war es, das Root-CA-Zertifikat meiner eigenen PKI MITMProxy selbst als vertrauenswürdig zu definieren. Ich musste dazu ein Dockerfile erstellen, das zunächst meine eigenen vertrauenswürdigen Zertifikate zum systemweiten Truststore hinzufügte. Anschließend - da es sich hier um eine Python-Anwendung handelte und Python per Default den systemweiten Truststore ignoriert - musste ich den modifizierten Truststore MITMProxy noch als Truststore unterschieben.

Dadurch sind die folgenden beiden Dateien entstanden:

FROM mitmproxy/mitmproxy

ADD root-ca-cert.pem /usr/local/share/ca-certificates/foo.crt RUN chmod 644 /usr/local/share/ca-certificates/foo.crt && update-ca-certificates

version: '3'
services:
  mitmweb:
    build: .
    #image: mitmproxy/mitmproxy
    tty: true
    ports:
#HTTP-PROXY
      - 6080:8080
#SOCKS-PROXY
      - 4080:1080
#WEB-FRONTEND
      - 6081:8081
#HTTP-PROXY
    command: mitmweb --web-host 0.0.0.0 --cert-passphrase $CERT_PASSPHRASE --set ssl_verify_upstream_trusted_ca=/etc/ssl/certs/ca-certificates.crt
#SOCKS-PROXY
#    command: mitmweb --web-host 0.0.0.0 --mode socks5 --cert-passphrase $CERT_PASSPHRASE --set ssl_verify_upstream_trusted_ca=/etc/ssl/certs/ca-certificates.crt
    volumes:
      - ./mitmproxy:/home/mitmproxy/.mitmproxy

Ich benutzte zum Testen eine stark angepasste Version des hier zu findenden Grundgerüsts:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;

import org.apache.http.Header; import org.apache.http.HttpHost; import org.apache.http.HttpResponseInterceptor; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.ManagedHttpClientConnection; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpCoreContext; import org.apache.http.ssl.SSLContexts; import org.apache.http.util.EntityUtils;

import javax.net.ssl.*;

/** * How to send a HTTP or HTTPS request via SOCKS proxy. * https://stackoverflow.com/a/32370018 */ public class ClientExecuteSOCKS { private final static java.lang.String SOCKSPROXYADDRESS="SOCKSPROXYADDRESS"; private final static java.lang.String SOCKSPROXYHOST="192.168.10.3"; private final static short SOCKSPROXYPORT=4080; private final static java.lang.String HTTPPROXYADDRESS="HTTPPROXYADDRESS"; private final static java.lang.String HTTPPROXYHOST="192.168.10.3"; private final static short HTTPPROXYPORT=6080; private static final String PEER_CERTIFICATES = "PEER_CERTIFICATES";

public static void main(String[] args) throws Exception { java.io.File pemFile=new java.io.File("/home/elbosso/tests/tls/Dama11_Root_CA-ca.crt");

KeyStore trustStore= de.elbosso.util.security.Utilities.initializeTruststoreWithPemCertificates(pemFile,true);

SSLContext sslContext = SSLContext.getInstance("TLS");

TrustManagerFactory tmf = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(trustStore);

sslContext.init(null, tmf.getTrustManagers() , null);

Registry<ConnectionSocketFactory> reg = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", new MyHTTPConnectionSocketFactory()) .register("https", new MyHTTPSConnectionSocketFactory( //sslContext SSLContexts.createSystemDefault() )) .build(); PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(reg); //https://memorynotfound.com/apache-httpclient-get-server-certificates/ // create http response certificate interceptor HttpResponseInterceptor certificateInterceptor = (httpResponse, context) -> { ManagedHttpClientConnection routedConnection = (ManagedHttpClientConnection)context.getAttribute(HttpCoreContext.HTTP_CONNECTION); SSLSession sslSession = routedConnection.getSSLSession(); if (sslSession != null) {

// get the server certificates from the {@Link SSLSession} Certificate[] certificates = sslSession.getPeerCertificates();

// add the certificates to the context, where we can later grab it from context.setAttribute(PEER_CERTIFICATES, certificates); } };

try (CloseableHttpClient httpclient = HttpClients.custom() .setConnectionManager(cm) .addInterceptorLast(certificateInterceptor) .build()) { InetSocketAddress socksaddr = new InetSocketAddress(SOCKSPROXYHOST, SOCKSPROXYPORT); InetSocketAddress httpaddr = new InetSocketAddress(HTTPPROXYHOST, HTTPPROXYPORT); HttpClientContext context = HttpClientContext.create(); // context.setAttribute(SOCKSPROXYADDRESS, socksaddr); context.setAttribute(HTTPPROXYADDRESS, httpaddr);

HttpHost target = new HttpHost("google.com", 443, "https"); HttpGet request = new HttpGet("/");

System.out.println("Executing request " + request + " to " + target + " via SOCKS " + "proxy " + socksaddr); try (CloseableHttpResponse response = httpclient.execute(target, request, context)) {

//https://memorynotfound.com/apache-httpclient-get-server-certificates/ // obtain the server certificates from the context Certificate[] peerCertificates = (Certificate[])context.getAttribute(PEER_CERTIFICATES); if(peerCertificates!=null) { System.out.println("----------------------------------------\nCertificate(s):"); for(Certificate peerCertificate:peerCertificates) { System.out.println(((X509Certificate) peerCertificate).getSubjectX500Principal()); } } else System.out.println("no certificates found!"); System.out.println("----------------------------------------\nHeaders:"); Header[] headers= response.getAllHeaders(); for(Header header:headers) { System.out.println(header); } System.out.println("----------------------------------------\nStatus:"); System.out.println(response.getStatusLine()); System.out.println("----------------------------------------\nResponse:"); System.out.println(EntityUtils.toString(response.getEntity(), StandardCharsets .UTF_8)); } } }

static class MyHTTPConnectionSocketFactory extends PlainConnectionSocketFactory { @Override public Socket createSocket(final HttpContext context) throws IOException { InetSocketAddress socksaddr = (InetSocketAddress) context.getAttribute(SOCKSPROXYADDRESS); if(socksaddr!=null) { Socket rv = null; if (socksaddr.isUnresolved()) { rv = super.createSocket(context); } else { Proxy proxy = new Proxy(Proxy.Type.SOCKS, socksaddr); rv = new Socket(proxy); } return rv; } else { socksaddr = (InetSocketAddress) context.getAttribute(HTTPPROXYADDRESS); if (socksaddr != null) { Socket rv = null; if (socksaddr.isUnresolved()) { rv = super.createSocket(context); } else { Proxy proxy = new Proxy(Proxy.Type.HTTP, socksaddr); rv = new Socket(proxy); } return rv; } else throw new java.io.IOException("no "+SOCKSPROXYADDRESS + " nor "+HTTPPROXYADDRESS+" found in context!"); } } }

static class MyHTTPSConnectionSocketFactory extends SSLConnectionSocketFactory { public MyHTTPSConnectionSocketFactory(final SSLContext sslContext) { super(sslContext); }

@Override public Socket createSocket(final HttpContext context) throws IOException { InetSocketAddress socksaddr = (InetSocketAddress) context.getAttribute(SOCKSPROXYADDRESS); if(socksaddr!=null) { Socket rv = null; if (socksaddr.isUnresolved()) { rv = super.createSocket(context); } else { Proxy proxy = new Proxy(Proxy.Type.SOCKS, socksaddr); rv = new Socket(proxy); } return rv; } else { socksaddr = (InetSocketAddress) context.getAttribute(HTTPPROXYADDRESS); if (socksaddr != null) { Socket rv = null; if (socksaddr.isUnresolved()) { rv = super.createSocket(context); } else { Proxy proxy = new Proxy(Proxy.Type.HTTP, socksaddr); rv = new Socket(proxy); } return rv; } else throw new java.io.IOException("no "+SOCKSPROXYADDRESS + " nor "+HTTPPROXYADDRESS+" found in context!"); } } } }

Artikel, die hierher verlinken

Mealie im Docker-Zoo

30.04.2023

Es ist wieder mal ein neuer Container in meinem Docker-Zoo eingezogen

PairDrop im Docker-Zoo

15.04.2023

Es ist wieder mal ein neuer Container in meinem Docker-Zoo eingezogen

Alle Artikel rss Wochenübersicht Monatsübersicht Github Repositories Gitlab Repositories Mastodon Über mich home xmpp


Vor 5 Jahren hier im Blog

  • Certstream, InfluxDB, Grafana und Netflix

    16.04.2019

    Nachdem ich vor kurzem über mein erstes Spielen mit dem certstream berichtete, habe ich weitere Experimente gemacht und die Daten zur besseren Auswertung in eine InfluxDB gepackt, um sie mit Grafana untersuchen zu können.

    Weiterlesen...

Neueste Artikel

  • Die sQLshell ist nun cloudnative!

    Die sQLshell hat eine weitere Integration erfahren - obwohl ich eigentlich selber nicht viel dazu tun musste: Es existiert ein Projekt/Produkt namens steampipe, dessen Slogan ist select * from cloud; - Im Prinzip eine Wrapperschicht um diverse (laut Eigenwerbung mehr als 140) (cloud) data sources.

    Weiterlesen...
  • LinkCollections 2024 III

    Nach der letzten losen Zusammenstellung (für mich) interessanter Links aus den Tiefen des Internet von 2024 folgt hier gleich die nächste:

    Weiterlesen...
  • Funktionen mit mehreren Rückgabewerten in Java

    Da ich seit nunmehr einem Jahr bei meinem neeun Arbeitgeber beschäftigt und damit seit ungefähr derselben Zeit für Geld mit Python arbeite, haben sich gewisse Antipathien gegenüber Python vertieft (ich kann mit typlosen Sprachen einfach nicht umgehen) - aber auch einige meiner Gründe, Python zu lieben sind ebenso stärker geworden. Einer davon ist der Fakt, dass eine Methode in Python mehr als einen Wert zurückgeben kann.

    Weiterlesen...

Manche nennen es Blog, manche Web-Seite - ich schreibe hier hin und wieder über meine Erlebnisse, Rückschläge und Erleuchtungen bei meinen Hobbies.

Wer daran teilhaben und eventuell sogar davon profitieren möchte, muß damit leben, daß ich hin und wieder kleine Ausflüge in Bereiche mache, die nichts mit IT, Administration oder Softwareentwicklung zu tun haben.

Ich wünsche allen Lesern viel Spaß und hin und wieder einen kleinen AHA!-Effekt...

PS: Meine öffentlichen GitHub-Repositories findet man hier - meine öffentlichen GitLab-Repositories finden sich dagegen hier.