def test(self, mock_scan_commands): # Given a bunch of servers to scan all_scan_requests = [ServerScanRequestFactory.create() for _ in range(20)] # And all the servers will be reachable connectivity_result = ServerTlsProbingResultFactory.create() with mock.patch.object( _mass_connectivity_tester, "check_connectivity_to_server", return_value=connectivity_result ): # And given an observer to monitor scans observer = _MockScannerObserver() # When running the scans with the observer scanner = Scanner(observers=[observer]) scanner.queue_scans(all_scan_requests) assert scanner._has_started_work # It succeeds all_scan_results = [] for result in scanner.get_results(): all_scan_results.append(result) # And the right results were returned assert len(all_scan_results) == len(all_scan_requests) assert {result.scan_status for result in all_scan_results} == {ServerScanStatusEnum.COMPLETED} # And the observer was called appropriately assert observer.server_connectivity_test_error_calls_count == 0 assert observer.server_connectivity_test_completed_calls_count == len(all_scan_requests) assert observer.server_scan_completed_calls_count == len(all_scan_requests) assert observer.all_server_scans_completed_calls_count == 1
def basic_example() -> None: # Define the server that you want to scan server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("www.google.com", 443) # Do connectivity testing to ensure SSLyze is able to connect try: server_info = ServerConnectivityTester().perform(server_location) except ConnectionToServerFailed as e: # Could not connect to the server; abort print(f"Error connecting to {server_location}: {e.error_message}") return # Then queue some scan commands for the server scanner = Scanner() server_scan_req = ServerScanRequest( server_info=server_info, scan_commands={ScanCommand.CERTIFICATE_INFO, ScanCommand.SSL_2_0_CIPHER_SUITES}, ) scanner.start_scans([server_scan_req]) # Then retrieve the results for server_scan_result in scanner.get_results(): print(f"\nResults for {server_scan_result.server_info.server_location.hostname}:") # SSL 2.0 results ssl2_result = server_scan_result.scan_commands_results[ScanCommand.SSL_2_0_CIPHER_SUITES] print("\nAccepted cipher suites for SSL 2.0:") for accepted_cipher_suite in ssl2_result.accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") # Certificate info results certinfo_result = server_scan_result.scan_commands_results[ScanCommand.CERTIFICATE_INFO] print("\nCertificate info:") for cert_deployment in certinfo_result.certificate_deployments: print(f"Leaf certificate: \n{cert_deployment.received_certificate_chain_as_pem[0]}")
def main() -> None: # First validate that we can connect to the servers we want to scan servers_to_scan = [] for hostname in ["cloudflare.com", "google.com"]: server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( hostname, 443) try: server_info = ServerConnectivityTester().perform(server_location) servers_to_scan.append(server_info) except ConnectionToServerFailed as e: print( f"Error connecting to {server_location.hostname}:{server_location.port}: {e.error_message}" ) return scanner = Scanner() # Then queue some scan commands for each server for server_info in servers_to_scan: server_scan_req = ServerScanRequest( server_info=server_info, scan_commands={ ScanCommand.CERTIFICATE_INFO, ScanCommand.SSL_2_0_CIPHER_SUITES }, ) scanner.queue_scan(server_scan_req) # Then retrieve the result of the scan commands for each server for server_scan_result in scanner.get_results(): print( f"\nResults for {server_scan_result.server_info.server_location.hostname}:" ) # Scan commands that were run with no errors try: ssl2_result = server_scan_result.scan_commands_results[ ScanCommand.SSL_2_0_CIPHER_SUITES] print(f"\nAccepted cipher suites for SSL 2.0:") for accepted_cipher_suite in ssl2_result.accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") except KeyError: pass try: certinfo_result = server_scan_result.scan_commands_results[ ScanCommand.CERTIFICATE_INFO] print("\nCertificate info:") for cert_deployment in certinfo_result.certificate_deployments: print( f"Leaf certificate: \n{cert_deployment.received_certificate_chain_as_pem[0]}" ) except KeyError: pass # Scan commands that were run with errors for scan_command, error in server_scan_result.scan_commands_errors.items( ): print( f"\nError when running {scan_command}:\n{error.exception_trace}" )
def test_connectivity_error(self, mock_scan_commands): # Given a server to scan scan_request = ServerScanRequestFactory.create() # And the server will NOT be reachable error = ConnectionToServerFailed( server_location=scan_request.server_location, network_configuration=scan_request.network_configuration, error_message="testt", ) with mock.patch.object(_mass_connectivity_tester, "check_connectivity_to_server", side_effect=error): # And given an observer to monitor scans observer = _MockScannerObserver() # When running the scans with the observer scanner = Scanner(observers=[observer]) scanner.queue_scans([scan_request]) # It succeeds all_scan_results = [] for result in scanner.get_results(): all_scan_results.append(result) # And the right result was returned assert len(all_scan_results) == 1 assert all_scan_results[0].scan_status == ServerScanStatusEnum.ERROR_NO_CONNECTIVITY # And the observer was called appropriately assert observer.server_connectivity_test_error_calls_count == 1 assert observer.server_connectivity_test_completed_calls_count == 0 assert observer.server_scan_completed_calls_count == 1 assert observer.all_server_scans_completed_calls_count == 1
def test_error_client_certificate_needed(self): # Given a server that requires client authentication with LegacyOpenSslServer(client_auth_config=ClientAuthConfigEnum.REQUIRED) as server: # And a scan request for it that does NOT provide a client certificate scan_request = ServerScanRequest( server_location=ServerNetworkLocation( hostname=server.hostname, ip_address=server.ip_address, port=server.port ), scan_commands={ # And the request has a scan command that cannot be completed without a client certificate ScanCommand.HTTP_HEADERS, }, ) # When running the scan scanner = Scanner() scanner.queue_scans([scan_request]) # It succeeds all_results = [] for result in scanner.get_results(): all_results.append(result) # And the right result was returned assert len(all_results) == 1 # And the fact that a client certificate is needed was properly returned http_headers_result = all_results[0].scan_result.http_headers assert http_headers_result.status == ScanCommandAttemptStatusEnum.ERROR assert http_headers_result.error_reason == ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED assert http_headers_result.error_trace assert http_headers_result.result is None
def test_badssl_compliant_with_modern(self): # Given the scan results for a server that is compliant with the "modern" Mozilla config scanner = Scanner() scanner.queue_scans( [ServerScanRequest(server_location=ServerNetworkLocation(hostname="mozilla-modern.badssl.com"))] ) server_scan_result = next(scanner.get_results()) # When checking if the server is compliant with the Mozilla "modern" TLS config # It succeeds and the server is returned as compliant checker = MozillaTlsConfigurationChecker.get_default() checker.check_server( against_config=MozillaTlsConfigurationEnum.MODERN, server_scan_result=server_scan_result, ) # And the server is returned as NOT compliant for the other Mozilla configs for mozilla_config in [MozillaTlsConfigurationEnum.OLD, MozillaTlsConfigurationEnum.INTERMEDIATE]: with pytest.raises(ServerNotCompliantWithMozillaTlsConfiguration): checker.check_server(against_config=mozilla_config, server_scan_result=server_scan_result)
def scan_runner(seq,host): hostname=host.decode("utf-8") servers_to_scan = [] server_location = None try: if r.hget(seq,"ipaddr"): server_location = ServerNetworkLocationViaDirectConnection(hostname, 443, r.hget(seq,"ipaddr").decode("utf-8")) else: server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup(hostname, 443) r.hset(seq,"ipaddr",server_location.ip_address) #Initialize with hostname, port int and ip address str #print(server_location) except Exception as e: return try: server_info = ServerConnectivityTester().perform(server_location) servers_to_scan.append(server_info) except ConnectionToServerFailed as e: return scanner = Scanner() # Then queue some scan commands for each server for server_info in servers_to_scan: server_scan_req = ServerScanRequest( server_info=server_info, scan_commands={ScanCommand.TLS_1_3_EARLY_DATA}, ) scanner.queue_scan(server_scan_req) # Then retrieve the result of the scan commands for each server for server_scan_result in scanner.get_results(): try: if server_scan_result.scan_commands_results[ScanCommand.TLS_1_3_EARLY_DATA].supports_early_data: r.hset(seq,"early","TRUE") except KeyError: return
def search_subject_alt_name(self, target): print("Searching for Subject Alt Names") try: server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( target, 443) # Do connectivity testing to ensure SSLyze is able to connect try: server_info = ServerConnectivityTester().perform(server_location) except ConnectionToServerFailed as e: # Could not connect to the server; abort print(f"Error connecting to {server_location}: {e.error_message}") return # Then queue some scan commands for the server scanner = Scanner() server_scan_req = ServerScanRequest(server_info=server_info, scan_commands={ ScanCommand.CERTIFICATE_INFO}, ) scanner.queue_scan(server_scan_req) # Then retrieve the results for server_scan_result in scanner.get_results(): # Certificate info results certinfo_result = server_scan_result.scan_commands_results[ ScanCommand.CERTIFICATE_INFO] # Direct object reference is pretty bad, but then again so is the crypto.x509 object implementation, so... cert_deployment = certinfo_result.certificate_deployments[0] chain = cert_deployment.received_certificate_chain[0] ext = chain.extensions.get_extension_for_oid( ExtensionOID.SUBJECT_ALTERNATIVE_NAME) for entry in ext.value.get_values_for_type(x509.DNSName): if entry.strip() not in self.domains: self.domains.append(entry.strip()) except Exception as e: self.handle_exception(e)
def main() -> None: # Parse the supplied command line date_scans_started = datetime.utcnow() sslyze_parser = CommandLineParser(__version__) try: parsed_command_line = sslyze_parser.parse_command_line() except CommandLineParsingError as e: print(e.get_error_msg()) return # Setup the observer to print to the console, if needed scanner_observers = [] if not parsed_command_line.should_disable_console_output: observer_for_console_output = ObserverToGenerateConsoleOutput( file_to=sys.stdout, json_path_out=parsed_command_line.json_path_out ) observer_for_console_output.command_line_parsed(parsed_command_line=parsed_command_line) scanner_observers.append(observer_for_console_output) # Setup the scanner sslyze_scanner = Scanner( per_server_concurrent_connections_limit=parsed_command_line.per_server_concurrent_connections_limit, concurrent_server_scans_limit=parsed_command_line.concurrent_server_scans_limit, observers=scanner_observers, ) # Queue the scans all_server_scan_requests = [] for server_location, network_config in parsed_command_line.servers_to_scans: scan_request = ServerScanRequest( server_location=server_location, network_configuration=network_config, scan_commands=parsed_command_line.scan_commands, scan_commands_extra_arguments=parsed_command_line.scan_commands_extra_arguments, ) all_server_scan_requests.append(scan_request) # If there are servers that we were able to resolve, scan them all_server_scan_results = [] if all_server_scan_requests: sslyze_scanner.queue_scans(all_server_scan_requests) for result in sslyze_scanner.get_results(): # Results are actually displayed by the observer; here we just store them all_server_scan_results.append(result) # Write results to a JSON file if needed json_file_out: Optional[TextIO] = None if parsed_command_line.should_print_json_to_console: json_file_out = sys.stdout elif parsed_command_line.json_path_out: json_file_out = parsed_command_line.json_path_out.open("wt", encoding="utf-8") if json_file_out: json_output = SslyzeOutputAsJson( server_scan_results=[ServerScanResultAsJson.from_orm(result) for result in all_server_scan_results], invalid_server_strings=[ InvalidServerStringAsJson.from_orm(bad_server) for bad_server in parsed_command_line.invalid_servers ], date_scans_started=date_scans_started, date_scans_completed=datetime.utcnow(), ) json_output_as_str = json_output.json(sort_keys=True, indent=4, ensure_ascii=True) json_file_out.write(json_output_as_str) # If we printed the JSON results to the console, don't run the Mozilla compliance check so we return valid JSON if parsed_command_line.should_print_json_to_console: sys.exit(0) if not all_server_scan_results: # There are no results to present: all supplied server strings were invalid? sys.exit(0) # Check the results against the Mozilla config if needed are_all_servers_compliant = True # TODO(AD): Expose format_title method title = ObserverToGenerateConsoleOutput._format_title("Compliance against Mozilla TLS configuration") print() print(title) if not parsed_command_line.check_against_mozilla_config: print(" Disabled; use --mozilla_config={old, intermediate, modern}.\n") else: print( f' Checking results against Mozilla\'s "{parsed_command_line.check_against_mozilla_config}"' f" configuration. See https://ssl-config.mozilla.org/ for more details.\n" ) mozilla_checker = MozillaTlsConfigurationChecker.get_default() for server_scan_result in all_server_scan_results: try: mozilla_checker.check_server( against_config=parsed_command_line.check_against_mozilla_config, server_scan_result=server_scan_result, ) print(f" {server_scan_result.server_location.display_string}: OK - Compliant.\n") except ServerNotCompliantWithMozillaTlsConfiguration as e: are_all_servers_compliant = False print(f" {server_scan_result.server_location.display_string}: FAILED - Not compliant.") for criteria, error_description in e.issues.items(): print(f" * {criteria}: {error_description}") print() except ServerScanResultIncomplete: are_all_servers_compliant = False print( f" {server_scan_result.server_location.display_string}: ERROR - Scan did not run successfully;" f" review the scan logs above." ) if not are_all_servers_compliant: # Return a non-zero error code to signal failure (for example to fail a CI/CD pipeline) sys.exit(1)
def server_scan_result_for_google(): scanner = Scanner() scanner.queue_scans([ServerScanRequest(server_location=ServerNetworkLocation(hostname="google.com"))]) for server_scan_result in scanner.get_results(): yield server_scan_result
def tls_scan(ip_address, str_host, commands_to_run, port_to_scan): servers_to_scan = [] start_date = datetime.today() global connection_data # Loop through all hostnames and attempt to connect # if error message is returned (ie scanner could not connect, return error message and exit function) error = createServerConnections(ip_address, str_host, servers_to_scan, port_to_scan) if error != 'success': connection_data = {} connection_data.update({'tls_error': error.__str__()}) return connection_data # exit function early scanner = Scanner() # Queue the desired scan commands for each server addScanRequests(scanner, servers_to_scan, commands_to_run) for server_scan_result in scanner.get_results(): connection_data = { } # Dictionary to hold data until it is written to JSON file recommendations_data = {} # Get IP address hostname hostname = server_scan_result.server_info.server_location.hostname # Collect relevant information from server_info results ip_address = server_scan_result.server_info.server_location.ip_address cipher_suite_supported = server_scan_result.server_info.tls_probing_result.cipher_suite_supported client_auth_requirement = \ server_scan_result.server_info.tls_probing_result.client_auth_requirement.name highest_tls_v_supported = \ server_scan_result.server_info.tls_probing_result.highest_tls_version_supported.name # Add information to dictionary connection_data.update({'hostname': hostname}) # from server location connection_data.update({'ip_address': ip_address}) # from server location connection_data.update( {'cipher_suite_supported': cipher_suite_supported}) # from tls_probing_result connection_data.update( {'client_authorization_requirement': client_auth_requirement}) # from tls_probing_result connection_data.update( {'highest_tls_version_supported': highest_tls_v_supported}) # from tls_probing_result if 'certificate_info' in commands_to_run: try: certinfo_result = server_scan_result.scan_commands_results[ ScanCommand.CERTIFICATE_INFO] all_certificates_info = {} # cycle through all certificates (IP may have more than one) count: int = 0 for cert_deployment in certinfo_result.certificate_deployments: #print(count) if count == 0: all_certificates_info.update({ 'leaf_certificate_has_must_staple_extension': cert_deployment. leaf_certificate_has_must_staple_extension }) all_certificates_info.update({ 'leaf_certificate_is_ev': cert_deployment.leaf_certificate_is_ev }) all_certificates_info.update({ 'received_chain_has_valid_order': cert_deployment.received_chain_has_valid_order }) all_certificates_info.update({ 'received_chain_has_contains_root': cert_deployment. received_chain_contains_anchor_certificate }) all_certificates_info.update({ 'leaf_certificate_signed_certificate_timestamps_count': cert_deployment. leaf_certificate_signed_certificate_timestamps_count }) all_certificates_info.update({ 'leaf_certificate_subject_matches_hostname': cert_deployment. leaf_certificate_subject_matches_hostname }) ocsp_response = cert_deployment.ocsp_response if ocsp_response is not None: ocsp_response_data = {} if ocsp_response.status.value == 0: ocsp_response_data.update( {'status': 'SUCCESSFUL'}) ocsp_response_data.update( {'type': ocsp_response.type}) ocsp_response_data.update( {'version': ocsp_response.version}) ocsp_response_data.update({ 'responder_id': ocsp_response.responder_id }) ocsp_response_data.update({ 'certificate_status': ocsp_response.certificate_status }) ocsp_response_data.update({ 'hash_algorithm': ocsp_response.hash_algorithm }) ocsp_response_data.update({ 'issuer_name_hash': ocsp_response.issuer_name_hash }) ocsp_response_data.update({ 'issuer_key_hash': ocsp_response.issuer_key_hash }) ocsp_response_data.update({ 'serial_number': ocsp_response.serial_number }) elif ocsp_response.status.value == 1: ocsp_response_data.update( {'status': 'MALFORMED_REQUEST'}) elif ocsp_response.status.value == 2: ocsp_response_data.update( {'status': 'INTERNAL_ERROR'}) elif ocsp_response.status.value == 3: ocsp_response_data.update( {'status': 'TRY_LATER'}) elif ocsp_response.status.value == 5: ocsp_response_data.update( {'status': 'SIG_REQUIRED'}) elif ocsp_response.status.value == 6: ocsp_response_data.update( {'status': 'UNAUTHORIZED'}) all_certificates_info.update( {'ocsp_response': ocsp_response_data}) else: all_certificates_info.update( {'ocsp_response': ocsp_response}) all_certificates_info.update({ 'ocsp_response_is_trusted': cert_deployment.ocsp_response_is_trusted }) # Create a dictionary with the path validation results for each validated trust store trust_store_checks = {} for path_validation_result in cert_deployment.path_validation_results: if path_validation_result.was_validation_successful: trust_store_checks.update({ path_validation_result.trust_store.name: path_validation_result.openssl_error_string }) # Code from sslyze for reference (we can use the was_validation_successful variable if needed) # for path_validation_result in all_path_validation_results: # if path_validation_result.was_validation_successful: # trust_store_that_can_build_verified_chain = path_validation_result.trust_store # verified_certificate_chain = path_validation_result.verified_certificate_chain # break # Check for certificate errors (using Mozilla as the trust store to check against) certificate_errors = {} if "Mozilla" in trust_store_checks.keys( ) and trust_store_checks.get("Mozilla") is None: certificate_errors.update({'cert_trusted': True}) elif "Mozilla" in trust_store_checks.keys(): certificate_errors.update({'cert_trusted': False}) certificate_errors.update( {'cert_error': trust_store_checks.get("Mozilla")}) else: certificate_errors.update({'cert_trusted': False}) certificate_errors.update( {'cert_error': "Mozilla not trusted"}) certificate_errors.update({ 'hostname_matches': cert_deployment. leaf_certificate_subject_matches_hostname }) # Collect certificate (returns a string literal from CertificateDeploymentAnalysisResult class) certificate = cert_deployment.received_certificate_chain_as_pem[ count] # Returns updated dictionary with certificate information certificate_info = getCertificateResults(certificate) # Add possible certificate errors to dictionary certificate_info.update( {'certificate_errors': certificate_errors}) # Add certificate data to overall scan dictionary all_certificates_info.update( {'certificate_' + str(count): certificate_info}) count += 1 connection_data.update( {"certificate_info": all_certificates_info}) except KeyError: pass if 'ssl_2_0_cipher_suites' in commands_to_run: # Collect results for accepted SSL 2.0 cipher suites try: ssl2_data = {} ssl2_result = server_scan_result.scan_commands_results[ ScanCommand.SSL_2_0_CIPHER_SUITES] preferred_cipher_suite = ssl2_result.cipher_suite_preferred_by_server if preferred_cipher_suite is not None: ssl2_data.update({ 'preferred_cipher_suite': preferred_cipher_suite.cipher_suite.name }) else: ssl2_data.update({'preferred_cipher_suite': None}) cipher_suite_list = [] for accepted_cipher_suite in ssl2_result.accepted_cipher_suites: cipher_suite_list.append( accepted_cipher_suite.cipher_suite.name) recommendations_data.update({ 'CRITICAL - SSLv2': 'SSLv2 is severely broken and should be disabled. Recommend disabling SSLv2 immediately. ' }) ssl2_data.update( {'accepted_ssl_2_0_cipher_suites': cipher_suite_list}) connection_data.update({'ssl_2_0': ssl2_data}) except KeyError: pass if 'ssl_3_0_cipher_suites' in commands_to_run: # Collect results for accepted SSL 3.0 cipher suites try: ssl3_data = {} ssl3_result = server_scan_result.scan_commands_results[ ScanCommand.SSL_3_0_CIPHER_SUITES] preferred_cipher_suite = ssl3_result.cipher_suite_preferred_by_server if preferred_cipher_suite is not None: ssl3_data.update({ 'preferred_cipher_suite': preferred_cipher_suite.cipher_suite.name }) else: ssl3_data.update({'preferred_cipher_suite': None}) cipher_suite_list = [] for accepted_cipher_suite in ssl3_result.accepted_cipher_suites: cipher_suite_list.append( accepted_cipher_suite.cipher_suite.name) recommendations_data.update({ 'CRITICAL - SSLv3': 'You may be vulnerable to the POODLE attack. Recommend disabling SSLv3 immediately. ' }) ssl3_data.update( {'accepted_ssl_3_0_cipher_suites': cipher_suite_list}) connection_data.update({'ssl_3_0': ssl3_data}) except KeyError: pass if 'tls_1_0_cipher_suites' in commands_to_run: # Collect results for accepted TLS 1.0 cipher suites try: tls1_0_data = {} tls1_0_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_0_CIPHER_SUITES] preferred_cipher_suite = tls1_0_result.cipher_suite_preferred_by_server if preferred_cipher_suite is not None: tls1_0_data.update({ 'preferred_cipher_suite': preferred_cipher_suite.cipher_suite.name }) else: tls1_0_data.update({'preferred_cipher_suite': None}) cipher_suite_list = [] cipher_suite_warning = [] for accepted_cipher_suite in tls1_0_result.accepted_cipher_suites: cipher_suite_list.append( accepted_cipher_suite.cipher_suite.name) recommendations_data.update({ 'HIGH - TLSv1.0': 'Major browsers are disabling TLS 1.0 imminently. Carefully monitor if clients still use this protocol. ' }) # See if this cipher suite is in the dictionary of weak ciphers for key, dict_warning in warning_bad_ciphers.items(): # Check if a bad cipher is in the list of ciphers support, but ignore if we've already come across it if (key in accepted_cipher_suite.cipher_suite.name ) and not (key in cipher_suite_warning): cipher_suite_warning.append(key) recommendations_data.update( {dict_warning[0]: dict_warning[1]}) tls1_0_data.update( {'accepted_tls_1_0_cipher_suites': cipher_suite_list}) connection_data.update({'tls_1_0': tls1_0_data}) except KeyError: pass if 'tls_1_1_cipher_suites' in commands_to_run: # Collect results for accepted TLS 1.1 cipher suites try: tls1_1_data = {} tls1_1_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_1_CIPHER_SUITES] preferred_cipher_suite = tls1_1_result.cipher_suite_preferred_by_server if preferred_cipher_suite is not None: tls1_1_data.update({ 'preferred_cipher_suite': preferred_cipher_suite.cipher_suite.name }) else: tls1_1_data.update({'preferred_cipher_suite': None}) cipher_suite_list = [] for accepted_cipher_suite in tls1_1_result.accepted_cipher_suites: cipher_suite_list.append( accepted_cipher_suite.cipher_suite.name) recommendations_data.update({ 'HIGH - TLSv1.1': 'Major browsers are disabling this TLS 1.1 immenently. Carefully monitor if clients still use this protocol. ' }) # See if this cipher suite is in the dictionary of weak ciphers for key, dict_warning in warning_bad_ciphers.items(): # Check if a bad cipher is in the list of ciphers support, but ignore if we've already come across it if (key in accepted_cipher_suite.cipher_suite.name ) and not (key in cipher_suite_warning): cipher_suite_warning.append(key) recommendations_data.update( {dict_warning[0]: dict_warning[1]}) tls1_1_data.update( {'accepted_tls_1_1_cipher_suites': cipher_suite_list}) connection_data.update({'tls_1_1': tls1_1_data}) except KeyError: pass if 'tls_1_2_cipher_suites' in commands_to_run: # Collect results for accepted TLS 1.2 cipher suites try: tls1_2_data = {} tls1_2_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_2_CIPHER_SUITES] preferred_cipher_suite = tls1_2_result.cipher_suite_preferred_by_server if preferred_cipher_suite is not None: tls1_2_data.update({ 'preferred_cipher_suite': preferred_cipher_suite.cipher_suite.name }) else: tls1_2_data.update({'preferred_cipher_suite': None}) cipher_suite_list = [] for accepted_cipher_suite in tls1_2_result.accepted_cipher_suites: cipher_suite_list.append( accepted_cipher_suite.cipher_suite.name) tls1_2_data.update( {'accepted_tls_1_2_cipher_suites': cipher_suite_list}) connection_data.update({'tls_1_2': tls1_2_data}) except KeyError: pass if 'tls_1_3_cipher_suites' in commands_to_run: # Collect results for accepted TLS 1.3 cipher suites try: tls1_3_data = {} tls1_3_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_3_CIPHER_SUITES] preferred_cipher_suite = tls1_3_result.cipher_suite_preferred_by_server if preferred_cipher_suite is not None: tls1_3_data.update({ 'preferred_cipher_suite': preferred_cipher_suite.cipher_suite.name }) else: tls1_3_data.update({'preferred_cipher_suite': None}) cipher_suite_list = [] for accepted_cipher_suite in tls1_3_result.accepted_cipher_suites: cipher_suite_list.append( accepted_cipher_suite.cipher_suite.name) tls1_3_data.update( {'accepted_tls_1_3_cipher_suites': cipher_suite_list}) connection_data.update({'tls_1_3': tls1_3_data}) except KeyError: pass test_results = {} # dictionary to store results of optional tests if 'tls_compression' in commands_to_run: # Collect results for TLS compression test try: tls_compression_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_COMPRESSION] if tls_compression_result.supports_compression: test_results.update({'compression_supported': True}) else: test_results.update({'compression_supported': False}) except KeyError: pass if 'tls_1_3_early_data' in commands_to_run: # Collect results for early data acceptance try: tls1_3_early_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_3_EARLY_DATA] if tls1_3_early_result.supports_early_data: test_results.update({'accepts_early_data': True}) else: test_results.update({'accepts_early_data': False}) except KeyError: pass if 'openssl_ccs_injection' in commands_to_run: # Collect results for CVE-2014-0224 vulnerability try: openssl_css_injection_result = server_scan_result.scan_commands_results[ ScanCommand.OPENSSL_CCS_INJECTION] if openssl_css_injection_result.is_vulnerable_to_ccs_injection: test_results.update({'CVE-2014-0224_vulnerable': True}) else: test_results.update({'CVE-2014-0224_vulnerable': False}) except KeyError: pass if 'tls_fallback_scsv' in commands_to_run: # Collect results for TLS fallback result try: tls_fallback_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_FALLBACK_SCSV] if tls_fallback_result.supports_fallback_scsv: test_results.update({'supports_tls_fallback': True}) else: test_results.update({'supports_tls_fallback': False}) except KeyError: pass if 'heartbleed' in commands_to_run: # Collect results for heartbleed vulnerability try: heartbleed_result = server_scan_result.scan_commands_results[ ScanCommand.HEARTBLEED] if heartbleed_result.is_vulnerable_to_heartbleed: test_results.update({'vulnerable_to_heartbleed': True}) else: test_results.update({'vulnerable_to_heartbleed': False}) except KeyError: pass if 'robot' in commands_to_run: # Collect results for robot vulnerability try: obj_robot_result = server_scan_result.scan_commands_results[ ScanCommand.ROBOT] int_robot_results = obj_robot_result.robot_result.value # server_info.tls_probing_result.highest_tls_version_supported.name if int_robot_results == 1: test_results.update({'vulnerable_to_robot': True}) test_results.update( {'vulnerable_to_robot_desc': 'Weak oracle'}) recommendations_data.update({ 'CRITICAL - ROBOT': 'ROBOT vulnerability detected. Recommend disabling RSA encryption and using DH, ECDH, DHE or ECDHE.' }) elif int_robot_results == 2: test_results.update({'vulnerable_to_robot': True}) test_results.update( {'vulnerable_to_robot_desc': 'Strong oracle'}) recommendations_data.update({ 'CRITICAL - ROBOT': 'ROBOT vulnerability detected. Recommend disabling RSA encryption and using DH, ECDH, DHE or ECDHE.' }) elif int_robot_results == 3: test_results.update({'vulnerable_to_robot': False}) test_results.update( {'vulnerable_to_robot_desc': 'No oracle'}) elif int_robot_results == 4: test_results.update({'vulnerable_to_robot': False}) test_results.update({'vulnerable_to_robot_desc': 'No RSA'}) elif int_robot_results == 5: test_results.update({'vulnerable_to_robot': False}) test_results.update({'vulnerable_to_robot_desc': ''}) else: test_results.update({'vulnerable_to_robot': False}) test_results.update( {'vulnerable_to_robot_desc': 'Test failed'}) except KeyError: pass # Section not finished yet, will come back to it if 'http_headers' in commands_to_run: http_info = {} try: http_results = server_scan_result.scan_commands_results[ ScanCommand.HTTP_HEADERS] strict_transport_security_header = http_results.strict_transport_security_header if strict_transport_security_header is not None: strict_transport_info = {} strict_transport_info.update( {'preload': strict_transport_security_header.preload}) strict_transport_info.update({ 'include_subdomains': strict_transport_security_header.include_subdomains }) strict_transport_info.update( {'max_age': strict_transport_security_header.max_age}) http_info.update({ 'strict_transport_security_header': strict_transport_info }) public_key_pins_header = http_results.public_key_pins_header if public_key_pins_header is not None: public_pins_info = {} public_pins_info.update({ 'include_subdomains': public_key_pins_header.include_subdomains }) public_pins_info.update( {'max_age': public_key_pins_header.max_age}) public_pins_info.update( {'sha256_pins': public_key_pins_header.sha256_pins}) public_pins_info.update( {'report_uri': public_key_pins_header.report_uri}) public_pins_info.update( {'report_to': public_key_pins_header.report_to}) http_info.update( {'public_key_pins_header': public_pins_info}) public_key_pins_report_only_header = http_results.public_key_pins_report_only_header if public_key_pins_report_only_header is not None: public_pins_report_info = {} public_pins_report_info.update({ 'include_subdomains': public_key_pins_report_only_header.include_subdomains }) public_pins_report_info.update({ 'max_age': public_key_pins_report_only_header.max_age }) public_pins_report_info.update({ 'sha256_pins': public_key_pins_report_only_header.sha256_pins }) public_pins_report_info.update({ 'report_uri': public_key_pins_report_only_header.report_uri }) public_pins_report_info.update({ 'report_to': public_key_pins_report_only_header.report_to }) http_info.update( {'public_key_pins_header': public_pins_report_info}) expect_ct_headers = http_results.expect_ct_header if expect_ct_headers is not None: expect_ct_info = {} expect_ct_info.update( {'max_age': expect_ct_headers.max_age}) expect_ct_info.update( {'report_uri': expect_ct_headers.report_uri}) expect_ct_info.update( {'enforce': expect_ct_headers.enforce}) http_info.update({'expect_ct_info': expect_ct_info}) test_results.update({'http_headers': http_info}) except KeyError: pass if 'session_renegotiation' in commands_to_run: try: renegotiation_results = server_scan_result.scan_commands_results[ ScanCommand.SESSION_RENEGOTIATION] session_reneg = {} session_reneg.update({ 'accepts_client_renegotiation': renegotiation_results.accepts_client_renegotiation }) session_reneg.update({ 'supports_secure_renegotiation': renegotiation_results.supports_secure_renegotiation }) test_results.update({'session_renegotiation': session_reneg}) except KeyError: pass if 'session_resumption' in commands_to_run: try: session_resumption_results = server_scan_result.scan_commands_results[ ScanCommand.SESSION_RESUMPTION] if session_resumption_results.is_session_id_resumption_supported: session_resumption_info = {} session_resumption_info.update({ 'attempted_session_id_resumptions_count': session_resumption_results. attempted_session_id_resumptions_count }) session_resumption_info.update({ 'successful_session_id_resumptions_count': session_resumption_results. successful_session_id_resumptions_count }) if session_resumption_results.is_tls_ticket_resumption_supported: if session_resumption_results.tls_ticket_resumption_result.value == 1: session_resumption_info.update( {'tls_ticket_resumption_results': 'SUCCEEDED'}) elif session_resumption_results.tls_ticket_resumption_result.value == 2: session_resumption_info.update({ 'tls_ticket_resumption_results': 'FAILED_TICKET_NOT_ASSIGNED' }) elif session_resumption_results.tls_ticket_resumption_result.value == 3: session_resumption_info.update({ 'tls_ticket_resumption_results': 'FAILED_TICKET_IGNORED' }) elif session_resumption_results.tls_ticket_resumption_result.value == 4: session_resumption_info.update({ 'tls_ticket_resumption_results': 'FAILED_ONLY_TLS_1_3_SUPPORTED' }) test_results.update( {'session_resumption': session_resumption_info}) except KeyError: pass if 'session_resumption_rate' in commands_to_run: try: session_resumption_rate_results = server_scan_result.scan_commands_results[ ScanCommand.SESSION_RESUMPTION_RATE] session_resume_info = {} session_resume_info.update({ 'attempted_session_id_resumptions_count': session_resumption_rate_results. attempted_session_id_resumptions_count }) session_resume_info.update({ 'successful_session_id_resumptions_count': session_resumption_rate_results. successful_session_id_resumptions_count }) test_results.update( {'session_resumption_rate': session_resume_info}) except KeyError: pass # Add results of vulnerability testing to dictionary connection_data.update({'tests': test_results}) # Scan meta data end_date = datetime.today() metadata = {} # Metadata for scan metadata.update({'tls_scan_start': start_date.__str__()}) metadata.update({'tls_scan_end': end_date.__str__()}) metadata.update({'scan_parameters': commands_to_run}) # Scan commands that were run with errors commands_with_errors = {} for scan_command, error in server_scan_result.scan_commands_errors.items( ): commands_with_errors.update({scan_command: error.exception_trace}) metadata.update({'commands_with_errors': commands_with_errors}) # Add meta data to overall information dictionary connection_data.update({'scan_information': metadata}) # Add recommendations data to overall information dictionary connection_data.update({'tls_recommendations': recommendations_data}) return connection_data
def main() -> None: # First create the scan requests for each server that we want to scan try: all_scan_requests = [ ServerScanRequest(server_location=ServerNetworkLocation(hostname="cloudflare.com")), ServerScanRequest(server_location=ServerNetworkLocation(hostname="google.com")), ] except ServerHostnameCouldNotBeResolved: # Handle bad input ie. invalid hostnames print("Error resolving the supplied hostnames") return # Then queue all the scans scanner = Scanner() scanner.queue_scans(all_scan_requests) # And retrieve and process the results for each server for server_scan_result in scanner.get_results(): print(f"\n\n****Results for {server_scan_result.server_location.hostname}****") # Were we able to connect to the server and run the scan? if server_scan_result.scan_status == ServerScanStatusEnum.ERROR_NO_CONNECTIVITY: # No we weren't print( f"\nError: Could not connect to {server_scan_result.server_location.hostname}:" f" {server_scan_result.connectivity_error_trace}" ) continue # Since we were able to run the scan, scan_result is populated assert server_scan_result.scan_result # Process the result of the SSL 2.0 scan command ssl2_attempt = server_scan_result.scan_result.ssl_2_0_cipher_suites if ssl2_attempt.status == ScanCommandAttemptStatusEnum.ERROR: # An error happened when this scan command was run _print_failed_scan_command_attempt(ssl2_attempt) elif ssl2_attempt.status == ScanCommandAttemptStatusEnum.COMPLETED: # This scan command was run successfully ssl2_result = ssl2_attempt.result assert ssl2_result print("\nAccepted cipher suites for SSL 2.0:") for accepted_cipher_suite in ssl2_result.accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") # Process the result of the TLS 1.3 scan command tls1_3_attempt = server_scan_result.scan_result.tls_1_3_cipher_suites if tls1_3_attempt.status == ScanCommandAttemptStatusEnum.ERROR: _print_failed_scan_command_attempt(ssl2_attempt) elif tls1_3_attempt.status == ScanCommandAttemptStatusEnum.COMPLETED: tls1_3_result = tls1_3_attempt.result assert tls1_3_result print("\nAccepted cipher suites for TLS 1.3:") for accepted_cipher_suite in tls1_3_result.accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") # Process the result of the certificate info scan command certinfo_attempt = server_scan_result.scan_result.certificate_info if certinfo_attempt.status == ScanCommandAttemptStatusEnum.ERROR: _print_failed_scan_command_attempt(certinfo_attempt) elif certinfo_attempt.status == ScanCommandAttemptStatusEnum.COMPLETED: certinfo_result = certinfo_attempt.result assert certinfo_result print("\nLeaf certificates deployed:") for cert_deployment in certinfo_result.certificate_deployments: leaf_cert = cert_deployment.received_certificate_chain[0] print( f"{leaf_cert.public_key().__class__.__name__}: {leaf_cert.subject.rfc4514_string()}" f" (Serial: {leaf_cert.serial_number})" )
class ReportSSL: def __init__(self): self.output = '' self.imageFolder = 'images' with open('ciphers.json') as j: self.ciphers = json.load(j) self.parseArgsAndCheckConnectivity() self.getAllCiphers() self.certificate() self.deprecatedTLS() self.deprecatedSSL() self.TLSv1_3() self.downgradePrevention() self.OCSPStapling() self.specificAlg('RC4', None, 'Accepted RC4 cipher suites', 'RC4') self.specificAlg( '3DES', None, 'Server is vulnerable to SWEET32 attacks because it supports block-based algorithms with block size of 64 (3DES)', 'SWEET32') self.specificAlg( 'CBC', ["SSLv2", "SSLv3", "TLSv1.0"], 'Server is vulnerable to BEAST attacks.\nIt supports block-based algorithms (CBC) in SSLv2, SSLv3 or TLSv1.0', 'BEAST') self.specificAlg( 'CBC', ["SSLv3"], 'Server is vulnerable to POODLE attacks.\nIt supports block-based algorithms (CBC) in SSLv3', 'POODLE') self.drown() self.specificAlg( 'CBC', ["TLSv1.0", "TLSv1.1", "TLSv1.2"], 'Server is vulnerable to LUCKY13 attacks.\nIt supports block-based algorithms (CBC) in TLS', 'LUCKY13') self.logjamAndFreak() self.breach() self.crime() self.secureRenegotiation() self.robot() #TODO -> make openssl command a function to reduce code ''' TIME -> HEIST -> SLOTH -> TLS 1.2, RSA-MD5 SIGNATURE -> generar un certificado de cliente con MD5 y enviarlo, si el servidor lo acepta es vulnerable HEARTBLEED ZOMBIE GOLDENDOODLE client renegotiation ''' def parseArgsAndCheckConnectivity(self): if len(sys.argv) == 3 or len(sys.argv) == 4: if len(sys.argv) == 4: if sys.argv[1] == '--verbose': self.verbose = True self.host = sys.argv[2] self.port = sys.argv[3] else: self.printHelp() else: self.verbose = False self.host = sys.argv[1] self.port = sys.argv[2] try: print('Testing connectivity ...', end='', flush=True) # Define the server that you want to scan serverLocation = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( self.host, self.port) # Do connectivity testing to ensure SSLyze is able to connect self.serverInfo = ServerConnectivityTester().perform( serverLocation) except ConnectionToServerFailed as e: # Could not connect to the server; abort print( f"Error connecting to {serverLocation}: {e.error_message}") sys.exit() except ServerHostnameCouldNotBeResolved: print( f"Cannot resolve {self.host}, check that it is correct (IP is correct and domain does not include protocol)" ) sys.exit() print(" COMPLETED.") self.highestProtocol = self.serverInfo.tls_probing_result.highest_tls_version_supported.name else: self.printHelp() def printHelp(self): print( 'Execute:\n\tpython reportSSL.py www.google.es 443\t\t(for silent mode)\n\tpython reportSSL.py --verbose www.google.es 443\t\t(for verbose mode)' ) sys.exit() def getAllCiphers(self): self.allCiphers = {} print('Retrieving ciphers for each protocol and checking order ...', end='', flush=True) results = self.initiateScan({ ScanCommand.SSL_2_0_CIPHER_SUITES, ScanCommand.SSL_3_0_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_3_CIPHER_SUITES }) keys = { "SSLv2": ScanCommand.SSL_2_0_CIPHER_SUITES, "SSLv3": ScanCommand.SSL_3_0_CIPHER_SUITES, "TLSv1.0": ScanCommand.TLS_1_0_CIPHER_SUITES, "TLSv1.1": ScanCommand.TLS_1_1_CIPHER_SUITES, "TLSv1.2": ScanCommand.TLS_1_2_CIPHER_SUITES, "TLSv1.3": ScanCommand.TLS_1_3_CIPHER_SUITES } order = [] for result in results: for key in keys.keys(): self.allCiphers.update({key: []}) try: tls = result.scan_commands_results[keys[key]] for cipher in tls.accepted_cipher_suites: try: self.allCiphers[key].append(cipher) except Exception: print( f'Cipher {cipher.cipher_suite.name} not found in database' ) except KeyError: print(key + ' scan failed') #Check if server has cipher order preference if tls.cipher_suite_preferred_by_server == None and len( self.allCiphers[key]) > 0: order.append(key) if len(order) > 0: print(' No cipher order for certain protocols.') self.generateImageAndPrintInfo( f"Server does not have cipher order for the following supported protocols (server {self.host}):", ', '.join(order), 'CIPHER_ORDER', None, None) else: print() def certificate(self): print('Checking certificate ...', end='', flush=True) check = False results = self.initiateScan({ScanCommand.CERTIFICATE_INFO}) for result in results: certs = result.scan_commands_results[ ScanCommand.CERTIFICATE_INFO].certificate_deployments #Check if hostname matches certificate name for cert in certs: if not cert.leaf_certificate_subject_matches_hostname: check = True data = 'Hostname: ' + result.scan_commands_results[ ScanCommand. CERTIFICATE_INFO].hostname_used_for_server_name_indication + '\n' print(cert.received_certificate_chain[0].subject) data += 'Certificate name: ' + str( cert.received_certificate_chain[0].subject).replace( '<Name(', '').replace(')>', '') self.generateImageAndPrintInfo( 'Certificate is not trusted because it does not match hostname', data, 'CertificateUntrustedNameMismatch', None, None) #Check if certificate chain is sent in the right order if not cert.received_chain_has_valid_order: check = True #Loop certs to get order counter = 1 data = '' for cert2 in cert.received_certificate_chain[::-1]: data += f"{counter}-> {str(cert2.subject).replace('<Name(', '').replace(')>', '')}\n" counter += 1 self.generateImageAndPrintInfo( 'Certificate chain is not sent in the right order', data[:-1], 'CertificateChainWrongOrder', None, None) #Check if leaf certificate is Extended Validation, according to Mozilla if not cert.leaf_certificate_is_ev: check = True data = 'Leaf Certificate: ' + str( cert.received_certificate_chain[0].subject).replace( '<Name(', '').replace(')>', '') self.generateImageAndPrintInfo( 'Leaf certificate is not EV (Extended Validation)', data, 'LeafCertificateNotEV', None, None) #Check if leaf certificate has OCSP must-staple extension if not cert.leaf_certificate_has_must_staple_extension: check = True data = 'Leaf Certificate: ' + str( cert.received_certificate_chain[0].subject).replace( '<Name(', '').replace(')>', '') self.generateImageAndPrintInfo( 'Leaf certificate does not have OCSP must-staple extension', data, 'LeafCertificateNotOCSPMustStaple', None, None) #Check if any certificate has SHA1 signature if cert.verified_chain_has_sha1_signature: print('sha1') check = True data = '' for cert2 in cert.received_certificate_chain[::-1]: if cert2.signature_hash_algorithm.name.lower( ) == 'sha1': data += f"{str(cert2.subject).replace('<Name(', '').replace(')>', '')} SHA1:{print(cert2.fingerprint(cert2.signature_hash_algorithm)).decode('utf-8')}\n" self.generateImageAndPrintInfo( 'Some certificates have SHA1 signatures', data[:-1], 'SHA1Signatures', None, None) # Check not_valid_before, not_valid_after and total validity period counter = 0 for c2 in cert.received_certificate_chain: n = datetime.datetime.now() if n < c2.not_valid_before: data = f"Certificate {c2.subject} is being used before its validity period\n" data += f"Checked on {n.strftime('%d/%m/%Y-%H:%M')} with a not_valid_before of {c2.not_valid_before.strftime('%d/%m/%Y-%H:%M')}. Difference of {self.formatTimedelta(c2.not_valid_before - n)}" self.generateImageAndPrintInfo( 'Certificate used before its validity period', data, 'Certificate' + str(counter) + '_Before', None, None) if n > c2.not_valid_after: data = f"Certificate {c2.subject} is being used after its validity period\n" data += f"Checked on {n.strftime('%d/%m/%Y-%H:%M')} with a not_valid_after of {c2.not_valid_after.strftime('%d/%m/%Y-%H:%M')}. Difference of {self.formatTimedelta(n - c2.not_valid_before)}" self.generateImageAndPrintInfo( 'Certificate used after its validity period', data, 'Certificate' + str(counter) + '_After', None, None) if (c2.not_valid_after - c2.not_valid_before).days > 398: # 398 days as stated by Apple and then followed by Mozilla and Google data = f"Certificate {c2.subject} has a validity period of over 398 (the standard of Apple, Mozilla and Google)\n" data += f"Checked on {n.strftime('%d/%m/%Y-%H:%M')} the validity period is {(c2.not_valid_after - c2.not_valid_before).days} days" self.generateImageAndPrintInfo( 'Certificate has a validity period of over 398 days', data, 'Certificate' + str(counter) + '_ValidityPeriod', None, None) counter += 1 if check: print(' MISCONFIGURATION') else: print() def formatTimedelta(self, delta): hours, remainder = divmod(delta.seconds, 3600) minutes, seconds = divmod(remainder, 60) res = '' if delta.days > 0: res = f'{delta.days} days, ' res += f'{hours} hours, {minutes} minutes, {seconds} seconds' return res def deprecatedTLS(self): keys = ["TLSv1.0", "TLSv1.1"] check = False print('Checking usage of deprecated TLS ...', end='', flush=True) for key in keys: pt = PrettyTable(border=False) pt.field_names = [ "Hexcode", "Cipher Suite Name (OpenSSL)", "Key Exch.", "Encryption", "Bits", "Cipher Suite Name (IANA/RFC)", "Security" ] pt.align = 'l' ciphers = self.allCiphers[key] for cipher in ciphers: try: elem = self.ciphers[cipher.cipher_suite.name] pt.add_row([ elem[1], elem[0], elem[2], elem[3], elem[4], cipher.cipher_suite.name, elem[5] ]) except Exception: print( f'Cipher {cipher.cipher_suite.name} not found in database' ) if len(ciphers) > 0: if not check: print(' VULNERABLE') check = True self.generateImageAndPrintInfo( f"Accepted cipher suites for {key} (server {self.host}):", pt, key, 0, 1 + len(str(pt).split('\n'))) if not check: print() def deprecatedSSL(self): keys = ["SSLv2", "SSLv3"] check = False print('Checking usage of deprecated SSL ...', end='', flush=True) for key in keys: pt = PrettyTable(border=False) pt.field_names = [ "Hexcode", "Cipher Suite Name (OpenSSL)", "Key Exch.", "Encryption", "Bits", "Cipher Suite Name (IANA/RFC)", "Security" ] pt.align = 'l' ciphers = self.allCiphers[key] for cipher in ciphers: try: elem = self.ciphers[cipher.cipher_suite.name] pt.add_row([ elem[1], elem[0], elem[2], elem[3], elem[4], cipher.cipher_suite.name, elem[5] ]) except Exception: print( f'Cipher {cipher.cipher_suite.name} not found in database' ) if len(ciphers) > 0: if not check: print(' VULNERABLE') check = True self.generateImageAndPrintInfo( f"Accepted cipher suites for {key} (server {self.host}):", pt, key, 0, 1 + len(str(pt).split('\n'))) if not check: print() def specificAlg(self, alg, protos, header, fileName): if protos != None: pr = ', '.join(protos) print(f'Checking {alg} in {pr} ...', end='', flush=True) else: print(f'Checking {alg} ...', end='', flush=True) pt = PrettyTable(border=False) pt.field_names = [ "Hexcode", "Cipher Suite Name (OpenSSL)", "Key Exch.", "Encryption", "Bits", "Cipher Suite Name (IANA/RFC)", "Security" ] pt.align = 'l' check = False #if no protocol is specified, check all if protos == None: protos = self.allCiphers.keys() for key in protos: pt.add_row([key, '', '', '', '', '', '']) for cipher in self.allCiphers[key]: if cipher.cipher_suite.name in self.ciphers.keys(): elem = self.ciphers[cipher.cipher_suite.name] if alg in cipher.cipher_suite.name: check = True pt.add_row([ elem[1], elem[0], elem[2], elem[3], elem[4], cipher.cipher_suite.name, elem[5] ]) if check: print(' VULNERABLE.') self.generateImageAndPrintInfo(f"{header} (server {self.host}):", pt, fileName, None, None) else: print() def drown(self): print('Checking DROWN ...', end='', flush=True) if len(self.allCiphers["SSLv2"]) > 0: print('ciphers in SSLv2') pt = PrettyTable(border=False) pt.field_names = [ "Hexcode", "Cipher Suite Name (OpenSSL)", "Key Exch.", "Encryption", "Bits", "Cipher Suite Name (IANA/RFC)", "Security" ] pt.align = 'l' for cipher in self.allCiphers["SSLv2"]: elem = self.ciphers[cipher.cipher_suite.name] pt.add_row([ elem[1], elem[0], elem[2], elem[3], elem[4], cipher.cipher_suite.name ], elem[5]) print(' VULNERABLE.') self.generateImageAndPrintInfo( f"Server is vulnerable to DROWN attacks because it supports SSLv2 (server {self.host}):", pt, 'DROWN', None, None) else: print() def logjamAndFreak(self): print('Checking LOGJAM and FREAK ...', end='', flush=True) ptL = PrettyTable(border=False) ptL.field_names = [ "Hexcode", "Cipher Suite Name (OpenSSL)", "Key Exch.", "Encryption", "Bits", "Cipher Suite Name (IANA/RFC)", "Key Size", "Key Ex. Type", "Security" ] ptL.align = 'l' ptF = PrettyTable(border=False) ptF.field_names = [ "Hexcode", "Cipher Suite Name (OpenSSL)", "Key Exch.", "Encryption", "Bits", "Cipher Suite Name (IANA/RFC)", "Key Size", "Key Ex. Type", "Security" ] ptF.align = 'l' logjam = False freak = False for key in ["TLSv1.0", "TLSv1.1", "TLSv1.2"]: ptL.add_row([key, '', '', '', '', '', '', '', '']) ptF.add_row([key, '', '', '', '', '', '', '', '']) for cipher in self.allCiphers[key]: if cipher.cipher_suite.name in self.ciphers.keys( ) and '_DHE_' in cipher.cipher_suite.name: elem = self.ciphers[cipher.cipher_suite.name] if cipher.ephemeral_key.size <= 1024: logjam = True ptL.add_row([ elem[1], elem[0], elem[2], elem[3], elem[4], cipher.cipher_suite.name, cipher.ephemeral_key.size, cipher.ephemeral_key.type, elem[5] ]) if cipher.ephemeral_key.size <= 512: freak = True ptF.add_row([ elem[1], elem[0], elem[2], elem[3], elem[4], cipher.cipher_suite.name, cipher.ephemeral_key.size, cipher.ephemeral_key.type, elem[5] ]) if freak: print(' VULNERABLE FOR BOTH.') self.generateImageAndPrintInfo( f"Server is vulnerable to FREAK attacks.\nIt supports Ephemeral Diffie-Hellman algorithms (EDH) with key sizes of 512 or lower (server {self.host}):", ptF, 'LOGJAM', None, None) elif logjam: print(' VULNERABLE FOR LOGJAM.') self.generateImageAndPrintInfo( f"Server is vulnerable to LOGJAM attacks.\nIt supports Ephemeral Diffie-Hellman algorithms (EDH) with key sizes of 1024 or lower (server {self.host}):", ptL, 'LOGJAM', None, None) else: print() def breach(self): print('Checking BREACH ...', end='', flush=True) s = requests.Session() headers = {"Host": self.host, "Accept-Encoding": "compress, gzip"} req = requests.Request('GET', "https://" + self.host + ':' + self.port, headers=headers) prepped = req.prepare() try: res = s.send(prepped, verify=False, allow_redirects=True, stream=True) except requests.exceptions.TooManyRedirects: print(' too many redirects.') return except requests.exceptions.ConnectionError: print(' connection error.') return if 'Content-Encoding' in res.headers.keys(): #May exist other values, havent found them yet if 'gzip' in res.headers['Content-Encoding']: request = '{}\n{}\r\n{}\r\n\r\n'.format( '-----------REQUEST-----------', prepped.method + ' ' + self.host + ':' + self.port, '\r\n'.join('{}: {}'.format(k, v) for k, v in prepped.headers.items())) response = '-----------RESPONSE-----------' for k, v in res.headers.items(): aux = '{}: {}'.format(k, v) mod = len(aux) % 80 i = int(len(aux) / 80) if i > 0: for counter in range(i): response += '\r\n' + aux[80 * counter:80 * counter + 80] if mod > 0: response += '\r\n' + aux[80 * counter + 80:] else: response += '\r\n' + '{}: {}'.format(k, v) resp = res.raw.read(80 * 4) response += '\r\n' for counter in range(4): response += '\r\n' + resp[80 * counter:80 * counter + 80].decode('latin-1') res.close() data = request + response for line in data.split('\n'): if 'Content-Encoding' in line: print(' VULNERABLE.') self.generateImageAndPrintInfo( f"Server is vulnerable to BREACH attacks.\nIt supports gzip compression in the HTTP responses (server {self.host}):", data, 'BREACH', data.split('\n').index(line) + 1, data.split('\n').index(line) + 1) break else: print() else: print() def crime(self): print('Checking CRIME ...', end='', flush=True) self.finishOpenSSL = threading.Event() self.output = '' p = Popen(os.getcwd() + '\\OpenSSL\\bin\\openssl.exe s_client -connect ' + self.host + ':' + self.port, stdin=PIPE, stdout=PIPE, stderr=PIPE) t = threading.Thread(target=self.outputReader, args=(p, 'Compression:')) t.start() start = timer() while not self.finishOpenSSL.is_set(): time.sleep(1) if (timer() - start) > 10: print(' TIMEOUT.') return p.terminate() # print() # print(self.output) # print() #Handle output to make image data = 'Command: openssl.exe s_client -connect ' + self.host + ':' + self.port + '\n\n' if '-----BEGIN CERTIFICATE-----' in data: data += self.output.split('-----BEGIN CERTIFICATE-----')[0] data += '[redacted]' data += self.output.split('-----END CERTIFICATE-----')[1] else: data += self.output for line in range(len(data)): if 'Compression:' in data[line] and 'NONE' not in data[line]: print(' VULNERABLE.') self.generateImageAndPrintInfo( f"Server is vulnerable to CRIME attacks.\nIt supports TLS-level compression (server {self.host}):", data, 'CRIME', line, line) return print() def secureRenegotiation(self): print('Checking SECURE RENEGOTIATION ...', end='', flush=True) self.finishOpenSSL = threading.Event() self.output = '' p = Popen(os.getcwd() + '\\OpenSSL\\bin\\openssl.exe s_client -connect ' + self.host + ':' + self.port, stdin=PIPE, stdout=PIPE, stderr=PIPE) t = threading.Thread(target=self.outputReader, args=(p, 'Secure Renegotiation')) t.start() start = timer() while not self.finishOpenSSL.is_set(): time.sleep(1) if (timer() - start) > 10: print(' TIMEOUT.') return p.terminate() #Handle output to make image data = 'Command: openssl.exe s_client -connect ' + self.host + ':' + self.port + '\n\n' if '-----BEGIN CERTIFICATE-----' in data: data += self.output.split('-----BEGIN CERTIFICATE-----')[0] data += '[redacted]' data += self.output.split('-----END CERTIFICATE-----')[1] else: data += self.output for line in range(len(data)): if 'Secure Renegotiation' in data[line] and 'IS NOT' in data[line]: print(' NOT SUPPORTED.') self.generateImageAndPrintInfo( f"Server does not support Secure Renegotiation (server {self.host}):", data, 'SECURE_RENEG', line, line) return print() def robot(self): #From testssl: A list of all non-PSK cipher suites that use RSA key transport nonPSK = [ "0x9d", "0xc0a1", "0xc09d", "0x3d", "0x35", "0xc0", "0x84", "0xc03d", "0xc051", "0xc07b", "0xff00", "0xff01", "0xff02", "0xff03", "0xc0a0", "0xc09c", "0x9c", "0x3c", "0x2f", "0xba", "0x96", "0x41", "0x07", "0xc03c", "0xc050", "0xc07a", "0x05", "0x04", "0x0a", "0xfeff", "0xffe0", "0x62", "0x09", "0x61", "0xfefe", "0xffe1", "0x64", "0x60", "0x08", "0x06", "0x03", "0x3b", "0x02", "0x01" ] check = False print('Checking ROBOT (this can take a while)...', end='', flush=True) results = self.initiateScan({ScanCommand.ROBOT}) for result in results: enumResult = result.scan_commands_results[ ScanCommand.ROBOT].robot_result.value if enumResult <= 2: pt = PrettyTable(border=False) pt.field_names = [ "Hexcode", "Cipher Suite Name (OpenSSL)", "Key Exch.", "Encryption", "Bits", "Cipher Suite Name (IANA/RFC)", "Security" ] pt.align = 'l' for key in self.allCiphers.keys(): ciphers = self.allCiphers[key] for cipher in ciphers: try: elem = self.ciphers[cipher.cipher_suite.name] if elem[1] in nonPSK: check = True print(elem) pt.add_row([ elem[1], elem[0], elem[2], elem[3], elem[4], cipher.cipher_suite.name, elem[5] ]) except Exception: print( f'Cipher {cipher.cipher_suite.name} not found in database' ) if check: if enumResult == 1: print(' POTENTIALLY VULNERABLE.') self.generateImageAndPrintInfo( f"Server is POTENTIALLY vulnerable to ROBOT attacks\nIt supports non-PSK cipher suites that use RSA key transport (server {self.host}):", pt, 'ROBOT', None, None) else: print(' VULNERABLE.') self.generateImageAndPrintInfo( f"Server is vulnerable to ROBOT attacks\nIt supports non-PSK cipher suites that use RSA key transport (server {self.host}):", pt, 'ROBOT', None, None) else: print() def TLSv1_3(self): print('Checking support of TLSv1.3 ...', end='', flush=True) if len(self.allCiphers["TLSv1.3"]) == 0: print(' NOT SUPPORTED.') self.generateImageAndPrintInfo( 'Server does not support TLSv1.3', 'The server does not support TLSv1.3 which is the only version of TLS\nthat currently has no known flaws or exploitable weaknesses.\n\nHighest supported protocol is ' + self.highestProtocol.replace('TLS_', 'TLSv').replace( 'SSL_', 'SSLv').replace('_', '.'), 'TLSv1.3NotSupported', None, None) else: print() def downgradePrevention(self): print('Checking DOWNGRADE PREVENTION ...', end='', flush=True) results = self.initiateScan({ScanCommand.TLS_FALLBACK_SCSV}) for result in results: checkPrint = False if not result.scan_commands_results[ ScanCommand.TLS_FALLBACK_SCSV].supports_fallback_scsv: protocolFlag = '-no_' #Check highest protocol to prevent its use in openssl if 'tls' in self.highestProtocol.lower( ) or 'ssl' in self.highestProtocol.lower(): protocolFlag += self.highestProtocol.lower().replace( 'tls_', 'tls').replace('ssl_', 'ssl').replace('_0', '') else: print( 'Potentially vulnerable to downgrade attack. Highest supported protocol is not TLS or SSL.' ) return self.finishOpenSSL = threading.Event() self.output = '' p = Popen(os.getcwd() + '\\OpenSSL\\bin\\openssl.exe s_client -connect ' + self.host + ':' + self.port + ' -fallback_scsv ' + protocolFlag, stdin=PIPE, stdout=PIPE, stderr=PIPE) t = threading.Thread(target=self.outputReader, args=(p, 'Master-Key')) t.start() start = timer() while not self.finishOpenSSL.is_set(): time.sleep(1) if (timer() - start) > 10: print(' TIMEOUT.') return p.terminate() #Handle output to make image data = 'Command: openssl.exe s_client -connect ' + self.host + ':' + self.port + ' -fallback_scsv ' + protocolFlag + '\n\n' if '-----BEGIN CERTIFICATE-----' in self.output: data += self.output.split('-----BEGIN CERTIFICATE-----')[0] data += '[redacted]' data += self.output.split('-----END CERTIFICATE-----')[1] else: data += self.output for line in range(len(data)): if 'New,' in data[line] and ', Cipher is ' in data[line]: print(' NOT SUPPORTED.') checkPrint = True self.generateImageAndPrintInfo( f"Downgrade prevention is not provided (server {self.host}):", data, 'downgradePrevention', line, line) break if not checkPrint: print() def OCSPStapling(self): print('Checking OCSP Stapling support ...', end='', flush=True) self.finishOpenSSL = threading.Event() self.output = '' p = Popen(os.getcwd() + '\\OpenSSL\\bin\\openssl.exe s_client -connect ' + self.host + ':' + self.port + ' -status', stdin=PIPE, stdout=PIPE, stderr=PIPE) t = threading.Thread(target=self.outputReader, args=(p, '-----BEGIN CERTIFICATE-----')) t.start() start = timer() while not self.finishOpenSSL.is_set(): time.sleep(1) if (timer() - start) > 10: print(' TIMEOUT.') return p.terminate() for line in self.output.split('\n'): if 'OCSP response: no response received' in line: print(' NOT SUPPORTED.') self.generateImageAndPrintInfo( f"OCSP Stapling not supported (server {self.host}):", '\n'.join( self.output.split('\n')[:self.output.split('\n').index( '-----BEGIN CERTIFICATE-----')]), 'OCSPStaplingNotSupported', self.output.split('\n').index(line), self.output.split('\n').index(line)) return print() def outputReader(self, proc, finish): for line in iter(proc.stdout.readline, b''): if finish in line.decode('utf-8'): self.finishOpenSSL.set() self.output += '{0}'.format(line.decode('utf-8')) def initiateScan(self, commands): self.scanner = Scanner() serverScanReq = ServerScanRequest( server_info=self.serverInfo, scan_commands=commands, ) self.scanner.queue_scan(serverScanReq) return self.scanner.get_results() def generateImageAndPrintInfo(self, prev, pt, imageName, startLine, endLine): data = '' self.printt('') self.printt(prev) data += prev + '\n' if len(prev.split('\n')) > 1: self.printt('-' * len(prev.split('\n')[-1])) data += '-' * len(prev.split('\n')[-1]) + '\n' else: self.printt('-' * len(prev)) data += '-' * len(prev) + '\n' #Delete first whitespace result of deleting borders if isinstance(pt, PrettyTable): table = str(pt).split('\n')[0][1:] + '\n' table += '-' * len(str(pt).split('\n')[0]) + '\n' for line in str(pt).split('\n')[1:]: table += line[1:] + '\n' self.printt(table) data += table else: self.printt(pt) data += pt self.text2png(data, self.imageFolder + '/' + imageName + '(' + self.host + '_' + self.port + ')_' + datetime.datetime.now().strftime("%d_%m_%Y_%H_%M") + '.png', startLine=startLine, endLine=endLine) def printt(self, text): if self.verbose: print(text) def text2png(self, text, fullpath, color="#000", bgcolor="#FFF", fontsize=30, padding=10, startLine=None, endLine=None): font = ImageFont.truetype("consola.ttf", fontsize) width = font.getsize(max(text.split('\n'), key=len))[0] + (padding * 2) lineHeight = font.getsize(text)[1] imgHeight = lineHeight * (len(text.split('\n')) + 1) + padding img = Image.new("RGBA", (width, imgHeight), bgcolor) draw = ImageDraw.Draw(img) y = padding #Draw the text for line in text.split('\n'): draw.text((padding, y), line, color, font=font) y += lineHeight #Draw the highlight rectangle, have to use line instead of rectangle because it does not support line THICCness if startLine != None and endLine != None and endLine >= startLine: #Add 2 to each bound because of the two heading lines if startLine == endLine: endLine += 1 startLine += 2 endLine += 2 point1 = (3, (padding / 2) + 3 + lineHeight * startLine) point2 = (3 + font.getsize(text.split('\n')[startLine])[0] + padding, (padding / 2) + 3 + lineHeight * startLine) point3 = (3 + font.getsize(text.split('\n')[startLine])[0] + padding, padding + 3 + lineHeight * (startLine + (endLine - startLine))) point4 = (3, padding + 3 + lineHeight * (startLine + (endLine - startLine))) draw.line((point1, point2, point3, point4, point1), fill="red", width=5) if not os.path.exists(self.imageFolder): os.makedirs(self.imageFolder) img.save(fullpath, quality=100)
def scan(target, ip, port, view, suite): """ Five inputs: web site name, ip, port split-dns view, and cipher suite """ server_location = ServerNetworkLocationViaDirectConnection( target, port, ip) # This line checks to see if the host is online try: server_info = ServerConnectivityTester().perform(server_location) except errors.ConnectionToServerTimedOut: raise ConnectionError("Connection Timeout", ERROR_MSG_CONNECTION_TIMEOUT(target, port)) except errors.ConnectionToServerFailed: raise ConnectionError("Unknown Connection Error", ERROR_MSG_UNKNOWN_CONNECTION(target, port)) # Create a new results dictionary scan_output = results.new_result_set() # I hash the combination of hostname and ip for tracking key = md5((target + ip).encode("utf-8")).hexdigest() results.set_result(scan_output, "MD5", key) results.set_result(scan_output, "Target", f"{target}:{port}") results.set_result(scan_output, "IP", f"{ip}:{port}") results.set_result(scan_output, "Scan", suite) results.set_result(scan_output, "View", view) scanner = Scanner() server_scan_req = ServerScanRequest(server_info=server_info, scan_commands=CIPHER_SUITES.get(suite)) scanner.queue_scan(server_scan_req) for result in scanner.get_results(): for cipher_suite in CIPHER_SUITES.get(suite): scan_result = result.scan_commands_results[cipher_suite] for accepted_cipher_suite in scan_result.accepted_cipher_suites: if suite == "policy" and scan_result.tls_version_used.name == "TLS_1_2": if (accepted_cipher_suite.cipher_suite.name not in ALLOWED_TLS12_CIPHERS): results.set_ciphers( scan_output, { "Version": f"{scan_result.tls_version_used.name}", "Cipher": f"{accepted_cipher_suite.cipher_suite.name}", }, ) else: results.set_ciphers( scan_output, { "Version": f"{scan_result.tls_version_used.name}", "Cipher": f"{accepted_cipher_suite.cipher_suite.name}", }, ) if len(scan_output["Results"]) == 0: results.set_result(scan_output, "Results", "No Policy Violations") return scan_output
def run(url): scan_result = {"name": __plugin__, "sequence": SEQUENCE, "result": []} error_result = {"name": __plugin__, "sequence": SEQUENCE, "result": []} error_result["result"] = [{ "name": "Error", "result": [{ "name": f"{__plugin__} can't scan this website" }] }] result_map = { "https": { "name": "Enabled HTTPS", "sequence": 0, "result": [] }, "effective": { "name": "Effective", "sequence": 1, "result": [] }, "subject": { "name": "Subject", "sequence": 2, "result": [] }, "issuer": { "name": "Issuer", "sequence": 3, "result": [] }, "public": { "name": "Public key algorithm", "sequence": 4, "result": [], }, "signature": { "name": "Signature hash algorithm", "sequence": 5, "result": [], }, "before": { "name": "Not valid before (UTC)", "sequence": 6, "result": [], }, "after": { "name": "Not valid after (UTC)", "sequence": 7, "result": [], }, "tls1_2": { "name": "Accepted TLS1.2 cipher suites", "sequence": 8, "result": [] }, "tls1_3": { "name": "Accepted TLS1.3 cipher suites", "sequence": 9, "result": [] }, "pfs": { "name": "Perfect Forward Secrecy (PFS)", "sequence": 10, "result": [], }, "ats": { "name": "App Transport Security (ATS)", "sequence": 11, "result": [], }, "tls1_3_early_data": { "name": "Support TLS1.3 early data", "sequence": 12, "result": [] }, "match": { "name": "Leaf certificate subject matches hostname", "sequence": 13, "result": [], }, "ocsp": { "name": "OCSP Must-Staple", "sequence": 14, "result": [], }, "fallback": { "name": "The TLS_FALLBACK_SCSV mechanism", "sequence": 15, "result": [] }, "ccs": { "name": "The OpenSSL CCS Injection vulnerability", "sequence": 16, "result": [] }, "heartbleed": { "name": "The Heartbleed vulnerability", "sequence": 17, "result": [] }, "crime": { "name": "The CRIME vulnerability", "sequence": 18, "result": [] }, "robot": { "name": "The ROBOT vulnerability", "sequence": 19, "result": [] }, } server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( url.netloc, 443) try: server_info = ServerConnectivityTester().perform(server_location) except ConnectionToServerFailed as e: return error_result scanner = Scanner() server_scan_req = ServerScanRequest( server_info, { ScanCommand.CERTIFICATE_INFO, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_3_EARLY_DATA, ScanCommand.TLS_FALLBACK_SCSV, ScanCommand.OPENSSL_CCS_INJECTION, ScanCommand.HEARTBLEED, ScanCommand.TLS_COMPRESSION, ScanCommand.ROBOT, }, ) scanner.queue_scan(server_scan_req) for server_scan_result in scanner.get_results(): certificate_result = server_scan_result.scan_commands_results[ ScanCommand.CERTIFICATE_INFO] tls_1_2_cipher_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_2_CIPHER_SUITES] tls_1_3_cipher_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_3_CIPHER_SUITES] tls_1_3_early_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_3_EARLY_DATA] tls_fallback_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_FALLBACK_SCSV] ccs_result = server_scan_result.scan_commands_results[ ScanCommand.OPENSSL_CCS_INJECTION] heartbleed_result = server_scan_result.scan_commands_results[ ScanCommand.HEARTBLEED] crime_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_COMPRESSION] robot_result = server_scan_result.scan_commands_results[ ScanCommand.ROBOT] for certificate_deployment in certificate_result.certificate_deployments: for certificate_info in certificate_deployment.received_certificate_chain: result_map["subject"]["result"] = [{ "name": certificate_info.subject.rfc4514_string() }] result_map["issuer"]["result"] = [{ "name": certificate_info.issuer.rfc4514_string() }] public_key = certificate_info.public_key() public_key_name = type(public_key).__name__[1:][:-9] if "key_size" in dir(public_key): result_map["public"]["result"] = [{ "name": f"{public_key_name}{certificate_info.public_key().key_size}" }] else: result_map["public"]["result"] = [{"name": public_key_name}] result_map["signature"]["result"] = [{ "name": certificate_info.signature_hash_algorithm.name.upper() }] if datetime.now( ) > certificate_info.not_valid_before and datetime.now( ) < certificate_info.not_valid_after: result_map["effective"]["result"] = True result_map["before"]["result"] = [{ "name": datetime.strftime(certificate_info.not_valid_before, "%Y-%m-%d %H:%M:%S") }] result_map["after"]["result"] = [{ "name": datetime.strftime(certificate_info.not_valid_after, "%Y-%m-%d %H:%M:%S") }] break result_map["https"]["result"] = True result_map["match"][ "result"] = certificate_deployment.leaf_certificate_subject_matches_hostname result_map["ocsp"][ "result"] = certificate_deployment.leaf_certificate_has_must_staple_extension break tls_1_2_cipher_list = [ accepted_cipher_suite.cipher_suite.name for accepted_cipher_suite in tls_1_2_cipher_result.accepted_cipher_suites ] tls_1_3_cipher_list = [ accepted_cipher_suite.cipher_suite.name for accepted_cipher_suite in tls_1_3_cipher_result.accepted_cipher_suites ] result_map["tls1_2"]["result"] = [{ "name": tls_1_2_cipher } for tls_1_2_cipher in tls_1_2_cipher_list ] if tls_1_2_cipher_list else False result_map["tls1_3"]["result"] = [{ "name": tls_1_3_cipher } for tls_1_3_cipher in tls_1_3_cipher_list ] if tls_1_3_cipher_list else False cipher_list = tls_1_2_cipher_list + tls_1_3_cipher_list for cipher in cipher_list: if "DHE" in cipher: result_map["pfs"]["result"] = True if set(cipher_list).intersection(ATS_CIPHER_SET): result_map["ats"]["result"] = True result_map["tls1_3_early_data"][ "result"] = tls_1_3_early_result.supports_early_data result_map["fallback"][ "result"] = tls_fallback_result.supports_fallback_scsv result_map["ccs"]["result"] = not ccs_result.is_vulnerable_to_ccs_injection result_map["heartbleed"][ "result"] = not heartbleed_result.is_vulnerable_to_heartbleed result_map["crime"]["result"] = not crime_result.supports_compression result_map["robot"]["result"] = [{"name": robot_result.robot_result.name}] scan_result["result"] = sorted([item for item in result_map.values()], key=lambda x: x.get("sequence", 0)) return scan_result
def main(server_software_running_on_localhost: WebServerSoftwareEnum) -> None: # Ensure the server is accessible on localhost server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( "localhost", 443) server_info = ServerConnectivityTester().perform(server_location) if server_software_running_on_localhost == WebServerSoftwareEnum.APACHE2: # Apache2 is configured to require a client cert, and returns an error at the TLS layer if it is missing if server_info.tls_probing_result.client_auth_requirement != ClientAuthRequirementEnum.REQUIRED: raise RuntimeError( f"SSLyze did not detect that client authentication was required by Apache2:" f" {server_info.tls_probing_result.client_auth_requirement}.") elif server_software_running_on_localhost == WebServerSoftwareEnum.NGINX: # Nginx is configured to require a client cert but implements this by returning an error at the HTTP layer, # if the client cert is missing. This gets translated in SSLyze as "optionally" requiring a client cert if server_info.tls_probing_result.client_auth_requirement != ClientAuthRequirementEnum.OPTIONAL: raise RuntimeError( f"SSLyze did not detect that client authentication was required by Nginx:" f" {server_info.tls_probing_result.client_auth_requirement}.") elif server_software_running_on_localhost == WebServerSoftwareEnum.IIS: # IIS is not configured to require a client cert for now because I don't know how to enable this if server_info.tls_probing_result.client_auth_requirement != ClientAuthRequirementEnum.DISABLED: raise RuntimeError( f"SSLyze detected that client authentication was enabled by IIS:" f" {server_info.tls_probing_result.client_auth_requirement}.") else: raise ValueError( f"Unexpected value: {server_software_running_on_localhost}") # Queue all scan commands print("Starting scan.") scanner = Scanner() server_scan_req = ServerScanRequest( server_info=server_info, scan_commands=ScanCommandsRepository.get_all_scan_commands(), ) scanner.queue_scan(server_scan_req) # Retrieve the result for server_scan_result in scanner.get_results(): successful_cmds_count = len(server_scan_result.scan_commands_results) errored_cmds_count = len(server_scan_result.scan_commands_errors) print( f"Finished scan with {successful_cmds_count} results and {errored_cmds_count} errors." ) # Crash if any scan commands triggered an error that's not due to client authentication being required triggered_unexpected_error = False for scan_command, error in server_scan_result.scan_commands_errors.items( ): if error.reason != ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED: triggered_unexpected_error = True print( f"\nError when running {scan_command}: {error.reason.name}." ) if error.exception_trace: exc_trace = "" for line in error.exception_trace.format(chain=False): exc_trace += f" {line}" print(exc_trace) print("\n") if triggered_unexpected_error: raise RuntimeError("The scan triggered unexpected errors") else: # The CLIENT_CERTIFICATE_NEEDED errors are expected, because of how Apache2 is configured print("OK: Triggered CLIENT_CERTIFICATE_NEEDED errors only.") # Crash if SSLyze didn't complete the scan commands that are supposed to work even when we don't provide a # client certificate if server_software_running_on_localhost == WebServerSoftwareEnum.APACHE2: expected_scan_command_results = { ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, ScanCommand.SSL_3_0_CIPHER_SUITES, ScanCommand.SSL_2_0_CIPHER_SUITES, ScanCommand.OPENSSL_CCS_INJECTION, ScanCommand.HEARTBLEED, ScanCommand.ELLIPTIC_CURVES, ScanCommand.TLS_FALLBACK_SCSV, ScanCommand.CERTIFICATE_INFO, ScanCommand.TLS_COMPRESSION, } elif server_software_running_on_localhost == WebServerSoftwareEnum.NGINX: # With nginx, when configured to require client authentication, more scan commands work because unlike # Apache2, it does complete a full TLS handshake even when a client cert was not provided. It then returns # an error page at the HTTP layer. expected_scan_command_results = { ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, ScanCommand.SSL_3_0_CIPHER_SUITES, ScanCommand.SSL_2_0_CIPHER_SUITES, ScanCommand.OPENSSL_CCS_INJECTION, ScanCommand.HEARTBLEED, ScanCommand.ELLIPTIC_CURVES, ScanCommand.TLS_FALLBACK_SCSV, ScanCommand.CERTIFICATE_INFO, ScanCommand.TLS_COMPRESSION, ScanCommand.SESSION_RESUMPTION, ScanCommand.TLS_1_3_EARLY_DATA, ScanCommand.HTTP_HEADERS, ScanCommand.SESSION_RESUMPTION_RATE, ScanCommand.SESSION_RENEGOTIATION, } elif server_software_running_on_localhost == WebServerSoftwareEnum.IIS: # With IIS, client authentication is not enabled so all scan commands should succeed expected_scan_command_results = ScanCommandsRepository.get_all_scan_commands( ) # type: ignore else: raise ValueError( f"Unexpected value: {server_software_running_on_localhost}") completed_scan_command_results = server_scan_result.scan_commands_results.keys( ) if completed_scan_command_results != expected_scan_command_results: raise RuntimeError( f"SSLyze did not complete all the expected scan commands: {completed_scan_command_results}" ) else: print("OK: Completed all the expected scan commands.") # Ensure the right TLS versions were detected by SSLyze as enabled # https://github.com/nabla-c0d3/sslyze/issues/472 if server_software_running_on_localhost in [ WebServerSoftwareEnum.APACHE2, WebServerSoftwareEnum.NGINX ]: # Apache and nginx are configured to only enable TLS 1.2 and TLS 1.3 expected_enabled_tls_scan_commands = { ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, } elif server_software_running_on_localhost == WebServerSoftwareEnum.IIS: # TLS 1.3 is not supported by IIS expected_enabled_tls_scan_commands = { ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, } else: raise ValueError( f"Unexpected value: {server_software_running_on_localhost}") for ciphers_scan_cmd in expected_enabled_tls_scan_commands: scan_cmd_result = server_scan_result.scan_commands_results[ ciphers_scan_cmd] # type: ignore if not scan_cmd_result.accepted_cipher_suites: raise RuntimeError( f"SSLyze did not detect {scan_cmd_result.tls_version_used.name} to be enabled on the server." ) else: print( f"OK: Scan command {ciphers_scan_cmd} detected cipher suites." ) # Ensure a JSON output can be generated from the results json_output = _SslyzeOutputAsJson( server_scan_results=[server_scan_result], server_connectivity_errors=[], total_scan_time=3, ) json_output_as_dict = asdict(json_output) json.dumps(json_output_as_dict, cls=JsonEncoder, sort_keys=True, indent=4, ensure_ascii=True) print("OK: Was able to generate JSON output.")
def ssl_scan(self, target): print("Running SSL Scan") # Define the server that you want to scan server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( target, 443) try: # Do connectivity testing to ensure SSLyze is able to connect try: server_info = ServerConnectivityTester().perform(server_location) except ConnectionToServerFailed as e: # Could not connect to the server; abort print(f"Error connecting to {server_location}: {e.error_message}") return # Then queue some scan commands for the server scanner = Scanner() server_scan_req = ServerScanRequest(server_info=server_info, scan_commands={ ScanCommand.CERTIFICATE_INFO, ScanCommand.SSL_2_0_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.HEARTBLEED, ScanCommand.HTTP_HEADERS}, ) scanner.queue_scan(server_scan_req) # Then retrieve the results for server_scan_result in scanner.get_results(): print( f"\nResults for {server_scan_result.server_info.server_location.hostname}:") heartbleed_vuln = server_scan_result.scan_commands_results[ ScanCommand.HEARTBLEED].is_vulnerable_to_heartbleed print(f"\nIs vulnerable to heartbleed? {heartbleed_vuln}") print("\nAccepted cipher suites for TLS 1.0:") for accepted_cipher_suite in server_scan_result.scan_commands_results[ ScanCommand.TLS_1_0_CIPHER_SUITES].accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") print("\nAccepted cipher suites for TLS 1.1:") for accepted_cipher_suite in server_scan_result.scan_commands_results[ ScanCommand.TLS_1_1_CIPHER_SUITES].accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") print("\nAccepted cipher suites for TLS 1.2:") for accepted_cipher_suite in server_scan_result.scan_commands_results[ ScanCommand.TLS_1_2_CIPHER_SUITES].accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") print("\nAccepted cipher suites for TLS 1.3:") for accepted_cipher_suite in server_scan_result.scan_commands_results[ ScanCommand.TLS_1_3_CIPHER_SUITES].accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") # SSL 2.0 results ssl2_result = server_scan_result.scan_commands_results[ ScanCommand.SSL_2_0_CIPHER_SUITES] print("\nAccepted cipher suites for SSL 2.0:") for accepted_cipher_suite in ssl2_result.accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") # Certificate info results certinfo_result = server_scan_result.scan_commands_results[ ScanCommand.CERTIFICATE_INFO] print("\nCertificate info:") for cert_deployment in certinfo_result.certificate_deployments: print( f"Leaf certificate: \n{cert_deployment.received_certificate_chain_as_pem[0]}") except Exception as e: self.handle_exception(e, "Error running SSL scan") pass
def main() -> None: # Ensure the server is accessible on localhost server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("localhost", 443) server_info = ServerConnectivityTester().perform(server_location) if server_info.tls_probing_result.client_auth_requirement != ClientAuthRequirementEnum.REQUIRED: raise RuntimeError( f"SSLyze did not detect that client authentication was required by the server:" f" {server_info.tls_probing_result.client_auth_requirement}." ) # Queue all scan commands print("Starting scan.") scanner = Scanner() server_scan_req = ServerScanRequest( server_info=server_info, scan_commands=ScanCommandsRepository.get_all_scan_commands(), ) scanner.queue_scan(server_scan_req) # Retrieve the result for server_scan_result in scanner.get_results(): successful_cmds_count = len(server_scan_result.scan_commands_results) errored_cmds_count = len(server_scan_result.scan_commands_errors) print(f"Finished scan with {successful_cmds_count} results and {errored_cmds_count} errors.") # Crash if any scan commands triggered an error that's not due to client authentication being required triggered_unexpected_error = False for scan_command, error in server_scan_result.scan_commands_errors.items(): if error.reason != ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED: triggered_unexpected_error = True print(f"\nError when running {scan_command}: {error.reason.name}.") if error.exception_trace: exc_trace = "" for line in error.exception_trace.format(chain=False): exc_trace += f" {line}" print(exc_trace) print("\n") if triggered_unexpected_error: raise RuntimeError("The scan triggered unexpected errors") else: # The CLIENT_CERTIFICATE_NEEDED errors are expected, because of how Apache2 is configured print("OK: Triggered CLIENT_CERTIFICATE_NEEDED errors only.") # Crash if SSLyze didn't complete the scan commands that are supposed to work even when we don't provide a # client certificate expected_scan_command_results = { ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, ScanCommand.SSL_3_0_CIPHER_SUITES, ScanCommand.SSL_2_0_CIPHER_SUITES, ScanCommand.OPENSSL_CCS_INJECTION, ScanCommand.HEARTBLEED, ScanCommand.ELLIPTIC_CURVES, ScanCommand.TLS_FALLBACK_SCSV, ScanCommand.CERTIFICATE_INFO, ScanCommand.TLS_COMPRESSION, } if server_scan_result.scan_commands_results.keys() != expected_scan_command_results: raise RuntimeError("SSLyze did not complete all the expected scan commands.") else: print("OK: Completed all the expected scan commands.") # Ensure TLS 1.2 and 1.3 were detected by SSLyze to be enabled # https://github.com/nabla-c0d3/sslyze/issues/472 for ciphers_scan_cmd in [ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES]: scan_cmd_result = server_scan_result.scan_commands_results[ciphers_scan_cmd] # type: ignore if not scan_cmd_result.accepted_cipher_suites: raise RuntimeError( f"SSLyze did not detect {scan_cmd_result.tls_version_used.name} to be enabled on the server." ) else: print(f"OK: Scan command {ciphers_scan_cmd} detected cipher suites.")
def scan_runner(seq, host): hostname = host.decode("utf-8") servers_to_scan = [] server_location = None try: if r.hget(seq, "ipaddr"): server_location = ServerNetworkLocationViaDirectConnection( hostname, 443, r.hget(seq, "ipaddr").decode("utf-8")) else: server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( hostname, 443) r.hset(seq, "ipaddr", server_location.ip_address) #Initialize with hostname, port int and ip address str #print(server_location) except Exception as e: print(e) r.hset(seq, "STATUS", 2) try: server_info = ServerConnectivityTester().perform(server_location) servers_to_scan.append(server_info) except ConnectionToServerFailed as e: if 'Probing failed' in str(e): r.hset(seq, "STATUS", 31) else: r.hset(seq, "STATUS", 32) return scanner = Scanner() # Then queue some scan commands for each server for server_info in servers_to_scan: server_scan_req = ServerScanRequest( server_info=server_info, scan_commands={ ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES }, ) scanner.queue_scan(server_scan_req) # Then retrieve the result of the scan commands for each server for server_scan_result in scanner.get_results(): try: tls1_3_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_3_CIPHER_SUITES] cipherstr = "" if tls1_3_result.accepted_cipher_suites: for accepted_cipher_suite in tls1_3_result.accepted_cipher_suites: cipherstr = cipherstr + str( accepted_cipher_suite.cipher_suite.name) + " " r.hset(seq, "TLS1_3", cipherstr) tls1_2_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_2_CIPHER_SUITES] cipherstr = "" if tls1_2_result.accepted_cipher_suites: for accepted_cipher_suite in tls1_2_result.accepted_cipher_suites: cipherstr = cipherstr + str( accepted_cipher_suite.cipher_suite.name) + " " r.hset(seq, "TLS1_2", cipherstr) tls1_1_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_1_CIPHER_SUITES] cipherstr = "" if tls1_1_result.accepted_cipher_suites: for accepted_cipher_suite in tls1_1_result.accepted_cipher_suites: cipherstr = cipherstr + str( accepted_cipher_suite.cipher_suite.name) + " " r.hset(seq, "TLS1_1", cipherstr) tls1_0_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_0_CIPHER_SUITES] cipherstr = "" if tls1_0_result.accepted_cipher_suites: for accepted_cipher_suite in tls1_0_result.accepted_cipher_suites: cipherstr = cipherstr + str( accepted_cipher_suite.cipher_suite.name) + " " r.hset(seq, "TLS1_0", cipherstr) r.hset(seq, "STATUS", 1) except KeyError: r.hset(seq, "STATUS", 4) # Scan commands that were run with errors for scan_command, error in server_scan_result.scan_commands_errors.items( ): r.hset(seq, "STATUS", 5)
def analyze(hostname: str, port: int) -> List[Tuple[int, str]]: results = [] # Define the server that you want to scan try: server_location = ServerNetworkLocation(hostname, port) except ServerHostnameCouldNotBeResolved: log_red(_("Could not resolve {0}"), hostname) return results # Then queue some scan commands for the server scanner = Scanner() server_scan_req = ServerScanRequest( server_location=server_location, scan_commands={ ScanCommand.CERTIFICATE_INFO, ScanCommand.SSL_2_0_CIPHER_SUITES, ScanCommand.SSL_3_0_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.ROBOT, ScanCommand.HEARTBLEED, ScanCommand.TLS_COMPRESSION, ScanCommand.TLS_FALLBACK_SCSV, ScanCommand.TLS_1_3_EARLY_DATA, ScanCommand.OPENSSL_CCS_INJECTION, ScanCommand.SESSION_RENEGOTIATION, ScanCommand.HTTP_HEADERS }, network_configuration=ServerNetworkConfiguration( tls_server_name_indication=server_location.hostname, network_timeout=5, network_max_retries=2)) scanner.queue_scans([server_scan_req]) # TLS 1.2 / 1.3 results good_protocols = { ScanCommand.TLS_1_2_CIPHER_SUITES: "TLS v1.2", ScanCommand.TLS_1_3_CIPHER_SUITES: "TLS v1.3" } # https://blog.mozilla.org/security/2014/10/14/the-poodle-attack-and-the-end-of-ssl-3-0/ # https://blog.qualys.com/product-tech/2018/11/19/grade-change-for-tls-1-0-and-tls-1-1-protocols bad_protocols = { ScanCommand.SSL_2_0_CIPHER_SUITES: "SSL v2", ScanCommand.SSL_3_0_CIPHER_SUITES: "SSL v3", ScanCommand.TLS_1_0_CIPHER_SUITES: "TLS v1.0", ScanCommand.TLS_1_1_CIPHER_SUITES: "TLS v1.1" } # Then retrieve the results for result in scanner.get_results(): log_blue("\n" + _("Results for") + f" {result.server_location.hostname}:") deprecated_protocols = [] if result.connectivity_error_trace: # Stuff like connection timeout log_red(result.connectivity_error_trace) continue for scan_command in result.scan_result.__annotations__: scan_results = getattr(result.scan_result, scan_command) if scan_results.error_reason: log_red(scan_results.error_reason) continue if scan_results.status != ScanCommandAttemptStatusEnum.COMPLETED: continue if scan_command == ScanCommand.CERTIFICATE_INFO: for level, message in process_certificate_info( scan_results.result): results.append((level, message)) elif scan_command in bad_protocols: if scan_results.result.accepted_cipher_suites: deprecated_protocols.append(bad_protocols[scan_command]) elif scan_command == ScanCommand.ROBOT: if scan_results.result.robot_result in ( RobotScanResultEnum.VULNERABLE_WEAK_ORACLE, RobotScanResultEnum.VULNERABLE_STRONG_ORACLE): message = _("Server is vulnerable to ROBOT attack") log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.HEARTBLEED: if scan_results.result.is_vulnerable_to_heartbleed: message = _("Server is vulnerable to Heartbleed attack") log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.TLS_COMPRESSION: if scan_results.result.supports_compression: message = _( "Server is vulnerable to CRIME attack (compression is supported)" ) log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.TLS_FALLBACK_SCSV: if not scan_results.result.supports_fallback_scsv: message = _( "Server is vulnerable to downgrade attacks (support for TLS_FALLBACK_SCSV is missing)" ) log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.TLS_1_3_EARLY_DATA: # https://blog.trailofbits.com/2019/03/25/what-application-developers-need-to-know-about-tls-early-data-0rtt/ if scan_results.result.supports_early_data: message = _( "TLS 1.3 Early Data (0RTT) is vulnerable to replay attacks" ) log_orange(message) results.append((MEDIUM_LEVEL, message)) elif scan_command == ScanCommand.OPENSSL_CCS_INJECTION: if scan_results.result.is_vulnerable_to_ccs_injection: message = _( "Server is vulnerable to OpenSSL CCS (CVE-2014-0224)") log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.SESSION_RENEGOTIATION: if scan_results.result.is_vulnerable_to_client_renegotiation_dos: message = _( "Server honors client-initiated renegotiations (vulnerable to DoS attacks)" ) log_red(message) results.append((HIGH_LEVEL, message)) if not scan_results.result.supports_secure_renegotiation: message = _("Server doesn't support secure renegotiations") log_orange(message) results.append((MEDIUM_LEVEL, message)) elif scan_command == ScanCommand.HTTP_HEADERS: if scan_results.result.strict_transport_security_header is None: message = _("Strict Transport Security (HSTS) is not set") log_red(message) results.append((HIGH_LEVEL, message)) elif scan_command in good_protocols: for level, message in process_cipher_suites( scan_results.result, good_protocols[scan_command]): results.append((level, message)) if deprecated_protocols: message = _("The following protocols are deprecated and/or insecure and should be deactivated:") + \ " " + ", ".join(deprecated_protocols) log_red(message) results.append((CRITICAL_LEVEL, message)) return results
def main(server_software_running_on_localhost: WebServerSoftwareEnum) -> None: # Queue all scan commands against a server running on localhost print("Starting scan.") date_scans_started = datetime.utcnow() scanner = Scanner() scanner.queue_scans([ ServerScanRequest( server_location=ServerNetworkLocation("localhost", 443)) ]) # Retrieve the result for server_scan_result in scanner.get_results(): # First validate the connectivity testing assert server_scan_result.connectivity_status == ServerConnectivityStatusEnum.COMPLETED assert server_scan_result.connectivity_result if server_software_running_on_localhost == WebServerSoftwareEnum.APACHE2: # Apache2 is configured to require a client cert, and returns an error at the TLS layer if it is missing if server_scan_result.connectivity_result.client_auth_requirement != ClientAuthRequirementEnum.REQUIRED: raise RuntimeError( f"SSLyze did not detect that client authentication was required by Apache2:" f" {server_scan_result.connectivity_result.client_auth_requirement}." ) elif server_software_running_on_localhost == WebServerSoftwareEnum.NGINX: # Nginx is configured to require a client cert but implements this by returning an error at the HTTP layer, # if the client cert is missing. This gets translated in SSLyze as "optionally" requiring a client cert if server_scan_result.connectivity_result.client_auth_requirement != ClientAuthRequirementEnum.OPTIONAL: raise RuntimeError( f"SSLyze did not detect that client authentication was required by Nginx:" f" {server_scan_result.connectivity_result.client_auth_requirement}." ) elif server_software_running_on_localhost == WebServerSoftwareEnum.IIS: # IIS is not configured to require a client cert for now because I don't know how to enable this if server_scan_result.connectivity_result.client_auth_requirement != ClientAuthRequirementEnum.DISABLED: raise RuntimeError( f"SSLyze detected that client authentication was enabled by IIS:" f" {server_scan_result.connectivity_result.client_auth_requirement}." ) else: raise ValueError( f"Unexpected value: {server_software_running_on_localhost}") successful_cmds = set() triggered_unexpected_error = False for scan_command in ScanCommandsRepository.get_all_scan_commands(): scan_cmd_attempt = getattr(server_scan_result.scan_result, scan_command.value) if scan_cmd_attempt.status == ScanCommandAttemptStatusEnum.COMPLETED: successful_cmds.add(scan_command) elif scan_cmd_attempt.status == ScanCommandAttemptStatusEnum.ERROR: # Crash if any scan commands triggered an error that's not due to client authentication being required if scan_cmd_attempt.error_reason != ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED: triggered_unexpected_error = True print( f"\nError when running {scan_command}: {scan_cmd_attempt.error_reason}." ) if scan_cmd_attempt.error_trace: exc_trace = "" for line in scan_cmd_attempt.error_trace.format( chain=False): exc_trace += f" {line}" print(exc_trace) print(f"Finished scan with {len(successful_cmds)} results.") if triggered_unexpected_error: raise RuntimeError("The scan triggered unexpected errors") else: # The CLIENT_CERTIFICATE_NEEDED errors are expected, because of how Apache2 is configured print("OK: Triggered CLIENT_CERTIFICATE_NEEDED errors only.") # Crash if SSLyze didn't complete the scan commands that are supposed to work even when we don't provide a # client certificate if server_software_running_on_localhost == WebServerSoftwareEnum.APACHE2: expected_scan_cmds_to_succeed = { ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, ScanCommand.SSL_3_0_CIPHER_SUITES, ScanCommand.SSL_2_0_CIPHER_SUITES, ScanCommand.OPENSSL_CCS_INJECTION, ScanCommand.HEARTBLEED, ScanCommand.ELLIPTIC_CURVES, ScanCommand.TLS_FALLBACK_SCSV, ScanCommand.CERTIFICATE_INFO, ScanCommand.TLS_COMPRESSION, } elif server_software_running_on_localhost == WebServerSoftwareEnum.NGINX: # With nginx, when configured to require client authentication, more scan commands work because unlike # Apache2, it does complete a full TLS handshake even when a client cert was not provided. It then returns # an error page at the HTTP layer. expected_scan_cmds_to_succeed = { ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, ScanCommand.SSL_3_0_CIPHER_SUITES, ScanCommand.SSL_2_0_CIPHER_SUITES, ScanCommand.OPENSSL_CCS_INJECTION, ScanCommand.HEARTBLEED, ScanCommand.ELLIPTIC_CURVES, ScanCommand.TLS_FALLBACK_SCSV, ScanCommand.CERTIFICATE_INFO, ScanCommand.TLS_COMPRESSION, ScanCommand.SESSION_RESUMPTION, ScanCommand.TLS_1_3_EARLY_DATA, ScanCommand.HTTP_HEADERS, ScanCommand.SESSION_RENEGOTIATION, } elif server_software_running_on_localhost == WebServerSoftwareEnum.IIS: # With IIS, client authentication is not enabled so all scan commands should succeed expected_scan_cmds_to_succeed = ScanCommandsRepository.get_all_scan_commands( ) # type: ignore else: raise ValueError( f"Unexpected value: {server_software_running_on_localhost}") missing_scan_cmds = expected_scan_cmds_to_succeed.difference( successful_cmds) if missing_scan_cmds: raise RuntimeError( f"SSLyze did not complete all the expected scan commands: {missing_scan_cmds}" ) print("OK: Completed all the expected scan commands.") # Ensure the right TLS versions were detected by SSLyze as enabled # https://github.com/nabla-c0d3/sslyze/issues/472 if server_software_running_on_localhost in [ WebServerSoftwareEnum.APACHE2, WebServerSoftwareEnum.NGINX ]: # Apache and nginx are configured to only enable TLS 1.2 and TLS 1.3 expected_enabled_tls_scan_commands = { ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, } elif server_software_running_on_localhost == WebServerSoftwareEnum.IIS: # TLS 1.3 is not supported by IIS expected_enabled_tls_scan_commands = { ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, } else: raise ValueError( f"Unexpected value: {server_software_running_on_localhost}") for ciphers_scan_cmd in expected_enabled_tls_scan_commands: scan_cmd_attempt = getattr(server_scan_result.scan_result, ciphers_scan_cmd, None) scan_cmd_result = scan_cmd_attempt.result if not scan_cmd_result.accepted_cipher_suites: raise RuntimeError( f"SSLyze did not detect {scan_cmd_result.tls_version_used.name} to be enabled on the server." ) else: print( f"OK: Scan command {ciphers_scan_cmd} detected cipher suites." ) # Ensure a JSON output can be generated from the results final_json_output = SslyzeOutputAsJson( server_scan_results=[ ServerScanResultAsJson.from_orm(server_scan_result) ], date_scans_started=date_scans_started, date_scans_completed=datetime.utcnow(), ) final_json_output.json(sort_keys=True, indent=4, ensure_ascii=True) print("OK: Was able to generate JSON output.")