#!/usr/bin/env python # -*- coding: UTF-8 -*- # Author : tintinweb@oststrom.com <github.com/tintinweb> """ An example implementation of a passive TLS security scanner with custom starttls support: TLSScanner() generates TLS probe traffic (optional) TLSInfo() passively evaluates the traffic and generates events/warning """ from __future__ import print_function import sys import concurrent.futures try: from scapy.all import get_if_list, sniff, IP, TCP except ImportError: from scapy import get_if_list, sniff, IP, TCP try: # This import works from the project directory from scapy_ssl_tls.ssl_tls import * import scapy_ssl_tls.ssl_tls_keystore as tlsk except ImportError as ie: # If you installed this package via pip, you just need to execute this from scapy.layers.ssl_tls import * import scapy.layers.ssl_tls_keystore as tlsk import socket from collections import namedtuple import time class TCPConnection(object): def __init__(self, target, starttls=None): last_exception = None self.target = target self._s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) for t in xrange(1, 4): try: self._s.connect(target) break except socket.error as se: print ("- connection retry %s: %s" % (t, repr(target))) last_exception = se if not self._s: raise last_exception if starttls: self.sendall(starttls.replace("\\r", "\r").replace("\\n", "\n")) self.recvall(timeout=2) def sendall(self, pkt, timeout=None): if timeout: self._s.settimeout(timeout) self._s.sendall(str(pkt)) def recvall(self, size=8192 * 4, timeout=None): resp = [] if timeout: self._s.settimeout(timeout) while True: try: data = self._s.recv(size) if not data: break resp.append(data) except socket.timeout: break return SSL(''.join(resp)) class TLSInfo(object): # https://en.wikipedia.org/wiki/RSA_numbers RSA_MODULI_KNOWN_FACTORED = (1522605027922533360535618378132637429718068114961380688657908494580122963258952897654000350692006139, # RSA-100 # RSA-110 35794234179725868774991807832568455403003778024228226193532908190484670252364677411513516111204504060317568667, # RSA-120 227010481295437363334259960947493668895875336466084780038173258247009162675779735389791151574049166747880487470296548479, # RSA-129 114381625757888867669235779976146612010218296721242362562561842935706935245733897830597123563958705058989075147599290026879543541, # RSA-130 1807082088687404805951656164405905566278102516769401349170127021450056662540244048387341127590812303371781887966563182013214880557, # RSA-140 21290246318258757547497882016271517497806703963277216278233383215381949984056495911366573853021918316783107387995317230889569230873441936471, # RSA-150 155089812478348440509606754370011861770654545830995430655466945774312632703463465954363335027577729025391453996787414027003501631772186840890795964683, # RSA-155 10941738641570527421809707322040357612003732945449205990913842131476349984288934784717997257891267332497625752899781833797076537244027146743531593354333897, # RSA-160 2152741102718889701896015201312825429257773588845675980170497676778133145218859135673011059773491059602497907111585214302079314665202840140619946994927570407753, # RSA-170 26062623684139844921529879266674432197085925380486406416164785191859999628542069361450283931914514618683512198164805919882053057222974116478065095809832377336510711545759, # RSA-576 188198812920607963838697239461650439807163563379417382700763356422988859715234665485319060606504743045317388011303396716199692321205734031879550656996221305168759307650257059, # RSA-180 191147927718986609689229466631454649812986246276667354864188503638807260703436799058776201365135161278134258296128109200046702912984568752800330221777752773957404540495707851421041, # RSA-190 1907556405060696491061450432646028861081179759533184460647975622318915025587184175754054976155121593293492260464152630093238509246603207417124726121580858185985938946945490481721756401423481, # RSA-640 3107418240490043721350750035888567930037346022842727545720161948823206440518081504556346829671723286782437916272838033415471073108501919548529007337724822783525742386454014691736602477652346609, # RSA-200 27997833911221327870829467638722601621070446786955428537560009929326128400107609345671052955360856061822351910951365788637105954482006576775098580557613579098734950144178863178946295187237869221823983, # RSA-210 245246644900278211976517663573088018467026787678332759743414451715061600830038587216952208399332071549103626827191679864079776723243005600592035631246561218465817904100131859299619933817012149335034875870551067, # R SA-704 74037563479561712828046796097429573142593188889231289084936232638972765034028266276891996419625117843995894330502127585370118968098286733173273108930900552505116877063299072396380786710086096962537934650563796359, # RSA-768 1230186684530117755130494958384962720772853569595334792197322452151726400507263657518745202199786469389956474942774063845925192557326303453731548268507917026122142913461670429214311602221240479274737794080665351419597459856902143413, ) def __init__(self): self.history = [] self.events = [] self.info = namedtuple("info", ['client', 'server']) self.info.client = namedtuple("client", ['versions', 'ciphers', 'compressions', 'preferred_ciphers', 'sessions_established', 'heartbeat', 'extensions']) self.info.client.versions = set([]) self.info.client.ciphers = set([]) self.info.client.compressions = set([]) self.info.client.preferred_ciphers = set([]) self.info.client.sessions_established = 0 self.info.client.heartbeat = None self.info.client.extensions = set([]) self.info.server = namedtuple("server", ['versions', 'ciphers', 'compressions', 'sessions_established', 'fallback_scsv', 'heartbeat', 'extensions']) self.info.server.versions = set([]) self.info.server.ciphers = set([]) self.info.server.compressions = set([]) self.info.server.sessions_established = 0 self.info.server.fallback_scsv = None self.info.server.heartbeat = None self.info.server.certificates = set([]) self.info.server.extensions = set([]) def __str__(self): return """<TLSInfo packets.processed: %s client.versions: %s client.ciphers: %s client.compressions: %s client.preferred_ciphers: %s client.sessions_established: %s client.heartbeat: %s server.versions: %s server.ciphers: %s server.compressions: %s server.sessions_established: %s server.fallback_scsv: %s server.heartbeat: %s server.certificates: %s > """ % (len(self.history), self.info.client.versions, self.info.client.ciphers, self.info.client.compressions, self.info.client.preferred_ciphers, self.info.client.sessions_established, self.info.client.heartbeat, self.info.server.versions, self.info.server.ciphers, self.info.server.compressions, self.info.server.sessions_established, self.info.server.fallback_scsv, self.info.server.heartbeat, repr(self.info.server.certificates)) def get_events(self): events = [] events.extend(self.events) for tlsinfo in (self.info.client, self.info.server): # test CRIME - compressions offered? tmp = tlsinfo.compressions.copy() if 0 in tmp: tmp.remove(0) if len(tmp): events.append(("CRIME - %s supports compression" % tlsinfo.__name__, tlsinfo.compressions)) # test RC4 cipher_namelist = [ TLS_CIPHER_SUITES.get( c, "SSLv2_%s" % SSLv2_CIPHER_SUITES.get( c, c)) for c in tlsinfo.ciphers] tmp = [ c for c in cipher_namelist if isinstance( c, basestring) and "SSLV2" in c.upper() and "EXP" in c.upper()] if tmp: events.append(("DROWN - SSLv2 with EXPORT ciphers enabled", tmp)) tmp = [c for c in cipher_namelist if isinstance(c, basestring) and "EXP" in c.upper()] if tmp: events.append(("CIPHERS - Export ciphers enabled", tmp)) tmp = [c for c in cipher_namelist if isinstance(c, basestring) and "RC4" in c.upper()] if tmp: events.append(("CIPHERS - RC4 ciphers enabled", tmp)) tmp = [c for c in cipher_namelist if isinstance(c, basestring) and "MD2" in c.upper()] if tmp: events.append(("CIPHERS - MD2 ciphers enabled", tmp)) tmp = [c for c in cipher_namelist if isinstance(c, basestring) and "MD4" in c.upper()] if tmp: events.append(("CIPHERS - MD4 ciphers enabled", tmp)) tmp = [c for c in cipher_namelist if isinstance(c, basestring) and "MD5" in c.upper()] if tmp: events.append(("CIPHERS - MD5 ciphers enabled", tmp)) tmp = [c for c in cipher_namelist if isinstance(c, basestring) and "RSA_EXP" in c.upper()] if tmp: # only check DHE EXPORT for now. we might want to add DH1024 here. events.append(("FREAK - server supports RSA_EXPORT cipher suites", tmp)) tmp = [ c for c in cipher_namelist if isinstance( c, basestring) and "DHE_" in c.upper() and "EXPORT_" in c.upper()] if tmp: # only check DHE EXPORT for now. we might want to add DH1024 here. events.append(("LOGJAM - server supports weak DH-Group (512) (DHE_*_EXPORT) cipher suites", tmp)) exts = [ext for ext in tlsinfo.extensions if ext.haslayer(TLSExtSignatureAlgorithms)] # obvious SLOTH check, does not detect impl. errors that allow md5 even though not announced. # makes only sense for client_hello for ext in exts: for alg in ext.algs: if alg in (TLSSignatureScheme.RSA_MD5, TLSSignatureScheme.RSA_PKCS1_SHA1, TLSSignatureScheme.ECDSA_MD5, TLSSignatureScheme.ECDSA_SECP256R1_SHA256, TLSSignatureScheme.DSA_MD5, TLSSignatureScheme.DSA_SHA1): events.append( ("SLOTH - %s announces capability of signature/hash algorithm: %s" % (tlsinfo.__name__, TLS_SIGNATURE_SCHEMES.get(alg)), TLS_SIGNATURE_SCHEMES.get(alg))) try: for certlist in tlsinfo.certificates: for cert in certlist.certificates: keystore = tlsk.RSAKeystore.from_der_certificate(str(cert.data)) pubkey = keystore.public pubkey_size = pubkey.size_in_bits() if pubkey_size < 2048: events.append( ("INSUFFICIENT SERVER CERT PUBKEY SIZE - 2048 >= %d bits" % pubkey_size, cert)) if pubkey_size % 2048 != 0: events.append( ("SUSPICIOUS SERVER CERT PUBKEY SIZE - %d not a multiple of 2048 bits" % pubkey_size, cert)) if pubkey.n in self.RSA_MODULI_KNOWN_FACTORED: events.append( ("SERVER CERT PUBKEY FACTORED - trivial private_key recovery possible due to known factors n = p x q. See https://en.wikipedia.org/wiki/RSA_numbers | grep %s" % pubkey.n, cert)) except AttributeError: pass # tlsinfo.client has no attribute certificates if TLSVersion.SSL_2_0 in tlsinfo.versions: events.append(("PROTOCOL VERSION - SSLv2 supported ", tlsinfo.versions)) if TLSVersion.SSL_3_0 in tlsinfo.versions: events.append(("PROTOCOL VERSION - SSLv3 supported ", tlsinfo.versions)) if TLSHeartbeatMode.PEER_ALLOWED_TO_SEND == tlsinfo.heartbeat: events.append(("HEARTBEAT - enabled (non conclusive heartbleed) ", tlsinfo.versions)) if self.info.server.fallback_scsv: events.append( ("DOWNGRADE / POODLE - FALLBACK_SCSV honored (alert.inappropriate_fallback seen)", self.info.server.fallback_scsv)) return events def insert(self, pkt, client=None): self._process(pkt, client=client) def _process(self, pkt, client=None): if pkt is None: return if not pkt.haslayer(SSL) and not (pkt.haslayer(TLSRecord) or pkt.haslayer(SSLv2Record)): return if pkt.haslayer(SSL): records = pkt[SSL].records else: records = [pkt] for record in records: if client or record.haslayer(TLSClientHello) or record.haslayer(SSLv2ClientHello): tlsinfo = self.info.client elif not client or record.haslayer(TLSServerHello) or record.haslayer(SSLv2ServerHello): tlsinfo = self.info.server if not pkt.haslayer(TLSAlert) and pkt.haslayer(TLSRecord): tlsinfo.versions.add(pkt[TLSRecord].version) elif not pkt.haslayer(TLSAlert) and pkt.haslayer(SSLv2Record): tlsinfo.versions.add(TLSVersion.SSL_2_0) if record.haslayer(TLSClientHello): tlsinfo.ciphers.update(record[TLSClientHello].cipher_suites) tlsinfo.compressions.update(record[TLSClientHello].compression_methods) if record[TLSClientHello].cipher_suites: tlsinfo.preferred_ciphers.add(pkt[TLSClientHello].cipher_suites[0]) tlsinfo.extensions.update(record[TLSClientHello].extensions) elif record.haslayer(SSLv2ClientHello): tlsinfo.ciphers.add(record[SSLv2ClientHello].cipher_suites) if record.haslayer(TLSServerHello): tlsinfo.ciphers.add(record[TLSServerHello].cipher_suite) tlsinfo.compressions.add(record[TLSServerHello].compression_method) if record.haslayer(TLSExtHeartbeat): tlsinfo.heartbeat = record[TLSExtHeartbeat].mode tlsinfo.extensions.update(record[TLSServerHello].extensions) elif record.haslayer(SSLv2ServerHello): tlsinfo.ciphers.update(record[SSLv2ServerHello].cipher_suites) if record.haslayer(TLSCertificateList): tlsinfo.certificates.add(record[TLSCertificateList]) if record.haslayer(TLSFinished): tlsinfo.session.established += 1 if record.haslayer(TLSHandshake): tlsinfo.versions.add(pkt[TLSRecord].version) elif record.haslayer(SSLv2ServerHello): tlsinfo.versions.add(pkt[SSLv2Record].version) if not client and record.haslayer( TLSAlert) and record[TLSAlert].description == TLSAlertDescription.INAPPROPRIATE_FALLBACK: tlsinfo.fallback_scsv = True # track packet self.history.append(pkt) class TLSScanner(object): def __init__(self, workers=10): self.workers = workers self.capabilities = TLSInfo() def scan(self, target, starttls=None): for scan_method in (f for f in dir(self) if f.startswith("_scan_")): print ("=> %s" % (scan_method.replace("_scan_", ""))) getattr(self, scan_method)(target, starttls=starttls) def sniff(self, target=None, iface=None): def _process(pkt): match_ip = pkt.haslayer(IP) and (pkt[IP].src == target[0] or pkt[IP].dst == target[0]) if target else True match_port = pkt.haslayer(TCP) and ( pkt[TCP].sport == target[1] or pkt[TCP].dport == target[1]) if len(target) == 2 else True if match_ip and match_port: self.capabilities.insert(pkt, client=False) events = self.capabilities.get_events() # misuse get_events :/ if events: strconn = {'src': None, 'dst': None, 'sport': None, 'dport': None} if pkt.haslayer(IP): strconn['src'] = pkt[IP].src strconn['dst'] = pkt[IP].dst if pkt.haslayer(TCP): strconn['sport'] = pkt[TCP].sport strconn['dport'] = pkt[TCP].dport print ("Connection: %(src)s:%(sport)d <==> %(dst)s:%(dport)d" % strconn) print ("* EVENT - " + "\n* EVENT - ".join(e[0] for e in events)) return if iface: conf.iface = iface while True: bpf = None if len(target): bpf = "host %s" % target[0] if len(target) == 2: bpf += " and tcp port %d" % target[1] sniff(filter=bpf, prn=_process, store=0, timeout=3) def _scan_poodle2(self, target, starttls=None, version=TLSVersion.TLS_1_0): """taken from poodle2_padding_check""" def modify_padding(crypto_container): padding = crypto_container.padding crypto_container.padding = "\xff%s" % padding[1:] return crypto_container try: t = TCPConnection(target, starttls=starttls) ts = TLSSocket(t._s, client=True) tls_do_handshake(ts, version, TLSCipherSuite.RSA_WITH_AES_128_CBC_SHA) ts.pre_encrypt_hook = modify_padding ts.sendall( TLSPlaintext( data="GET / HTTP/1.1\r\nHOST: %s\r\n\r\n" % target[0]),) r = ts.recvall() if len(r.records) == 0: self.capabilities.events.append( ("Poodle2 - not vulnerable, but implementation does not send a BAD_RECORD_MAC alert", r)) elif r.haslayer(TLSAlert) and r[TLSAlert].description == TLSAlertDescription.BAD_RECORD_MAC: # not vulnerable pass else: self.capabilities.events.append(("Poodle2 - vulnerable", r)) except (socket.error, NotImplementedError) as se: print (repr(se)) return None def _scan_compressions(self, target, starttls=None, compression_list=TLS_COMPRESSION_METHODS.keys()): for comp in compression_list: # prepare pkt pkt = TLSRecord() / \ TLSHandshakes(handshakes=[TLSHandshake() / TLSClientHello(version=TLSVersion.TLS_1_1, cipher_suites=list(range(0xfe))[::-1], compression_methods=comp)]) # connect try: t = TCPConnection(target, starttls=starttls) t.sendall(pkt) resp = t.recvall(timeout=0.5) self.capabilities.insert(resp, client=False) except socket.error as se: print (repr(se)) def _check_cipher(self, target, cipher_id, starttls=None, version=TLSVersion.TLS_1_0): pkt = TLSRecord(version=version) / \ TLSHandshakes(handshakes=[TLSHandshake() / TLSClientHello(version=version, cipher_suites=[cipher_id])]) try: t = TCPConnection(target, starttls=starttls) t.sendall(pkt) resp = t.recvall(timeout=0.5) except socket.error as se: print (repr(se)) return None return resp def _scan_accepted_ciphersuites( self, target, starttls=None, cipherlist=TLS_CIPHER_SUITES.keys(), version=TLSVersion.TLS_1_0): with concurrent.futures.ThreadPoolExecutor(max_workers=self.workers) as executor: tasks = [ executor.submit( self._check_cipher, target, cipher_id, starttls, version) for cipher_id in cipherlist] for future in concurrent.futures.as_completed(tasks): self.capabilities.insert(future.result(), client=False) def _scan_supported_protocol_versions( self, target, starttls=None, versionlist=( (k, v) for k, v in TLS_VERSIONS.iteritems() if v.startswith("TLS_") or v.startswith("SSL_"))): for magic, name in versionlist: pkt = TLSRecord(version=magic) / \ TLSHandshakes(handshakes=[TLSHandshake() / TLSClientHello(version=magic, cipher_suites=list(range(0xfe))[::-1], extensions=[TLSExtension() / TLSExtHeartbeat(mode=TLSHeartbeatMode.PEER_ALLOWED_TO_SEND)])]) try: # connect t = TCPConnection(target, starttls=starttls) t.sendall(pkt) resp = t.recvall(timeout=0.5) self.capabilities.insert(resp, client=False) except socket.error as se: print (repr(se)) def _check_cipher_sslv2(self, target, cipher_id, starttls=None, version=TLSVersion.SSL_2_0): pkt = SSLv2Record() / SSLv2ClientHello(cipher_suites=[cipher_id], challenge='A' * 16, session_id='') try: t = TCPConnection(target, starttls=starttls) t.sendall(pkt) resp = t.recvall(timeout=0.5) except socket.error as se: print (repr(se)) return None return resp def _scan_accepted_ciphersuites_ssl2( self, target, starttls=None, cipherlist=SSLv2_CIPHER_SUITES.keys(), version=TLSVersion.SSL_2_0): with concurrent.futures.ThreadPoolExecutor(max_workers=self.workers) as executor: tasks = [ executor.submit( self._check_cipher_sslv2, target, cipher_id, starttls, version) for cipher_id in cipherlist] for future in concurrent.futures.as_completed(tasks): self.capabilities.insert(future.result(), client=False) def _scan_scsv(self, target, starttls=None): pkt = TLSRecord(version=TLSVersion.TLS_1_1) / \ TLSHandshakes(handshakes=[TLSHandshake() / TLSClientHello(version=TLSVersion.TLS_1_0, cipher_suites=[TLSCipherSuite.FALLBACK_SCSV] + list(range(0xfe))[::-1])]) # connect try: t = TCPConnection(target, starttls=starttls) t.sendall(pkt) resp = t.recvall(timeout=2) self.capabilities.insert(resp, client=False) if not (resp.haslayer(TLSAlert) and resp[TLSAlert].description == TLSAlertDescription.INAPPROPRIATE_FALLBACK): self.capabilities.events.append(("DOWNGRADE / POODLE - FALLBACK_SCSV - not honored", resp)) except socket.error as se: print (repr(se)) def _scan_heartbleed(self, target, starttls=None, version=TLSVersion.TLS_1_0, payload_length=20): try: t = TCPConnection(target, starttls=starttls) pkt = TLSRecord(version=version) / TLSHandshakes(handshakes=[TLSHandshake() / TLSClientHello(version=version)]) t.sendall(pkt) resp = t.recvall(timeout=0.5) pkt = TLSRecord(version=version) / TLSHeartBeat(length=2**14 - 1, data='bleed...') t.sendall(str(pkt)) resp = t.recvall(timeout=0.5) if resp.haslayer(TLSHeartBeat) and resp[TLSHeartBeat].length > 8: self.capabilities.events.append(("HEARTBLEED - vulnerable", resp)) except socket.error as se: print (repr(se)) return None return resp def _scan_secure_renegotiation(self, target, starttls=None, version=TLSVersion.TLS_1_0, payload_length=20): # todo: also test EMPTY_RENEGOTIATION_INFO_SCSV try: t = TCPConnection(target, starttls=starttls) pkt = TLSRecord(version=version) / \ TLSHandshakes(handshakes=[TLSHandshake() / TLSClientHello(version=version, extensions=TLSExtension() / TLSExtRenegotiationInfo())]) t.sendall(pkt) resp = t.recvall(timeout=0.5) if resp.haslayer(TLSExtRenegotiationInfo): self.capabilities.events.append(("TLS EXTENSION SECURE RENEGOTIATION - not supported", resp)) except socket.error as se: print (repr(se)) return None return resp def main(): print (__doc__) if len(sys.argv) <= 3: print ("USAGE: <mode> <host> <port> [starttls] [num_worker] [interface]") print (" mode ... client | sniff") print (" starttls ... starttls keyword e.g. 'starttls\\n' or 'ssl\\n'") print ("available interfaces") for i in get_if_list(): print (" * %s" % i) exit(1) mode = sys.argv[1] starttls = sys.argv[4] if len(sys.argv) > 4 else None host = sys.argv[2] port = int(sys.argv[3]) num_workers = 10 if not len(sys.argv) > 5 else int(sys.argv[5]) iface = "eth0" if not len(sys.argv) > 6 else sys.argv[6] scanner = TLSScanner(workers=num_workers) if mode == "sniff": print ("[*] [passive] Scanning in 'sniff' mode for %s on %s..." % (repr((host, port)), iface)) scanner.sniff((host, port), iface=iface) else: print ("[*] [active] Scanning with %s parallel threads..." % num_workers) t_start = time.time() scanner.scan((host, port), starttls=starttls) print ("\n") print ("[*] Capabilities (Debug)") print (scanner.capabilities) print ("[*] supported ciphers: %s/%s" % ( len(scanner.capabilities.info.server.ciphers), len(TLS_CIPHER_SUITES) + len(SSLv2_CIPHER_SUITES))) print (" * " + "\n * ".join( ("%s (0x%0.4x)" % (TLS_CIPHER_SUITES.get(c, "SSLv2_%s" % SSLv2_CIPHER_SUITES.get(c, c)), c) for c in scanner.capabilities.info.server.ciphers))) print ("") print ( "[*] supported protocol versions: %s/%s" % (len( scanner.capabilities.info.server.versions), len(TLS_VERSIONS))) print (" * " + "\n * ".join( ("%s (0x%0.4x)" % (TLS_VERSIONS.get(c, c), c) for c in scanner.capabilities.info.server.versions))) print ("") print ("[*] supported compressions methods: %s/%s" % ( len(scanner.capabilities.info.server.compressions), len(TLS_COMPRESSION_METHODS))) print (" * " + "\n * ".join(("%s (0x%0.4x)" % (TLS_COMPRESSION_METHODS.get(c, c), c) for c in scanner.capabilities.info.server.compressions))) print ("") events = scanner.capabilities.get_events() print ("[*] Events: %s" % len(events)) print ("* EVENT - " + "\n* EVENT - ".join(e[0] for e in events)) t_diff = time.time() - t_start print ("") print ("Scan took: %ss" % t_diff) if __name__ == "__main__": main()