Example #1
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}"
            )
Example #2
0
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]}")
Example #3
0
    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
Example #4
0
    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
Example #5
0
    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
Example #6
0
    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 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)
Example #8
0
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
Example #9
0
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)
Example #10
0
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 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
Example #12
0
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
Example #13
0
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})"
                )
Example #14
0
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
Example #15
0
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)
Example #16
0
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
Example #17
0
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.")
Example #18
0
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.")
Example #19
0
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)
Example #20
0
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.")
Example #21
0
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)
Example #22
0
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