class CompressionPlugin(plugin_base.PluginBase): interface = plugin_base.PluginInterface(title="CompressionPlugin", description="") interface.add_command( command="compression", help="Tests the server(s) for Zlib compression support.") def process_task(self, server_info, command, options_dict=None): ssl_connection = server_info.get_preconfigured_ssl_connection() # Make sure OpenSSL was built with support for compression to avoid false negatives if 'zlib compression' not in ssl_connection.get_available_compression_methods( ): raise RuntimeError( 'OpenSSL was not built with support for zlib / compression. ' 'Did you build nassl yourself ?') try: # Perform the SSL handshake ssl_connection.connect() compression_name = ssl_connection.get_current_compression_method() except ClientCertificateRequested: # The server asked for a client cert compression_name = ssl_connection.get_current_compression_method() finally: ssl_connection.close() return CompressionResult(server_info, command, options_dict, compression_name)
class ShellshockTesterPlugin(plugin_base.PluginBase): """class ShellshockTesterPlugin This class inherited from abstract class PluginBase. Instance of this class tests server for vulnerability CVE-2014-6271 and makes decision if the server is vulnerable. """ interface = plugin_base.PluginInterface( "ShellshockTesterPlugin", "Tests the server(s) for Shellshock vulnerability (CVE-2014-6271)") interface.add_command(command="shellshock", help="Tests server for vulnerability CVE-2014-6271") interface.add_option( option="http_path", help= "Specify path which following the domain name or ip address. Default is set to /", dest="path") def process_task(self, server_connectivity_info, plugin_command, option_dict=None): if option_dict and 'path' in option_dict.keys(): path = str(option_dict['path']) else: path = '/' if server_connectivity_info.port == 80: conn = httplib.HTTPConnection(server_connectivity_info.ip_address, server_connectivity_info.port) elif server_connectivity_info.port == 443: conn = httplib.HTTPSConnection( server_connectivity_info.ip_address, server_connectivity_info.port, context=ssl._create_unverified_context()) else: raise ValueError( "ShellshockTesterPlugin: Can\'t make test for this port {0}". format(server_connectivity_info.port)) try: conn.connect() except Exception as e: raise ValueError( "ShellshockTesterPlugin: Connection error for port {0}. {1}". format(server_connectivity_info.port, str(e))) else: conn.request("GET", path, "", { 'User-Agent': '() { :; }; echo; echo Vulnerable to CVE-2014-6271' }) response = conn.getresponse() return ShellshockTesterResult(server_connectivity_info, plugin_command, option_dict, self.is_vulnerable(response)) def is_vulnerable(self, message): """Returns true if value in parameter message contains string 'Vulnerable to CVE-2014-6271', otherwise returns false """ msg = message.read() return 'Vulnerable to CVE-2014-6271' in msg
class HttpHeadersPlugin(plugin_base.PluginBase): interface = plugin_base.PluginInterface(title="HttpHeadersPlugin", description='') interface.add_command( command="http_headers", help= "Checks for the HTTP Strict Transport Security (HSTS) and HTTP Public Key Pinning (HPKP) HTTP headers " "within the response sent back by the server(s). Also computes the HPKP pins for the server(s)' current " "certificate chain.") def process_task(self, server_info, command, options_dict=None): if server_info.tls_wrapped_protocol not in [ TlsWrappedProtocolEnum.PLAIN_TLS, TlsWrappedProtocolEnum.HTTPS ]: raise ValueError( 'Cannot test for HTTP headers on a StartTLS connection.') hsts_header, hpkp_header, hpkp_report_only, certificate_chain = self._get_security_headers( server_info) return HttpHeadersResult(server_info, command, options_dict, hsts_header, hpkp_header, hpkp_report_only, certificate_chain) @classmethod def _get_security_headers(cls, server_info): hpkp_report_only = False # Perform the SSL handshake ssl_connection = server_info.get_preconfigured_ssl_connection() ssl_connection.connect() certificate_chain = ssl_connection.get_peer_cert_chain() # Send an HTTP GET request to the server ssl_connection.write( HttpRequestGenerator.get_request(host=server_info.hostname)) http_resp = HttpResponseParser.parse(ssl_connection) ssl_connection.close() if http_resp.version == 9: # HTTP 0.9 => Probably not an HTTP response raise ValueError('Server did not return an HTTP response') else: hsts_header = http_resp.getheader('strict-transport-security', None) hpkp_header = http_resp.getheader('public-key-pins', None) if hpkp_header is None: hpkp_report_only = True hpkp_header = http_resp.getheader( 'public-key-pins-report-only', None) # We do not follow redirections because the security headers must be set on the first page according to # https://hstspreload.appspot.com/: # "If you are serving an additional redirect from your HTTPS site, that redirect must still have the HSTS # header (rather than the page it redirects to)." return hsts_header, hpkp_header, hpkp_report_only, certificate_chain
class CRIMEVulnerabilityTesterPlugin(plugin_base.PluginBase): """class CRIMEVulnerabilityTesterPlugin This class inherited from abstract class PluginBase. Instance of this class tests server for vulnerability CVE-2012-4929 and makes decision if the server is vulnerable. """ interface = plugin_base.PluginInterface( "CRIMEVulnerabilityTesterPlugin", "Scans the server(s) and checks if requirements for CRIME attack are satisfied." ) interface.add_command(command="crime", help="Tests server for CVE-2012-4929 vulnerability.") VERBOSE_LINE = ' Compression for {protocol:<10}{status:<20}'.format def process_task(self, server_connectivity_info, plugin_command, options_dict=None): if options_dict and 'verbose' in options_dict.keys(): verbose_mode = options_dict['verbose'] else: verbose_mode = False is_vulnerable = False protocols = {'SSLv3', 'SSLv2', 'TLSv1', 'TLSv1.1', 'TLSv1.2'} if verbose_mode: print ' VERBOSE MODE PRINT' print ' ------------------' for proto in protocols: ssl_connection = server_connectivity_info.get_preconfigured_ssl_connection( override_ssl_version=PROTOCOL_VERSION[proto]) try: ssl_connection.connect() compression = True if ssl_connection.get_current_compression_method( ) is not None else False is_vulnerable |= compression except SSLHandshakeRejected as e: if verbose_mode: print self.VERBOSE_LINE( protocol=proto, status='Connect error: {error}'.format(error=str(e))) pass else: if verbose_mode: print self.VERBOSE_LINE(protocol=proto, status=str(compression)) finally: ssl_connection.close() if verbose_mode: print ' ----------------------' print ' END VERBOSE MODE PRINT' print ' ----------------------' return CRIMEVulnerabilityTesterResult(server_connectivity_info, plugin_command, options_dict, is_vulnerable)
class FallbackScsvPlugin(plugin_base.PluginBase): interface = plugin_base.PluginInterface(title="FallbackScsvPlugin", description="") interface.add_command( command="fallback", help= "Checks support for the TLS_FALLBACK_SCSV cipher suite to prevent downgrade attacks." ) def process_task(self, server_info, plugin_command, plugin_options=None): if server_info.highest_ssl_version_supported <= SSLV3: raise ValueError( 'Server only supports SSLv3; no downgrade attacks are possible' ) # Try to connect using a lower TLS version with the fallback cipher suite enabled ssl_version_downgrade = server_info.highest_ssl_version_supported - 1 ssl_connection = server_info.get_preconfigured_ssl_connection( override_ssl_version=ssl_version_downgrade) ssl_connection.set_mode(SSL_MODE_SEND_FALLBACK_SCSV) supports_fallback_scsv = False try: # Perform the SSL handshake ssl_connection.connect() except _nassl.OpenSSLError as e: # This is the right, specific alert the server should return if 'tlsv1 alert inappropriate fallback' in str(e.args): supports_fallback_scsv = True else: raise except SSLHandshakeRejected: # If the handshake is rejected, we assume downgrade attacks are prevented (this is how F5 balancers do it) # although it could also be because the server does not support this version of TLS # https://github.com/nabla-c0d3/sslyze/issues/119 supports_fallback_scsv = True finally: ssl_connection.close() return FallbackScsvResult(server_info, plugin_command, plugin_options, supports_fallback_scsv)
class HeartbleedPlugin(plugin_base.PluginBase): interface = plugin_base.PluginInterface("HeartbleedPlugin", "") interface.add_command( command="heartbleed", help= "Tests the server(s) for the OpenSSL Heartbleed vulnerability (experimental)." ) def process_task(self, server_info, command, options_dict=None): ssl_connection = server_info.get_preconfigured_ssl_connection() ssl_connection.ssl_version = server_info.highest_ssl_version_supported # Needed by the heartbleed payload # Awful hack #1: replace nassl.sslClient.do_handshake() with a heartbleed # checking SSL handshake so that all the SSLyze options # (startTLS, proxy, etc.) still work ssl_connection.do_handshake = types.MethodType( do_handshake_with_heartbleed, ssl_connection) heartbleed = None try: # Perform the SSL handshake ssl_connection.connect() except HeartbleedSent: # Awful hack #2: directly read the underlying network socket heartbleed = ssl_connection._sock.recv(16381) finally: ssl_connection.close() # Text output is_vulnerable_to_heartbleed = False if heartbleed is None: raise ValueError("Error: connection failed.") elif '\x01\x01\x01\x01\x01\x01\x01\x01\x01' in heartbleed: # Server replied with our hearbeat payload is_vulnerable_to_heartbleed = True return HeartbleedResult(server_info, command, options_dict, is_vulnerable_to_heartbleed)
class FallbackScsvPlugin(plugin_base.PluginBase): interface = plugin_base.PluginInterface(title="FallbackScsvPlugin", description="") interface.add_command( command="fallback", help= "Checks support for the TLS_FALLBACK_SCSV cipher suite to prevent downgrade attacks." ) def process_task(self, server_info, plugin_command, plugin_options=None): if server_info.highest_ssl_version_supported <= SSLV3: raise ValueError( 'Server only supports SSLv3; no downgrade attacks are possible' ) # Try to connect using a lower TLS version with the fallback cipher suite enabled ssl_version_downgrade = server_info.highest_ssl_version_supported - 1 ssl_connection = server_info.get_preconfigured_ssl_connection( override_ssl_version=ssl_version_downgrade) ssl_connection.set_mode(SSL_MODE_SEND_FALLBACK_SCSV) supports_fallback_scsv = False try: # Perform the SSL handshake ssl_connection.connect() except _nassl.OpenSSLError as e: if 'tlsv1 alert inappropriate fallback' in str(e.args): supports_fallback_scsv = True else: raise finally: ssl_connection.close() return FallbackScsvResult(server_info, plugin_command, plugin_options, supports_fallback_scsv)
class CertificateInfoPlugin(plugin_base.PluginBase): interface = plugin_base.PluginInterface(title="CertificateInfoPlugin", description='') interface.add_command( command="certinfo_basic", help= "Verifies the validity of the server(s) certificate(s) against various trust stores, checks for support " "for OCSP stapling, and prints relevant fields of the certificate.") interface.add_command( command="certinfo_full", help= "Same as --certinfo_basic but also prints the full server certificate." ) interface.add_option( option="ca_file", help="Local Certificate Authority file (in PEM format), to verify the " "validity of the server(s) certificate(s) against.", dest="ca_file") def process_task(self, server_info, command, options_dict=None): if command == 'certinfo_basic': result_class = CertInfoBasicResult elif command == 'certinfo_full': result_class = CertInfoFullResult else: raise ValueError("PluginCertInfo: Unknown command.") final_trust_store_list = list(DEFAULT_TRUST_STORE_LIST) if options_dict and 'ca_file' in options_dict.keys(): final_trust_store_list.append( TrustStore(options_dict['ca_file'], 'Custom --ca_file', 'N/A')) thread_pool = ThreadPool() for trust_store in final_trust_store_list: # Try to connect with each trust store thread_pool.add_job( (self._get_certificate_chain, (server_info, trust_store))) # Start processing the jobs; one thread per trust thread_pool.start(len(final_trust_store_list)) # Store the results as they come certificate_chain = [] path_validation_result_list = [] path_validation_error_list = [] ocsp_response = None for (job, result) in thread_pool.get_result(): (_, (_, trust_store)) = job certificate_chain, validation_result, ocsp_response = result # Store the returned verify string for each trust store path_validation_result_list.append( PathValidationResult(trust_store, validation_result)) # Store thread pool errors last_exception = None for (job, exception) in thread_pool.get_error(): (_, (_, trust_store)) = job path_validation_error_list.append( PathValidationError(trust_store, exception)) last_exception = exception thread_pool.join() if len(path_validation_error_list) == len(final_trust_store_list): # All connections failed unexpectedly; raise an exception instead of returning a result raise RuntimeError( 'Could not connect to the server; last error: {}'.format( last_exception)) # All done return result_class(server_info, command, options_dict, certificate_chain, path_validation_result_list, path_validation_error_list, ocsp_response) def _get_certificate_chain(self, server_info, trust_store): """Connects to the target server and uses the supplied trust store to validate the server's certificate. Returns the server's certificate and OCSP response. """ ssl_connection = server_info.get_preconfigured_ssl_connection( ssl_verify_locations=trust_store.path) # Enable OCSP stapling ssl_connection.set_tlsext_status_ocsp() try: # Perform the SSL handshake ssl_connection.connect() ocsp_response = ssl_connection.get_tlsext_status_ocsp_resp() x509_cert_chain = ssl_connection.get_peer_cert_chain() (_, verify_str ) = ssl_connection.get_certificate_chain_verify_result() except ClientCertificateRequested: # The server asked for a client cert # We can get the server cert anyway ocsp_response = ssl_connection.get_tlsext_status_ocsp_resp() x509_cert_chain = ssl_connection.get_peer_cert_chain() (_, verify_str ) = ssl_connection.get_certificate_chain_verify_result() finally: ssl_connection.close() return x509_cert_chain, verify_str, ocsp_response
class HstsPlugin(plugin_base.PluginBase): interface = plugin_base.PluginInterface(title="HstsPlugin", description='') interface.add_command( command="hsts", help="Checks support for HTTP Strict Transport Security (HSTS) by collecting any Strict-Transport-Security " "field present in the HTTP response sent back by the server(s)." ) def process_task(self, server_info, command, options_dict=None): if server_info.tls_wrapped_protocol not in [TlsWrappedProtocolEnum.PLAIN_TLS, TlsWrappedProtocolEnum.HTTPS]: raise ValueError('Cannot test for HSTS on a StartTLS connection.') hsts_header = self._get_hsts_header(server_info) return HstsResult(server_info, command, options_dict, hsts_header) MAX_REDIRECT = 5 def _get_hsts_header(self, server_info): hsts_header = None nb_redirect = 0 http_get_format = 'GET {0} HTTP/1.0\r\nHost: {1}\r\n{2}Connection: close\r\n\r\n'.format http_path = '/' http_append = '' while nb_redirect < self.MAX_REDIRECT: # Always use a new connection as some servers always close the connection after sending back an HTTP # response ssl_connection = server_info.get_preconfigured_ssl_connection() # Perform the SSL handshake ssl_connection.connect() ssl_connection.write(http_get_format(http_path, server_info.hostname, http_append)) http_resp = parse_http_response(ssl_connection) ssl_connection.close() if http_resp.version == 9 : # HTTP 0.9 => Probably not an HTTP response raise ValueError('Server did not return an HTTP response') else: hsts_header = http_resp.getheader('strict-transport-security', None) # If there was no HSTS header, check if the server returned a redirection if hsts_header is None and 300 <= http_resp.status < 400: redirect_header = http_resp.getheader('Location', None) cookie_header = http_resp.getheader('Set-Cookie', None) if redirect_header is None: break o = urlparse(redirect_header) # Handle absolute redirection URL but only allow redirections to the same domain and port if o.hostname and o.hostname != server_info.hostname: break else: http_path = o.path if o.scheme == 'http': # We would have to use urllib for http: URLs break # Handle cookies if cookie_header: cookie = Cookie.SimpleCookie(cookie_header) if cookie: http_append = 'Cookie:' + cookie.output(attrs=[], header='', sep=';') + '\r\n' nb_redirect+=1 else: # If the server did not return a redirection just give up break return hsts_header
class OpenSslCipherSuitesPlugin(plugin_base.PluginBase): interface = plugin_base.PluginInterface( "OpenSslCipherSuitesPlugin", "Scans the server(s) for supported OpenSSL cipher suites.") interface.add_command( command="sslv2", help= "Lists the SSL 2.0 OpenSSL cipher suites supported by the server(s).", aggressive=False) interface.add_command( command="sslv3", help= "Lists the SSL 3.0 OpenSSL cipher suites supported by the server(s).", aggressive=True) interface.add_command( command="tlsv1", help= "Lists the TLS 1.0 OpenSSL cipher suites supported by the server(s).", aggressive=True) interface.add_command( command="tlsv1_1", help= "Lists the TLS 1.1 OpenSSL cipher suites supported by the server(s).", aggressive=True) interface.add_command( command="tlsv1_2", help= "Lists the TLS 1.2 OpenSSL cipher suites supported by the server(s).", aggressive=True) interface.add_option( option='http_get', help="Option - For each cipher suite, sends an HTTP GET request after " "completing the SSL handshake and returns the HTTP status code.") interface.add_option( option='hide_rejected_ciphers', help= "Option - Hides the (usually long) list of cipher suites that were rejected by the server(s)." ) MAX_THREADS = 15 SSL_VERSIONS_MAPPING = { 'sslv2': SSLV2, 'sslv3': SSLV3, 'tlsv1': TLSV1, 'tlsv1_1': TLSV1_1, 'tlsv1_2': TLSV1_2 } def process_task(self, server_connectivity_info, plugin_command, options_dict=None): ssl_version = self.SSL_VERSIONS_MAPPING[plugin_command] # Get the list of available cipher suites for the given ssl version ssl_client = SslClient(ssl_version=ssl_version) ssl_client.set_cipher_list('ALL:COMPLEMENTOFALL') cipher_list = ssl_client.get_cipher_list() # Scan for every available cipher suite thread_pool = ThreadPool() for cipher in cipher_list: thread_pool.add_job( (self._test_cipher_suite, (server_connectivity_info, ssl_version, cipher))) # Start processing the jobs; One thread per cipher thread_pool.start(nb_threads=min(len(cipher_list), self.MAX_THREADS)) accepted_cipher_list = [] rejected_cipher_list = [] errored_cipher_list = [] # Store the results as they come for completed_job in thread_pool.get_result(): (job, cipher_result) = completed_job if isinstance(cipher_result, AcceptedCipherSuite): accepted_cipher_list.append(cipher_result) elif isinstance(cipher_result, RejectedCipherSuite): rejected_cipher_list.append(cipher_result) elif isinstance(cipher_result, ErroredCipherSuite): errored_cipher_list.append(cipher_result) else: raise ValueError('Unexpected result') # Store thread pool errors; only something completely unexpected would trigger an error for failed_job in thread_pool.get_error(): (_, exception) = failed_job raise exception thread_pool.join() # Test for the cipher suite preference preferred_cipher = self._get_preferred_cipher_suite( server_connectivity_info, ssl_version, accepted_cipher_list) # Generate the results plugin_result = OpenSSLCipherSuitesResult(server_connectivity_info, plugin_command, options_dict, preferred_cipher, accepted_cipher_list, rejected_cipher_list, errored_cipher_list) return plugin_result @staticmethod def _test_cipher_suite(server_connectivity_info, ssl_version, openssl_cipher_name): """Initiates a SSL handshake with the server using the SSL version and the cipher suite specified. """ ssl_connection = server_connectivity_info.get_preconfigured_ssl_connection( override_ssl_version=ssl_version) ssl_connection.set_cipher_list(openssl_cipher_name) if len(ssl_connection.get_cipher_list()) != 1: raise ValueError( 'Passed an OpenSSL string for multiple cipher suites: "{}"'. format(openssl_cipher_name)) try: # Perform the SSL handshake ssl_connection.connect() cipher_result = AcceptedCipherSuite.from_ongoing_ssl_connection( ssl_connection, ssl_version) except SSLHandshakeRejected as e: cipher_result = RejectedCipherSuite(openssl_cipher_name, ssl_version, str(e)) except Exception as e: cipher_result = ErroredCipherSuite(openssl_cipher_name, ssl_version, e) finally: ssl_connection.close() return cipher_result def _get_preferred_cipher_suite(self, server_connectivity_info, ssl_version, accepted_cipher_list): """Try to detect the server's preferred cipher suite among all cipher suites supported by SSLyze. """ if len(accepted_cipher_list) < 2: return None first_cipher_string = ', '.join( [cipher.openssl_name for cipher in accepted_cipher_list]) # Swap the first two ciphers in the list to see if the server always picks the client's first cipher second_cipher_string = ', '.join([ accepted_cipher_list[1].openssl_name, accepted_cipher_list[0].openssl_name ] + [cipher.openssl_name for cipher in accepted_cipher_list[2:]]) first_cipher = self._get_selected_cipher_suite( server_connectivity_info, ssl_version, first_cipher_string) second_cipher = self._get_selected_cipher_suite( server_connectivity_info, ssl_version, second_cipher_string) if first_cipher.name == second_cipher.name: # The server has its own preference for picking a cipher suite return first_cipher else: # The server has no preferred cipher suite as it follows the client's preference for picking a cipher suite return None @staticmethod def _get_selected_cipher_suite(server_connectivity_info, ssl_version, openssl_cipher_string): """Given an OpenSSL cipher string (which may specify multiple cipher suites), return the cipher suite that was selected by the server during the SSL handshake. """ ssl_connection = server_connectivity_info.get_preconfigured_ssl_connection( override_ssl_version=ssl_version) ssl_connection.set_cipher_list(openssl_cipher_string) # Perform the SSL handshake ssl_connection.connect() selected_cipher = AcceptedCipherSuite.from_ongoing_ssl_connection( ssl_connection, ssl_version) ssl_connection.close() return selected_cipher
class SessionResumptionPlugin(plugin_base.PluginBase): interface = plugin_base.PluginInterface( title="SessionResumptionPlugin", description="Analyzes the target server's SSL session resumption capabilities." ) interface.add_command( command="resum", help="Tests the server(s) for session resumption support using session IDs and TLS session tickets (RFC 5077)." ) interface.add_command( command="resum_rate", help="Performs 100 session resumptions with the server(s), in order to estimate the session resumption rate.", aggressive=True ) MAX_THREADS_NB = 20 def process_task(self, server_info, command, options_dict=None): if command == 'resum': result = self._command_resum(server_info) elif command == 'resum_rate': successful_resumptions_nb, errored_resumptions_list = self._test_session_resumption_rate(server_info, 100) result = ResumptionRateResult(server_info, command, options_dict, 100, successful_resumptions_nb, errored_resumptions_list) else: raise ValueError("PluginSessionResumption: Unknown command.") return result def _command_resum(self, server_info): """Tests the server for session resumption support using session IDs and TLS session tickets (RFC 5077). """ # Test Session ID support successful_resumptions_nb, errored_resumptions_list = self._test_session_resumption_rate(server_info, 5) # Test TLS tickets support ticket_exception = None ticket_reason = None ticket_supported = False try: (ticket_supported, ticket_reason) = self._resume_with_session_ticket(server_info) except Exception as e: ticket_exception = e return ResumptionResult(server_info, 'resum', {}, 5, successful_resumptions_nb, errored_resumptions_list, ticket_supported, ticket_reason, ticket_exception) def _test_session_resumption_rate(self, server_info, resumption_attempts_nb): """Attempts several session ID resumption with the server.""" thread_pool = ThreadPool() for _ in xrange(resumption_attempts_nb): thread_pool.add_job((self._resume_with_session_id, (server_info, ))) thread_pool.start(nb_threads=min(resumption_attempts_nb, self.MAX_THREADS_NB)) # Count successful/failed resumptions successful_resumptions_nb = 0 for completed_job in thread_pool.get_result(): (job, was_resumption_successful) = completed_job if was_resumption_successful: successful_resumptions_nb += 1 # Count errors and store error messages errored_resumptions_list = [] for failed_job in thread_pool.get_error(): (job, exception) = failed_job error_msg = '{} - {}'.format(str(exception.__class__.__name__), str(exception)) errored_resumptions_list.append(error_msg) thread_pool.join() return successful_resumptions_nb, errored_resumptions_list def _resume_with_session_id(self, server_info): """Performs one session resumption using Session IDs. """ session1 = self._resume_ssl_session(server_info) try: # Recover the session ID session1_id = self._extract_session_id(session1) except IndexError: # Session ID not assigned return False if session1_id == '': # Session ID empty return False # Try to resume that SSL session session2 = self._resume_ssl_session(server_info, session1) try: # Recover the session ID session2_id = self._extract_session_id(session2) except IndexError: # Session ID not assigned return False # Finally, compare the two Session IDs if session1_id != session2_id: # Session ID assigned but not accepted return False return True def _resume_with_session_ticket(self, server_info): """Performs one session resumption using TLS Session Tickets. """ # Connect to the server and keep the SSL session session1 = self._resume_ssl_session(server_info, should_enable_tls_ticket=True) try: # Recover the TLS ticket session1_tls_ticket = self._extract_tls_session_ticket(session1) except IndexError: return False, 'TLS ticket not assigned' # Try to resume that session using the TLS ticket session2 = self._resume_ssl_session(server_info, session1, should_enable_tls_ticket=True) try: # Recover the TLS ticket session2_tls_ticket = self._extract_tls_session_ticket(session2) except IndexError: return False, 'TLS ticket not assigned' # Finally, compare the two TLS Tickets if session1_tls_ticket != session2_tls_ticket: return False, 'TLS ticket assigned but not accepted' return True, '' @staticmethod def _extract_session_id(ssl_session): """Extracts the SSL session ID from a SSL session object or raises IndexError if the session ID was not set. """ session_string = ( (ssl_session.as_text()).split("Session-ID:") )[1] session_id = ( session_string.split("Session-ID-ctx:") )[0].strip() return session_id @staticmethod def _extract_tls_session_ticket(ssl_session): """Extracts the TLS session ticket from a SSL session object or raises IndexError if the ticket was not set. """ session_string = ((ssl_session.as_text()).split("TLS session ticket:"))[1] session_tls_ticket = (session_string.split("Compression:"))[0] return session_tls_ticket def _resume_ssl_session(self, server_info, ssl_session=None, should_enable_tls_ticket=False): """Connects to the server and returns the session object that was assigned for that connection. If ssl_session is given, tries to resume that session. """ ssl_connection = server_info.get_preconfigured_ssl_connection() if not should_enable_tls_ticket: # Need to disable TLS tickets to test session IDs, according to rfc5077: # If a ticket is presented by the client, the server MUST NOT attempt # to use the Session ID in the ClientHello for stateful session resumption ssl_connection.set_options(SSL_OP_NO_TICKET) # Turning off TLS tickets. if ssl_session: ssl_connection.set_session(ssl_session) try: # Perform the SSL handshake ssl_connection.connect() new_session = ssl_connection.get_session() # Get session data finally: ssl_connection.close() return new_session
class OpenSslCcsInjectionPlugin(plugin_base.PluginBase): interface = plugin_base.PluginInterface("OpenSslCcsInjectionPlugin", "") interface.add_command( command="openssl_ccs", help= "Tests the server(s) for the OpenSSL CCS injection vulnerability (experimental)." ) def srecv(self): r = self._sock.recv(4096) self._inbuffer += r return r != '' def process_task(self, server_info, plugin_command, options_dict=None): ssl_connection = server_info.get_preconfigured_ssl_connection() self._ssl_version = server_info.highest_ssl_version_supported is_vulnerable = False self._inbuffer = "" ssl_connection.do_pre_handshake() # H4ck to directly send the CCS payload self._sock = ssl_connection._sock # Send hello and wait for server hello & cert serverhello, servercert = False, False self._sock.send(self.make_hello()) while not serverhello: # or not servercert try: if not self.srecv(): break except: break rs = self.parse_records() for record in rs: if record['type'] == 22: for p in record['proto']: if p['type'] == 2: serverhello = True if p['type'] == 11: servercert = True # Send the CCS if serverhello: # and servercert: is_vulnerable, stop = True, False self._sock.send(self.make_ccs()) while not stop: try: if not self.srecv(): break except socket.timeout: break except: is_vulnerable = False stop = True rs = self.parse_records() for record in rs: if record['type'] == 21: for p in record['proto']: if p['level'] == 2 or (p['level'] == 1 and p['desc'] == 0): is_vulnerable = False stop = True # If we receive no alert message check whether it is really is_vulnerable if is_vulnerable: self._sock.send('\x15' + self.ssl_tokens[self._ssl_version] + '\x00\x02\x01\x00') try: if not self.srecv(): is_vulnerable = False except: is_vulnerable = False self._sock.close() return OpenSslCcsInjectionResult(server_info, plugin_command, options_dict, is_vulnerable) ssl_tokens = { SSLV3: "\x03\x00", TLSV1: "\x03\x01", TLSV1_1: "\x03\x02", TLSV1_2: "\x03\x03", } ssl3_cipher = [ '\x00\x00', '\x00\x01', '\x00\x02', '\x00\x03', '\x00\x04', '\x00\x05', '\x00\x06', '\x00\x07', '\x00\x08', '\x00\x09', '\x00\x0a', '\x00\x0b', '\x00\x0c', '\x00\x0d', '\x00\x0e', '\x00\x0f', '\x00\x10', '\x00\x11', '\x00\x12', '\x00\x13', '\x00\x14', '\x00\x15', '\x00\x16', '\x00\x17', '\x00\x18', '\x00\x19', '\x00\x1a', '\x00\x1b', '\x00\x1c', '\x00\x1d', '\x00\x1e', '\x00\x1F', '\x00\x20', '\x00\x21', '\x00\x22', '\x00\x23', '\x00\x24', '\x00\x25', '\x00\x26', '\x00\x27', '\x00\x28', '\x00\x29', '\x00\x2A', '\x00\x2B', '\x00\x2C', '\x00\x2D', '\x00\x2E', '\x00\x2F', '\x00\x30', '\x00\x31', '\x00\x32', '\x00\x33', '\x00\x34', '\x00\x35', '\x00\x36', '\x00\x37', '\x00\x38', '\x00\x39', '\x00\x3A', '\x00\x3B', '\x00\x3C', '\x00\x3D', '\x00\x3E', '\x00\x3F', '\x00\x40', '\x00\x41', '\x00\x42', '\x00\x43', '\x00\x44', '\x00\x45', '\x00\x46', '\x00\x60', '\x00\x61', '\x00\x62', '\x00\x63', '\x00\x64', '\x00\x65', '\x00\x66', '\x00\x67', '\x00\x68', '\x00\x69', '\x00\x6A', '\x00\x6B', '\x00\x6C', '\x00\x6D', '\x00\x80', '\x00\x81', '\x00\x82', '\x00\x83', '\x00\x84', '\x00\x85', '\x00\x86', '\x00\x87', '\x00\x88', '\x00\x89', '\x00\x8A', '\x00\x8B', '\x00\x8C', '\x00\x8D', '\x00\x8E', '\x00\x8F', '\x00\x90', '\x00\x91', '\x00\x92', '\x00\x93', '\x00\x94', '\x00\x95', '\x00\x96', '\x00\x97', '\x00\x98', '\x00\x99', '\x00\x9A', '\x00\x9B', '\x00\x9C', '\x00\x9D', '\x00\x9E', '\x00\x9F', '\x00\xA0', '\x00\xA1', '\x00\xA2', '\x00\xA3', '\x00\xA4', '\x00\xA5', '\x00\xA6', '\x00\xA7', '\x00\xA8', '\x00\xA9', '\x00\xAA', '\x00\xAB', '\x00\xAC', '\x00\xAD', '\x00\xAE', '\x00\xAF', '\x00\xB0', '\x00\xB1', '\x00\xB2', '\x00\xB3', '\x00\xB4', '\x00\xB5', '\x00\xB6', '\x00\xB7', '\x00\xB8', '\x00\xB9', '\x00\xBA', '\x00\xBB', '\x00\xBC', '\x00\xBD', '\x00\xBE', '\x00\xBF', '\x00\xC0', '\x00\xC1', '\x00\xC2', '\x00\xC3', '\x00\xC4', '\x00\xC5', '\x00\x00', '\xc0\x01', '\xc0\x02', '\xc0\x03', '\xc0\x04', '\xc0\x05', '\xc0\x06', '\xc0\x07', '\xc0\x08', '\xc0\x09', '\xc0\x0a', '\xc0\x0b', '\xc0\x0c', '\xc0\x0d', '\xc0\x0e', '\xc0\x0f', '\xc0\x10', '\xc0\x11', '\xc0\x12', '\xc0\x13', '\xc0\x14', '\xc0\x15', '\xc0\x16', '\xc0\x17', '\xc0\x18', '\xc0\x19', '\xC0\x1A', '\xC0\x1B', '\xC0\x1C', '\xC0\x1D', '\xC0\x1E', '\xC0\x1F', '\xC0\x20', '\xC0\x21', '\xC0\x22', '\xC0\x23', '\xC0\x24', '\xC0\x25', '\xC0\x26', '\xC0\x27', '\xC0\x28', '\xC0\x29', '\xC0\x2A', '\xC0\x2B', '\xC0\x2C', '\xC0\x2D', '\xC0\x2E', '\xC0\x2F', '\xC0\x30', '\xC0\x31', '\xC0\x32', '\xC0\x33', '\xC0\x34', '\xC0\x35', '\xC0\x36', '\xC0\x37', '\xC0\x38', '\xC0\x39', '\xC0\x3A', '\xC0\x3B', '\xfe\xfe', '\xfe\xff', '\xff\xe0', '\xff\xe1' ] # Create a TLS record out of a protocol packet def make_record(self, t, body): l = struct.pack("!H", len(body)) return chr(t) + self.ssl_tokens[self._ssl_version] + l + body def make_hello(self): suites = "".join(self.ssl3_cipher) rand = "".join([chr(int(256 * random.random())) for x in range(32)]) l = struct.pack("!L", 39 + len(suites))[1:] # 3 bytes sl = struct.pack("!H", len(suites)) # Client hello, lenght and version # Random data + session ID + cipher suites + compression suites data = "\x01" + l + self.ssl_tokens[self._ssl_version] + rand + "\x00" data += sl + suites + "\x01\x00" return self.make_record(22, data) def make_ccs(self): ccsbody = "\x01" # Empty CCS return self.make_record(20, ccsbody) def parse_handshake_pkt(self, buf): r = [] while len(buf) >= 4: mt = ord(buf[0]) mlen = struct.unpack("!L", buf[0:4])[0] & 0xFFFFFF if mlen + 4 > len(buf): break r.append({"type": mt, "data": buf[4:4 + mlen]}) buf = buf[4 + mlen:] return r def parse_alert_pkt(self, buf): return [{"level": ord(buf[0]), "desc": ord(buf[1])}] def parse_records(self): r = [] # 5 byte header while len(self._inbuffer) >= 5: mtype = ord(self._inbuffer[0]) mtlsv = self._inbuffer[1:3] mlen = struct.unpack("!H", self._inbuffer[3:5])[0] if len(self._inbuffer) < 5 + mlen: break if mtype == 22: # Handshake protp = self.parse_handshake_pkt(self._inbuffer[5:5 + mlen]) elif mtype == 21: # Alert protp = self.parse_alert_pkt(self._inbuffer[5:5 + mlen]) else: protp = [] r.append({"type": mtype, "sslv": mtlsv, "proto": protp}) self._inbuffer = self._inbuffer[5 + mlen:] return r
class OpenSslCipherSuitesPlugin(plugin_base.PluginBase): interface = plugin_base.PluginInterface( "OpenSslCipherSuitesPlugin", "Scans the server(s) for supported OpenSSL cipher suites.") interface.add_command( command="sslv2", help= "Lists the SSL 2.0 OpenSSL cipher suites supported by the server(s).", aggressive=False) interface.add_command( command="sslv3", help= "Lists the SSL 3.0 OpenSSL cipher suites supported by the server(s).", aggressive=True) interface.add_command( command="tlsv1", help= "Lists the TLS 1.0 OpenSSL cipher suites supported by the server(s).", aggressive=True) interface.add_command( command="tlsv1_1", help= "Lists the TLS 1.1 OpenSSL cipher suites supported by the server(s).", aggressive=True) interface.add_command( command="tlsv1_2", help= "Lists the TLS 1.2 OpenSSL cipher suites supported by the server(s).", aggressive=True) interface.add_option( option='http_get', help="Option - For each cipher suite, sends an HTTP GET request after " "completing the SSL handshake and returns the HTTP status code.") interface.add_option( option='hide_rejected_ciphers', help= "Option - Hides the (usually long) list of cipher suites that were rejected by the server(s)." ) MAX_THREADS = 15 SSL_VERSIONS_MAPPING = { 'sslv2': SSLV2, 'sslv3': SSLV3, 'tlsv1': TLSV1, 'tlsv1_1': TLSV1_1, 'tlsv1_2': TLSV1_2 } def process_task(self, server_connectivity_info, plugin_command, options_dict=None): ssl_version = self.SSL_VERSIONS_MAPPING[plugin_command] # Get the list of available cipher suites for the given ssl version ssl_client = SslClient(ssl_version=ssl_version) ssl_client.set_cipher_list('ALL:COMPLEMENTOFALL') cipher_list = ssl_client.get_cipher_list() # Scan for every available cipher suite thread_pool = ThreadPool() for cipher in cipher_list: thread_pool.add_job( (self._test_ciphersuite, (server_connectivity_info, ssl_version, cipher))) # Scan for the preferred cipher suite preferred_cipher = self._get_preferred_ciphersuite( server_connectivity_info, ssl_version) # Start processing the jobs; One thread per cipher thread_pool.start(nb_threads=min(len(cipher_list), self.MAX_THREADS)) accepted_cipher_list = [] rejected_cipher_list = [] errored_cipher_list = [] # Store the results as they come for completed_job in thread_pool.get_result(): (job, cipher_result) = completed_job if isinstance(cipher_result, AcceptedCipherSuite): accepted_cipher_list.append(cipher_result) elif isinstance(cipher_result, RejectedCipherSuite): rejected_cipher_list.append(cipher_result) elif isinstance(cipher_result, ErroredCipherSuite): errored_cipher_list.append(cipher_result) else: raise ValueError('Unexpected result') # Store thread pool errors; only something completely unexpected would trigger an error for failed_job in thread_pool.get_error(): (_, exception) = failed_job raise exception thread_pool.join() plugin_result = OpenSSLCipherSuitesResult(server_connectivity_info, plugin_command, options_dict, preferred_cipher, accepted_cipher_list, rejected_cipher_list, errored_cipher_list) return plugin_result def _test_ciphersuite(self, server_connectivity_info, ssl_version, ssl_cipher): """Initiates a SSL handshake with the server, using the SSL version and cipher suite specified. """ rfc_cipher_name = OPENSSL_TO_RFC_NAMES_MAPPING[ssl_version].get( ssl_cipher, ssl_cipher) ssl_connection = server_connectivity_info.get_preconfigured_ssl_connection( override_ssl_version=ssl_version) ssl_connection.set_cipher_list(ssl_cipher) try: # Perform the SSL handshake ssl_connection.connect() except SSLHandshakeRejected as e: cipher_result = RejectedCipherSuite(rfc_cipher_name, str(e)) except Exception as e: cipher_result = ErroredCipherSuite(rfc_cipher_name, e) else: keysize = ssl_connection.get_current_cipher_bits() if 'ECDH' in ssl_cipher: dh_infos = ssl_connection.get_ecdh_param() elif 'DH' in ssl_cipher: dh_infos = ssl_connection.get_dh_param() else: dh_infos = None status_msg = ssl_connection.post_handshake_check() cipher_result = AcceptedCipherSuite(rfc_cipher_name, keysize, dh_infos, status_msg) finally: ssl_connection.close() return cipher_result def _get_preferred_ciphersuite(self, server_connectivity_info, ssl_version): """Initiates a SSL handshake with the server, using the SSL version and cipher suite specified. """ preferred_cipher = None # First try the default cipher list, and then all ciphers for cipher_list in [ SSLConnection.DEFAULT_SSL_CIPHER_LIST, 'ALL:COMPLEMENTOFALL' ]: ssl_connection = server_connectivity_info.get_preconfigured_ssl_connection( override_ssl_version=ssl_version) ssl_connection.set_cipher_list(cipher_list) try: # Perform the SSL handshake ssl_connection.connect() cipher_name = ssl_connection.get_current_cipher_name() rfc_cipher_name = OPENSSL_TO_RFC_NAMES_MAPPING[ ssl_version].get(cipher_name, cipher_name) keysize = ssl_connection.get_current_cipher_bits() if 'ECDH' in cipher_name: dh_infos = ssl_connection.get_ecdh_param() elif 'DH' in cipher_name: dh_infos = ssl_connection.get_dh_param() else: dh_infos = None status_msg = ssl_connection.post_handshake_check() preferred_cipher = AcceptedCipherSuite(rfc_cipher_name, keysize, dh_infos, status_msg) break except: pass finally: ssl_connection.close() return preferred_cipher
class LuckyThirteenVulnerabilityTesterPlugin(plugin_base.PluginBase): """@class LuckyThirteenVulnerabilityTesterPlugin This class inherited from abstract class PluginBase. Instance of this class tests server for vulnerability CVE-2013-0169 and makes decision if the server is vulnerable. """ interface = plugin_base.PluginInterface( "LuckyThirteenVulnerabilityTesterPlugin", "Scans the server(s) and checks if requirements for Lucky13 attack are satisfied." ) interface.add_command( command="lucky13_tls", help= "Tests server(s) for CVE-2013-0169 vulnerability. It uses only protocol TLSv1.1 and TLSv1.2." ) interface.add_command( command="lucky13_dtls", help= "Tests server(s) for CVE-2013-0169 vulnerability. It uses only protocol DTLSv1 and DTLSv1.2." ) interface.add_option(option="port", help="Specify which port you want connect to.", dest="port") MAX_THREADS = 20 DTLSv1_METHOD = 7 DTLSv1_2_METHOD = 8 DTLS_MODULE = { DTLSv1_METHOD: "DTLSv1_client_method", DTLSv1_2_METHOD: "DTLSv1_2_client_method" } def process_task(self, server_connectivity_info, plugin_command, option_dict=None): dtls_title = None if option_dict and 'verbose' in option_dict.keys(): verbose_mode = option_dict['verbose'] else: verbose_mode = False if plugin_command == 'lucky13_tls': (thread_pool, threads) = self.create_thread_pool_for_protocol_tls( server_connectivity_info) adv_info = {'protocol': 'TLS'} elif plugin_command == 'lucky13_dtls': if option_dict and 'port' in option_dict.keys(): (thread_pool, threads) = self.create_thread_pool_for_protocol_dtls( server_connectivity_info, int(option_dict['port'])) adv_info = {'protocol': 'DTLS', 'port': option_dict['port']} else: raise ValueError( "LuckyThirteenVulnerabilityTesterPlugin: Missing option --port for command --lucky13-dtls" ) else: raise ValueError( "LuckyThirteenVulnerabilityTesterPlugin: Unknown command") thread_pool.start(nb_threads=threads) accept_ciphers = [] reject_ciphers = [] if verbose_mode: print ' VERBOSE MODE PRINT' print ' ------------------' for completed_job in thread_pool.get_result(): (job, cipher_result) = completed_job if isinstance(cipher_result, AcceptCipher): accept_ciphers.append(cipher_result) elif isinstance(cipher_result, RejectCipher): reject_ciphers.append(cipher_result) else: raise ValueError("Unexpected result") if verbose_mode: cipher_result.print_cipher() if verbose_mode: print ' ----------------------' print ' END VERBOSE MODE PRINT' print ' ----------------------' for error_job in thread_pool.get_error(): (_, exception) = error_job raise exception thread_pool.join() support_vulnerable_ciphers = self.get_vulnerable_cipher_set( accept_ciphers) is_vulnerable = True \ if len(support_vulnerable_ciphers) > 0 \ else False return LuckyThirteenVulnerabilityTesterResult( server_connectivity_info, plugin_command, option_dict, is_vulnerable, support_vulnerable_ciphers, adv_info) def create_thread_pool_for_protocol_tls(self, server_connectivity_info): """ Creates and returns instance of ThreadPool class. Adds into ThreadPool new jobs for each cipher suite, which is available for protocol TLS 1.1 and TLS 1.2 Args: server_connectivity_info (ServerConnectivityInfo): contains information for connection on server. """ thread_pool = ThreadPool() protocols = ['TLSv1.1', 'TLSv1.2'] cipher_list = [] for protocol in protocols: if self.test_protocol_support(server_connectivity_info, protocol): cipher_list = self.get_cipher_list(protocol) for cipher in cipher_list: thread_pool.add_job( (self._test_ciphersuite, (server_connectivity_info, protocol, cipher))) return (thread_pool, min(self.MAX_THREADS, len(cipher))) def create_thread_pool_for_protocol_dtls(self, server_connectivity_info, port): """ Creates and returns instance of ThreadPool class. Adds into ThreadPool new jobs for each cipher suite, which is available for protocol DTLS 1.0 and/or DTLS 1.2 Args: server_connectivity_info (ServerConnectivityInfo): contains information for connection on server port (int): contains port number for connecting comunication. """ dtls_protocols = self.get_support_dtls_protocols_by_client() thread_pool = ThreadPool() cipher_list = [] for protocol in dtls_protocols: if self.test_dtls_protocol_support(server_connectivity_info, protocol, port): cipher_list = self.get_dtls_cipher_list(protocol) for cipher in cipher_list: thread_pool.add_job( (self._test_dtls_ciphersuite, (server_connectivity_info, protocol, cipher, port))) return (thread_pool, 1) def _test_ciphersuite(self, server_connectivity_info, ssl_version, cipher): """This function is used by threads to it investigates with support the cipher suite on server, when TLS protocol(s) is/are tested. Returns instance of class AcceptCipher or RejectCipher. Args: server_connectivity_info (ServerConnectivityInfo): contains information for connection on server ssl_version (str): contains SSL/TLS protocol version, which is used to connect cipher (str): contains OpenSSL shortcut for identification cipher suite. """ ssl_conn = server_connectivity_info.get_preconfigured_ssl_connection( override_ssl_version=PROTOCOL_VERSION[ssl_version]) ssl_conn.set_cipher_list(cipher) try: ssl_conn.connect() except SSLHandshakeRejected as e: cipher_result = RejectCipher( OPENSSL_TO_RFC_NAMES_MAPPING[ PROTOCOL_VERSION[ssl_version]].get(cipher, cipher), str(e)) else: cipher_result = AcceptCipher(OPENSSL_TO_RFC_NAMES_MAPPING[ PROTOCOL_VERSION[ssl_version]].get(cipher, cipher)) finally: ssl_conn.close() return cipher_result def _test_dtls_ciphersuite(self, server_connectivity_info, dtls_version, cipher, port): """This function is used by threads to it investigates with support the cipher suite on server, when DTLS protocol(s) is/are tested. Returns instance of class AcceptCipher or RejectCipher. Args: server_connectivity_info (ServerConnectivityInfo): contains information for connection on server dtls_version (str): contains SSL/TLS protocol version, which is used to connect cipher (str): contains OpenSSL shortcut for identification cipher suite port (int): contains port number for connecting comunication. """ cnx = SSL.Context(dtls_version) cnx.set_cipher_list(cipher) conn = SSL.Connection(cnx, socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) try: conn.connect((server_connectivity_info.ip_address, port)) conn.do_handshake() except SSL.Error as e: error_msg = ((e[0])[0])[2] cipher_result = RejectCipher( TLS_OPENSSL_TO_RFC_NAMES_MAPPING[cipher], error_msg) else: cipher_result = AcceptCipher( TLS_OPENSSL_TO_RFC_NAMES_MAPPING[cipher]) finally: conn.shutdown() conn.close() return cipher_result def test_dtls_protocol_support(self, server_connectivity_info, dtls_version, port): """Tests if DTLS protocols are supported by server. Returns true if server supports protocol otherwise returns false. Args: server_connectivity_info (ServerConnectivityInfo): contains information for connection on server dtls_protocol (str): contains version of DTLS protocol, which is supposed to be tested port (int): contains port number for connecting comunication. """ cnx = SSL.Context(dtls_version) cnx.set_cipher_list('ALL:COMPLEMENTOFALL') conn = SSL.Connection(cnx, socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) try: conn.connect((server_connectivity_info.ip_address, port)) conn.do_handshake() except SSL.SysCallError as ex: if ex[0] == 111: raise ValueError( 'LuckyThirteenVulnerabilityTesterPlugin: It is entered wrong port for DTLS connection.' ) else: support = False else: support = True finally: conn.shutdown() conn.close() return support def get_support_dtls_protocols_by_client(self): """Returns array which contains all DTLS protocols which are supported by client. """ dtls_ary = [] for dtls_version in self.DTLS_MODULE: try: SSL.Context._methods[dtls_version] = getattr( _lib, self.DTLS_MODULE[dtls_version]) except Exception as e: pass else: dtls_ary.append(dtls_version) return dtls_ary def test_protocol_support(self, server_connectivity_info, ssl_protocol): """Tests if SSL/TLS protocols are supported by server. Returns true if server supports protocol otherwise returns false. Args: server_connectivity_info (ServerConnectivityInfo): contains information for connection on server ssl_protocol (str): contains version of SSL/TLS protocol, which is supposed to be tested. """ if server_connectivity_info.highest_ssl_version_supported < PROTOCOL_VERSION[ ssl_protocol]: return False ssl_conn = server_connectivity_info.get_preconfigured_ssl_connection( override_ssl_version=PROTOCOL_VERSION[ssl_protocol]) protocol_supported = True try: ssl_conn.connect() except SSLHandshakeRejected: protocol_supported = False finally: ssl_conn.close() return protocol_supported def get_dtls_cipher_list(self, dtls_version): """Returns list of cipher suites available for protocol version, saves in parameter dtls_protocol. Args: dtls_protocol (str):. """ cnx = SSL.Context(dtls_version) cnx.set_cipher_list('ALL:COMPLEMENTOFALL') conn = SSL.Connection(cnx, socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) return conn.get_cipher_list() def get_cipher_list(self, ssl_protocol): """Returns list of cipher suites available for protocol version, saves in parameter ssl_protocol. Args: ssl_protocol (str):. """ ssl_client = SslClient(ssl_version=PROTOCOL_VERSION[ssl_protocol]) ssl_client.set_cipher_list('ALL:COMPLEMENTOFALL') return ssl_client.get_cipher_list() def get_vulnerable_cipher_set(self, accept_ciphers): """Returns set of cipher suites, which using CBC mode and are supported by server. Args: accept_ciphers(array): contains cipher suites, which are supported by server. """ result_set = Set() for cipher in accept_ciphers: if cipher.use_CBC_mode(): result_set = result_set.union([cipher._cipher_rfc_name]) return result_set
class BEASTVulnerabilityTesterPlugin(plugin_base.PluginBase): """class BEASTVulnerabilityTesterPlugin This class inherited from abstract class PluginBase. Instance of this class tests server for vulnerability CVE-2011-3389 and makes decision if the server is vulnerable. """ interface = plugin_base.PluginInterface( "BEASTVulnerabilityTesterPlugin", "Scans the server(s) and checks if requirements for BEAST attack are satisfied." ) interface.add_command(command="beast", help="Tests server for BEAST vulnerability.") MAX_THREADS = 15 def process_task(self, server_connectivity_info, plugin_command, options_dict=None): if options_dict and 'verbose' in options_dict.keys(): verbose_mode = options_dict['verbose'] else: verbose_mode = False test_protocols = {'SSLv3', 'TLSv1'} thread_pool = ThreadPool() ciphers_list = [] support_protocol_list = [] for proto in test_protocols: if self.test_protocol_support(PROTOCOL_VERSION[proto], server_connectivity_info): ssl_client = SslClient(ssl_version=PROTOCOL_VERSION[proto]) ssl_client.set_cipher_list(proto) ciphers_list = ssl_client.get_cipher_list() for cipher in ciphers_list: thread_pool.add_job((self._test_ciphersuite, (server_connectivity_info, PROTOCOL_VERSION[proto], cipher))) support_protocol_list.append(proto) thread_pool.start(nb_threads=min(len(ciphers_list), self.MAX_THREADS)) accept_ciphers = [] reject_ciphers = [] if verbose_mode: print ' VERBOSE MODE PRINT' print ' ------------------' for completed_job in thread_pool.get_result(): (job, cipher_result) = completed_job if isinstance(cipher_result, AcceptCipher): accept_ciphers.append(cipher_result) elif isinstance(cipher_result, RejectCipher): reject_ciphers.append(cipher_result) else: raise ValueError("Unexpected result") if verbose_mode: cipher_result.print_cipher() if verbose_mode: print ' ----------------------' print ' END VERBOSE MODE PRINT' print ' ----------------------' for error_job in thread_pool.get_error(): (_, exception) = error_job raise exception thread_pool.join() support_vulnerable_ciphers_set = self.get_vulnerable_ciphers( accept_ciphers) is_vulnerable = True \ if len(support_vulnerable_ciphers_set) > 0 \ else False return BEASTVulnerabilityTesterResult(server_connectivity_info, plugin_command, options_dict, support_vulnerable_ciphers_set, is_vulnerable, support_protocol_list) def _test_ciphersuite(self, server_connectivity_info, ssl_version, cipher): """This function is used by threads to it investigates with support the cipher suite on server. Returns instance of class AcceptCipher or RejectCipher. Args: server_connectivity_info (ServerConnectivityInfo): contains information for connection on server ssl_version (str): contains version of SSL/TLS protocol, uses to connect cipher (str): contains OpenSSL shortcut for identification cipher suite """ ssl_conn = server_connectivity_info.get_preconfigured_ssl_connection( override_ssl_version=ssl_version) ssl_conn.set_cipher_list(cipher) try: ssl_conn.connect() except SSLHandshakeRejected as e: cipher_result = RejectCipher( OPENSSL_TO_RFC_NAMES_MAPPING[ssl_version].get(cipher, cipher), str(e)) else: cipher_result = AcceptCipher( OPENSSL_TO_RFC_NAMES_MAPPING[ssl_version].get(cipher, cipher)) finally: ssl_conn.close() return cipher_result def test_protocol_support(self, ssl_version, server_info): """Tests if SSL/TLS protocol version in parameter ssl_version is supported by server. Returns true if server supports protocol version otherwise returns false. Args: ssl_version (str): server_info (ServerConnectivityInfo): contains information for connection on server """ ssl_conn = server_info.get_preconfigured_ssl_connection( override_ssl_version=ssl_version) try: ssl_conn.connect() except SSLHandshakeRejected: support = False else: support = True finally: ssl_conn.close() return support def get_vulnerable_ciphers(self, accept_ciphers): """Returns set of cipher suites, which using CBC mode and are supported by server. Args: accept_ciphers(array): contains cipher suites, which are supported by server """ result_set = Set() for cipher in accept_ciphers: if cipher.use_CBC_mode(): result_set = result_set.union([cipher._cipher_rfc_name]) return result_set
class POODLEVulnerabilityTesterPlugin(plugin_base.PluginBase): """class POODLEVulnerabilityTesterPlugin This class inherited from abstract class PluginBase. Instance of this class tests server for vulnerability CVE-2014-3566 and makes decision if the server is vulnerable. """ interface = plugin_base.PluginInterface( "POODLEVulnerabilityTesterPlugin", "Scans the server(s) and checks if requirements for POODLE attack are satisfied." ) interface.add_command( command="poodle", help="Tests server(s) for CVE-2014-3566 vulnerability.") MAX_THREADS = 15 def process_task(self, server_connectivity_info, plugin_command, option_dict=None): if option_dict and 'verbose' in option_dict.keys(): verbose_mode = option_dict['verbose'] else: verbose_mode = False ssl3_support = self.test_SSLv3_support(server_connectivity_info) support_vulnerable_ciphers = None if ssl3_support: cipher_list = self.get_ssl3_cipher_list() thread_pool = ThreadPool() for cipher in cipher_list: thread_pool.add_job((self._test_ciphersuite, (server_connectivity_info, cipher))) thread_pool.start( nb_threads=min(len(cipher_list), self.MAX_THREADS)) accept_ciphers = [] reject_ciphers = [] if verbose_mode: print ' VERBOSE MODE PRINT' print ' ------------------' for completed_job in thread_pool.get_result(): (job, cipher_result) = completed_job if isinstance(cipher_result, AcceptCipher): accept_ciphers.append(cipher_result) elif isinstance(cipher_result, RejectCipher): reject_ciphers.append(cipher_result) else: raise ValueError("Unexpected result") if verbose_mode: cipher_result.print_cipher() if verbose_mode: print ' ----------------------' print ' END VERBOSE MODE PRINT' print ' ----------------------' for error_job in thread_pool.get_error(): (_, exception) = error_job raise exception thread_pool.join() support_vulnerable_ciphers = self.get_vulnerable_ciphers( accept_ciphers) is_vulnerable = ssl3_support and ( support_vulnerable_ciphers is not None or len(support_vulnerable_ciphers) > 0) return POODLEVulnerabilityTesterResult(server_connectivity_info, plugin_command, option_dict, ssl3_support, is_vulnerable, support_vulnerable_ciphers) def _test_ciphersuite(self, server_connectivity_info, cipher): """This function is used by threads to it investigates with support the cipher suite on server. Returns instance of class AcceptCipher or RejectCipher. Args: server_connectivity_info (ServerConnectivityInfo): contains information for connection on server cipher (str): contains OpenSSL shortcut for identification cipher suite """ ssl_conn = server_connectivity_info.get_preconfigured_ssl_connection( override_ssl_version=SSLV3) ssl_conn.set_cipher_list(cipher) try: ssl_conn.connect() except SSLHandshakeRejected as e: cipher_result = RejectCipher( OPENSSL_TO_RFC_NAMES_MAPPING[SSLV3].get(cipher, cipher), str(e)) except Exception as e: cipher_result = RejectCipher( OPENSSL_TO_RFC_NAMES_MAPPING[SSLV3].get(cipher, cipher), str(e)) else: cipher_result = AcceptCipher( OPENSSL_TO_RFC_NAMES_MAPPING[SSLV3].get(cipher, cipher)) finally: ssl_conn.close() return cipher_result def get_ssl3_cipher_list(self): """Returns list of cipher suites available for protocol SSL 3.0 """ ssl_client = SslClient(ssl_version=SSLV3) ssl_client.set_cipher_list('SSLv3') return ssl_client.get_cipher_list() def test_SSLv3_support(self, server_connectivity_info): """Tests if protocol SSL 3.0 is supported by server. Returns true if server supports protocol SSL 3.0 otherwise returns false. Args: server_connectivity_info (ServerConnectivityInfo): contains information for connection on server """ if server_connectivity_info.highest_ssl_version_supported <= SSLV3: return True ssl_conn = server_connectivity_info.get_preconfigured_ssl_connection( override_ssl_version=SSLV3) try: ssl_conn.connect() except SSLHandshakeRejected as e: ssl3 = False else: ssl3 = True finally: ssl_conn.close() return ssl3 def get_vulnerable_ciphers(self, accept_ciphers): """Returns set of cipher suites, which using CBC mode and are supported by server. Args: accept_ciphers(array): contains cipher suites, which are supported by server """ result_set = [] for cipher in accept_ciphers: if cipher.use_CBC_mode: result_set.append([cipher._cipher_rfc_name]) return result_set
class SessionRenegotiationPlugin(plugin_base.PluginBase): interface = plugin_base.PluginInterface("SessionRenegotiationPlugin", "") interface.add_command( command="reneg", help= 'Tests the server(s) for client-initiated renegotiation and secure renegotiation support.' ) def process_task(self, server_info, command, options_dict=None): # Check for client-initiated renegotiation accepts_client_renegotiation = self._test_client_renegotiation( server_info) # Check for secure renegotiation supports_secure_renegotiation = self._test_secure_renegotiation( server_info) return SessionRenegotiationResult(server_info, command, options_dict, accepts_client_renegotiation, supports_secure_renegotiation) @staticmethod def _test_secure_renegotiation(server_info): """Checks whether the server supports secure renegotiation. """ ssl_connection = server_info.get_preconfigured_ssl_connection() try: # Perform the SSL handshake ssl_connection.connect() supports_secure_renegotiation = ssl_connection.get_secure_renegotiation_support( ) finally: ssl_connection.close() return supports_secure_renegotiation @staticmethod def _test_client_renegotiation(server_info): """Checks whether the server honors session renegotiation requests. """ ssl_connection = server_info.get_preconfigured_ssl_connection() try: # Perform the SSL handshake ssl_connection.connect() try: # Let's try to renegotiate ssl_connection.do_renegotiate() accepts_client_renegotiation = True # Errors caused by a server rejecting the renegotiation except socket.timeout: # This is how Netty rejects a renegotiation - https://github.com/nabla-c0d3/sslyze/issues/114 accepts_client_renegotiation = False except socket.error as e: if 'connection was forcibly closed' in str(e.args): accepts_client_renegotiation = False elif 'reset by peer' in str(e.args): accepts_client_renegotiation = False else: raise except OpenSSLError as e: if 'handshake failure' in str(e.args): accepts_client_renegotiation = False elif 'no renegotiation' in str(e.args): accepts_client_renegotiation = False elif 'tlsv1 unrecognized name' in str(e.args): # Yahoo's very own way of rejecting a renegotiation accepts_client_renegotiation = False else: raise # Should be last as socket errors are also IOError except IOError as e: if 'Nassl SSL handshake failed' in str(e.args): accepts_client_renegotiation = False else: raise finally: ssl_connection.close() return accepts_client_renegotiation
class HttpHeadersPlugin(plugin_base.PluginBase): interface = plugin_base.PluginInterface(title="HttpHeadersPlugin", description='') interface.add_command( command="http_headers", help= "Checks for the HTTP Strict Transport Security (HSTS) and HTTP Public Key Pinning (HPKP) HTTP headers " "within the response sent back by the server(s). Also computes the HPKP pins for the server(s)' current " "certificate chain.") def process_task(self, server_info, command, options_dict=None): if server_info.tls_wrapped_protocol not in [ TlsWrappedProtocolEnum.PLAIN_TLS, TlsWrappedProtocolEnum.HTTPS ]: raise ValueError( 'Cannot test for HTTP headers on a StartTLS connection.') hsts_header, hpkp_header, hpkp_report_only, certificate_chain = self._get_security_headers( server_info) return HttpHeadersResult(server_info, command, options_dict, hsts_header, hpkp_header, hpkp_report_only, certificate_chain) # Sample GET request with the Chrome for Windows 7 User Agent HTTP_GET_FORMAT = 'GET / HTTP/1.1\r\n' \ 'Host: {host}\r\n' \ 'User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36\r\n' \ 'Accept: */*' \ 'Connection: close\r\n\r\n' @classmethod def _get_security_headers(cls, server_info): hpkp_report_only = False # Perform the SSL handshake ssl_connection = server_info.get_preconfigured_ssl_connection() ssl_connection.connect() certificate_chain = ssl_connection.get_peer_cert_chain() # Send an HTTP GET request to the server ssl_connection.write( cls.HTTP_GET_FORMAT.format(host=server_info.hostname)) http_resp = parse_http_response(ssl_connection) ssl_connection.close() if http_resp.version == 9: # HTTP 0.9 => Probably not an HTTP response raise ValueError('Server did not return an HTTP response') else: hsts_header = http_resp.getheader('strict-transport-security', None) hpkp_header = http_resp.getheader('public-key-pins', None) if hpkp_header is None: hpkp_report_only = True hpkp_header = http_resp.getheader( 'public-key-pins-report-only', None) # We do not follow redirections because the security headers must be set on the first page according to # https://hstspreload.appspot.com/: # "If you are serving an additional redirect from your HTTPS site, that redirect must still have the HSTS # header (rather than the page it redirects to)." return hsts_header, hpkp_header, hpkp_report_only, certificate_chain