Ejemplo n.º 1
0
    def get_supported_tls(self, highest_supported):

        supported = [highest_supported]

        for version, method in {
                "SSL_2_0": TlsVersionEnum.SSLV2,
                "SSL_3_0": TlsVersionEnum.SSLV3,
                "TLS_1_0": TlsVersionEnum.TLSV1,
                "TLS_1_1": TlsVersionEnum.TLSV1_1,
                "TLS_1_2": TlsVersionEnum.TLSV1_2,
        }.items():

            # Only test SSL/TLS connections with lesser versions
            if highest_supported == version:
                break

            try:
                # Attempt connection
                # If connection fails, exception will be raised, causing the failure to be
                # logged and the version to not be appended to the supported list
                ctx = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup(
                    self.domain, 443)
                cfg = ServerNetworkConfiguration(self.domain)
                connx = SslConnection(ctx, cfg, method, True)
                connx.connect(self.domain)
                supported.append(version)
            except Exception as e:
                logging.info(
                    f"Failed to connect using %{version}: ({type(e)}) - {e}")

        return supported
Ejemplo n.º 2
0
    def get_preconfigured_tls_connection(
        self,
        override_tls_version: Optional[TlsVersionEnum] = None,
        ca_certificates_path: Optional[Path] = None,
        should_use_legacy_openssl: Optional[bool] = None,
        should_enable_server_name_indication: bool = True,
    ) -> SslConnection:
        """Get an SSLConnection instance with the right SSL configuration for successfully connecting to the server.

        Used by all plugins to connect to the server and run scans.
        """
        final_ssl_version = self.tls_probing_result.highest_tls_version_supported
        final_openssl_cipher_string: Optional[str]
        final_openssl_cipher_string = self.tls_probing_result.cipher_suite_supported
        if override_tls_version is not None:
            # Caller wants to override the TLS version to use for this connection
            final_ssl_version = override_tls_version
            # Then we don't know which cipher suite is supported by the server for this ssl version
            final_openssl_cipher_string = None

        if should_use_legacy_openssl is not None:
            final_openssl_cipher_string = None

        if self.network_configuration.tls_client_auth_credentials is not None:
            # If we have creds for client authentication, go ahead and use them
            should_ignore_client_auth = False
        else:
            # Ignore client auth requests if the server allows optional TLS client authentication
            should_ignore_client_auth = True
            # But do not ignore them is client authentication is required so that the right exceptions get thrown
            # within the plugins, providing a better output
            if self.tls_probing_result.client_auth_requirement == ClientAuthRequirementEnum.REQUIRED:
                should_ignore_client_auth = False

        ssl_connection = SslConnection(
            server_location=self.server_location,
            network_configuration=self.network_configuration,
            tls_version=final_ssl_version,
            should_ignore_client_auth=should_ignore_client_auth,
            ca_certificates_path=ca_certificates_path,
            should_use_legacy_openssl=should_use_legacy_openssl,
            should_enable_server_name_indication=
            should_enable_server_name_indication,
        )
        if final_openssl_cipher_string:
            if final_ssl_version == TlsVersionEnum.TLS_1_3:
                # OpenSSL uses a different API for TLS 1.3
                if not isinstance(ssl_connection.ssl_client, SslClient):
                    raise RuntimeError("Should never happen")
                ssl_connection.ssl_client.set_ciphersuites(
                    final_openssl_cipher_string)
            else:
                ssl_connection.ssl_client.set_cipher_list(
                    final_openssl_cipher_string)

        return ssl_connection
Ejemplo n.º 3
0
def _detect_ecdh_support(
    server_location: ServerNetworkLocation,
    network_config: ServerNetworkConfiguration,
    tls_version: TlsVersionEnum,
) -> bool:
    if tls_version.value < TlsVersionEnum.TLS_1_2.value:
        # Retrieving ECDH information is only implemented in the modern nassl.SslClient, which is TLS 1.2+
        return False

    is_ecdh_key_exchange_supported = False
    ssl_connection = SslConnection(
        server_location=server_location,
        network_configuration=network_config,
        tls_version=tls_version,
        should_use_legacy_openssl=False,
        should_ignore_client_auth=True,
    )
    if not isinstance(ssl_connection.ssl_client, SslClient):
        raise RuntimeError(
            "Should never happen: specified should_use_legacy_openssl=False but didn't get the modern"
            " SSL client")

    # Set the right elliptic curve cipher suites
    enable_ecdh_cipher_suites(tls_version, ssl_connection.ssl_client)
    try:
        ssl_connection.connect(should_retry_connection=False)
        is_ecdh_key_exchange_supported = True
    except ClientCertificateRequested:
        is_ecdh_key_exchange_supported = True
    except ServerRejectedTlsHandshake:
        is_ecdh_key_exchange_supported = False
    finally:
        ssl_connection.close()

    return is_ecdh_key_exchange_supported
Ejemplo n.º 4
0
def _detect_client_auth_requirement_with_tls_1_2_or_below(
    server_location: ServerNetworkLocation,
    network_config: ServerNetworkConfiguration,
    tls_version: TlsVersionEnum,
    cipher_list: str,
) -> ClientAuthRequirementEnum:
    """Try to detect if client authentication is optional or required."""
    if tls_version.value >= TlsVersionEnum.TLS_1_3.value:
        raise ValueError("Use _detect_client_auth_requirement_with_tls_1_3()")

    ssl_connection_auth = SslConnection(
        server_location=server_location,
        network_configuration=network_config,
        tls_version=tls_version,
        should_ignore_client_auth=True,
    )
    ssl_connection_auth.ssl_client.set_cipher_list(cipher_list)

    try:
        ssl_connection_auth.connect(should_retry_connection=False)
        client_auth_requirement = ClientAuthRequirementEnum.OPTIONAL
    except (ClientCertificateRequested, ServerRejectedTlsHandshake):
        client_auth_requirement = ClientAuthRequirementEnum.REQUIRED
    finally:
        ssl_connection_auth.close()

    return client_auth_requirement
Ejemplo n.º 5
0
def _detect_client_auth_requirement_with_tls_1_3(
    server_location: ServerNetworkLocation,
    network_config: ServerNetworkConfiguration,
) -> ClientAuthRequirementEnum:
    """Try to detect if client authentication is optional or required."""
    ssl_connection_auth = SslConnection(
        server_location=server_location,
        network_configuration=network_config,
        tls_version=TlsVersionEnum.TLS_1_3,
        should_ignore_client_auth=True,
    )
    try:
        ssl_connection_auth.connect(should_retry_connection=False)

        # With TLS 1.3 we need to send some data and then read the response
        # to force a ClientCertificateRequested exception; not sure why
        # https://github.com/nabla-c0d3/sslyze/issues/472
        ssl_connection_auth.ssl_client.write(b"A")
        ssl_connection_auth.ssl_client.read(1)

        client_auth_requirement = ClientAuthRequirementEnum.OPTIONAL

    except (ClientCertificateRequested, ServerRejectedTlsHandshake):
        client_auth_requirement = ClientAuthRequirementEnum.REQUIRED

    except socket.timeout:
        # The timeout is triggered when calling read() because the server has client auth optional and is waiting for
        # more data from us the client
        client_auth_requirement = ClientAuthRequirementEnum.OPTIONAL

    finally:
        ssl_connection_auth.close()

    return client_auth_requirement
Ejemplo n.º 6
0
def _detect_support_for_tls_1_2_or_below(
    server_location: ServerNetworkLocation,
    network_config: ServerNetworkConfiguration,
    tls_version: TlsVersionEnum,
) -> _TlsVersionDetectionResult:
    # First try the default cipher list, and then all ciphers; this is to work around F5 network devices
    # that time out when the client hello is too long (ie. too many cipher suites enabled)
    # https://support.f5.com/csp/article/K14758
    for cipher_list in ["DEFAULT", "ALL:COMPLEMENTOFALL:-PSK:-SRP"]:
        ssl_connection = SslConnection(
            server_location=server_location,
            network_configuration=network_config,
            tls_version=tls_version,
            should_ignore_client_auth=False,
        )
        ssl_connection.ssl_client.set_cipher_list(cipher_list)

        try:
            # Only do one attempt when testing connectivity
            ssl_connection.connect(should_retry_connection=False)
            return _TlsVersionDetectionResult(
                tls_version_supported=tls_version,
                server_requested_client_cert=False,
                cipher_suite_supported=ssl_connection.ssl_client.
                get_current_cipher_name(),
            )

        except ClientCertificateRequested:
            # Connection successful but the servers wants a client certificate which wasn't supplied to sslyze
            return _TlsVersionDetectionResult(
                tls_version_supported=tls_version,
                server_requested_client_cert=True,
                # Calling ssl_connection.ssl_client.get_current_cipher_name() will fail in this situation so we just
                # store the whole cipher_list
                cipher_suite_supported=cipher_list,
            )

        except TlsHandshakeFailed:
            # Try the next cipher list
            pass

        except (OSError, _nassl.OpenSSLError) as e:
            # If these errors get propagated here, it means they're not part of the known/normal errors that
            # can happen when trying to connect to a server and defined in tls_connection.py
            # Hence we re-raise these as "unknown" connection errors; might be caused by bad connectivity to
            # the server (random disconnects, etc.) and the scan against this server should not be performed
            raise ConnectionToServerFailed(
                server_location=server_location,
                network_configuration=network_config,
                error_message=f'Unexpected connection error: "{e.args}"',
            )

        finally:
            ssl_connection.close()

    # If we get here, none of the handshakes were successful
    raise _TlsVersionNotSupported()
Ejemplo n.º 7
0
def _detect_client_auth_requirement_with_tls_1_3(
    server_location: ServerNetworkLocation,
    network_config: ServerNetworkConfiguration,
) -> ClientAuthRequirementEnum:
    """Try to detect if client authentication is optional or required."""
    ssl_connection_auth = SslConnection(
        server_location=server_location,
        network_configuration=network_config,
        tls_version=TlsVersionEnum.TLS_1_3,
        should_ignore_client_auth=True,
    )
    try:
        ssl_connection_auth.connect(should_retry_connection=False)

        # With TLS 1.3 we need to send some data and then read the response
        # to force a ClientCertificateRequested exception; not sure why
        # https://github.com/nabla-c0d3/sslyze/issues/472
        ssl_connection_auth.ssl_client.write(b"A")
        ssl_connection_auth.ssl_client.read(1)

        client_auth_requirement = ClientAuthRequirementEnum.OPTIONAL

    except (ClientCertificateRequested, ServerRejectedTlsHandshake):
        client_auth_requirement = ClientAuthRequirementEnum.REQUIRED

    except socket.timeout:
        # The timeout is triggered when calling read() because the server has client auth optional and is waiting for
        # more data from us the client
        client_auth_requirement = ClientAuthRequirementEnum.OPTIONAL

    except _nassl.OpenSSLError as e:
        # Here we re-use some of the rejection handling logic already implemented in SslConnection.connect()
        # This is because the call to read(1) in the try block might trigger similar errors as connect()
        # https://github.com/nabla-c0d3/sslyze/issues/562
        # TODO(AD): Find a way to unify exception handling between the two calls
        openssl_error_message = e.args[0]
        is_known_server_rejection_error = False
        for error_msg in _HANDSHAKE_REJECTED_TLS_ERRORS.keys():
            if error_msg in openssl_error_message:
                is_known_server_rejection_error = True
                break

        if is_known_server_rejection_error:
            client_auth_requirement = ClientAuthRequirementEnum.REQUIRED
        else:
            raise

    finally:
        ssl_connection_auth.close()

    return client_auth_requirement
Ejemplo n.º 8
0
def _detect_support_for_tls_1_3(
    server_location: ServerNetworkLocation,
    network_config: ServerNetworkConfiguration,
) -> _TlsVersionDetectionResult:
    ssl_connection = SslConnection(
        server_location=server_location,
        network_configuration=network_config,
        tls_version=TlsVersionEnum.TLS_1_3,
        should_ignore_client_auth=False,
    )

    try:
        ssl_connection.connect(should_retry_connection=False)
        return _TlsVersionDetectionResult(
            tls_version_supported=TlsVersionEnum.TLS_1_3,
            server_requested_client_cert=False,
            cipher_suite_supported=ssl_connection.ssl_client.
            get_current_cipher_name(),
        )
    except ClientCertificateRequested:
        # Connection successful but the servers wants a client certificate which wasn't supplied to sslyze
        return _TlsVersionDetectionResult(
            tls_version_supported=TlsVersionEnum.TLS_1_3,
            server_requested_client_cert=True,
            cipher_suite_supported=ssl_connection.ssl_client.
            get_current_cipher_name(),
        )

    except TlsHandshakeFailed:
        pass

    except (OSError, _nassl.OpenSSLError) as e:
        # If these errors get propagated here, it means they're not part of the known/normal errors that
        # can happen when trying to connect to a server and defined in tls_connection.py
        # Hence we re-raise these as "unknown" connection errors; might be caused by bad connectivity to
        # the server (random disconnects, etc.) and the scan against this server should not be performed
        raise ConnectionToServerFailed(
            server_location=server_location,
            network_configuration=network_config,
            error_message=f'Unexpected connection error: "{e.args}"',
        )

    finally:
        ssl_connection.close()

    # If we get here, none of the handshakes were successful
    raise _TlsVersionNotSupported()
Ejemplo n.º 9
0
    def perform(
        self,
        server_location: ServerNetworkLocation,
        network_configuration: Optional[ServerNetworkConfiguration] = None
    ) -> ServerConnectivityInfo:
        """Attempt to perform a full SSL/TLS handshake with the server.

        This method will ensure that the server can be reached, and will also identify one SSL/TLS version and one
        cipher suite that is supported by the server.

        Args:
            server_location
            network_configuration

        Returns:
            An object encapsulating all the information needed to connect to the server, to be passed to a `Scanner` in
            order to run scan commands against the server.

        Raises:
            ServerConnectivityError: If the server was not reachable or an SSL/TLS handshake could not be completed.
        """
        if network_configuration is None:
            final_network_config = ServerNetworkConfiguration.default_for_server_location(
                server_location)
        else:
            final_network_config = network_configuration

        # Try to complete an SSL handshake to figure out the SSL version and cipher supported by the server
        highest_tls_version_supported = None
        cipher_suite_supported = None
        client_auth_requirement = ClientAuthRequirementEnum.DISABLED

        # TODO(AD): Switch to using the protocol discovery logic available in OpenSSL 1.1.0 with TLS_client_method()
        for tls_version in [
                OpenSslVersionEnum.TLSV1_3,
                OpenSslVersionEnum.TLSV1_2,
                OpenSslVersionEnum.TLSV1_1,
                OpenSslVersionEnum.TLSV1,
                OpenSslVersionEnum.SSLV3,
        ]:
            # First try the default cipher list, and then all ciphers
            for cipher_list in [None, "ALL:COMPLEMENTOFALL:-PSK:-SRP"]:
                ssl_connection = SslConnection(
                    server_location=server_location,
                    network_configuration=final_network_config,
                    tls_version=tls_version,
                    should_ignore_client_auth=False,
                )
                if cipher_list:
                    if tls_version == OpenSslVersionEnum.TLSV1_3:
                        # Skip the second attempt with all ciphers enabled as these ciphers don't exist in TLS 1.3
                        continue

                    ssl_connection.ssl_client.set_cipher_list(cipher_list)

                try:
                    # Only do one attempt when testing connectivity
                    ssl_connection.connect(should_retry_connection=False)
                    highest_tls_version_supported = tls_version
                    cipher_suite_supported = ssl_connection.ssl_client.get_current_cipher_name(
                    )
                except ClientCertificateRequested:
                    # Connection successful but the servers wants a client certificate which wasn't supplied to sslyze
                    # Store the SSL version and cipher list that is supported
                    highest_tls_version_supported = tls_version
                    cipher_suite_supported = cipher_list
                    # Close the current connection and try again but ignore client authentication
                    ssl_connection.close()

                    # Try a new connection to see if client authentication is optional
                    ssl_connection_auth = SslConnection(
                        server_location=server_location,
                        network_configuration=final_network_config,
                        tls_version=tls_version,
                        should_ignore_client_auth=True,
                    )
                    if cipher_list:
                        ssl_connection_auth.ssl_client.set_cipher_list(
                            cipher_list)
                    try:
                        ssl_connection_auth.connect(
                            should_retry_connection=False)
                        cipher_suite_supported = ssl_connection_auth.ssl_client.get_current_cipher_name(
                        )
                        client_auth_requirement = ClientAuthRequirementEnum.OPTIONAL

                    # If client authentication is required, we either get a ClientCertificateRequested
                    except ClientCertificateRequested:
                        client_auth_requirement = ClientAuthRequirementEnum.REQUIRED
                    # Or a ServerRejectedTlsHandshake
                    except ServerRejectedTlsHandshake:
                        client_auth_requirement = ClientAuthRequirementEnum.REQUIRED
                    finally:
                        ssl_connection_auth.close()

                except TlsHandshakeFailed:
                    # This TLS version did not work; keep going
                    pass

                finally:
                    ssl_connection.close()

            if cipher_suite_supported:
                # A handshake was successful
                break

        if highest_tls_version_supported is None or cipher_suite_supported is None:
            raise ServerTlsConfigurationNotSupported(
                server_location=server_location,
                network_configuration=final_network_config,
                error_message=
                "Probing failed: could not find a TLS version and cipher suite supported by the server",
            )
        tls_probing_result = ServerTlsProbingResult(
            highest_tls_version_supported=highest_tls_version_supported,
            cipher_suite_supported=cipher_suite_supported,
            client_auth_requirement=client_auth_requirement,
        )

        return ServerConnectivityInfo(
            server_location=server_location,
            network_configuration=final_network_config,
            tls_probing_result=tls_probing_result,
        )
Ejemplo n.º 10
0
    def perform(
        self,
        server_location: ServerNetworkLocation,
        network_configuration: Optional[ServerNetworkConfiguration] = None
    ) -> ServerConnectivityInfo:
        """Attempt to perform a full SSL/TLS handshake with the server.

        This method will ensure that the server can be reached, and will also identify one SSL/TLS version and one
        cipher suite that is supported by the server.

        Args:
            server_location
            network_configuration

        Returns:
            An object encapsulating all the information needed to connect to the server, to be passed to a `Scanner` in
            order to run scan commands against the server.

        Raises:
            ServerConnectivityError: If the server was not reachable or an SSL/TLS handshake could not be completed.
        """
        if network_configuration is None:
            final_network_config = ServerNetworkConfiguration.default_for_server_location(
                server_location)
        else:
            final_network_config = network_configuration

        # Try to complete an SSL handshake to figure out the SSL version and cipher supported by the server
        highest_tls_version_supported = None
        cipher_suite_supported = None
        client_auth_requirement = ClientAuthRequirementEnum.DISABLED

        # TODO(AD): Switch to using the protocol discovery logic available in OpenSSL 1.1.0 with TLS_client_method()
        for tls_version in [
                TlsVersionEnum.TLS_1_3,
                TlsVersionEnum.TLS_1_2,
                TlsVersionEnum.TLS_1_1,
                TlsVersionEnum.TLS_1_0,
                TlsVersionEnum.SSL_3_0,
        ]:
            # First try the default cipher list, and then all ciphers
            for cipher_list in [None, "ALL:COMPLEMENTOFALL:-PSK:-SRP"]:
                ssl_connection = SslConnection(
                    server_location=server_location,
                    network_configuration=final_network_config,
                    tls_version=tls_version,
                    should_ignore_client_auth=False,
                )
                if cipher_list:
                    if tls_version == TlsVersionEnum.TLS_1_3:
                        # Skip the second attempt with all ciphers enabled as these ciphers don't exist in TLS 1.3
                        continue

                    ssl_connection.ssl_client.set_cipher_list(cipher_list)

                try:
                    # Only do one attempt when testing connectivity
                    ssl_connection.connect(should_retry_connection=False)
                    highest_tls_version_supported = tls_version
                    cipher_suite_supported = ssl_connection.ssl_client.get_current_cipher_name(
                    )
                except ClientCertificateRequested:
                    # Connection successful but the servers wants a client certificate which wasn't supplied to sslyze
                    # Store the SSL version and cipher list that is supported
                    highest_tls_version_supported = tls_version
                    cipher_suite_supported = cipher_list
                    # Close the current connection and try again but ignore client authentication
                    ssl_connection.close()

                    # Try a new connection to see if client authentication is optional
                    ssl_connection_auth = SslConnection(
                        server_location=server_location,
                        network_configuration=final_network_config,
                        tls_version=tls_version,
                        should_ignore_client_auth=True,
                    )
                    if cipher_list:
                        ssl_connection_auth.ssl_client.set_cipher_list(
                            cipher_list)
                    try:
                        ssl_connection_auth.connect(
                            should_retry_connection=False)
                        cipher_suite_supported = ssl_connection_auth.ssl_client.get_current_cipher_name(
                        )
                        client_auth_requirement = ClientAuthRequirementEnum.OPTIONAL

                    # If client authentication is required, we either get a ClientCertificateRequested
                    except ClientCertificateRequested:
                        client_auth_requirement = ClientAuthRequirementEnum.REQUIRED
                    # Or a ServerRejectedTlsHandshake
                    except ServerRejectedTlsHandshake:
                        client_auth_requirement = ClientAuthRequirementEnum.REQUIRED
                    finally:
                        ssl_connection_auth.close()

                except TlsHandshakeFailed:
                    # This TLS version did not work; keep going
                    pass

                except (OSError, _nassl.OpenSSLError) as e:
                    # If these errors get propagated here, it means they're not part of the known/normal errors that
                    # can happen when trying to connect to a server and defined in tls_connection.py
                    # Hence we re-raise these as "unknown" connection errors; might be caused by bad connectivity to
                    # the server (random disconnects, etc.) and the scan against this server should not be performed
                    raise ConnectionToServerFailed(
                        server_location=server_location,
                        network_configuration=final_network_config,
                        error_message=
                        f'Unexpected connection error: "{e.args}"',
                    )

                finally:
                    ssl_connection.close()

            if cipher_suite_supported:
                # A handshake was successful
                break

        if highest_tls_version_supported is None or cipher_suite_supported is None:
            raise ServerTlsConfigurationNotSupported(
                server_location=server_location,
                network_configuration=final_network_config,
                error_message=
                "Probing failed: could not find a TLS version and cipher suite supported by the server",
            )

        # Check if ECDH key exchanges are supported
        is_ecdh_key_exchange_supported = False
        if "ECDH" in cipher_suite_supported:
            is_ecdh_key_exchange_supported = True
        else:
            if highest_tls_version_supported.value >= TlsVersionEnum.TLS_1_2.value:
                ssl_connection = SslConnection(
                    server_location=server_location,
                    network_configuration=final_network_config,
                    tls_version=highest_tls_version_supported,
                    should_use_legacy_openssl=False,
                    should_ignore_client_auth=True,
                )
                if not isinstance(ssl_connection.ssl_client, SslClient):
                    raise RuntimeError(
                        "Should never happen: specified should_use_legacy_openssl=False but didn't get the modern"
                        " SSL client")

                # Set the right elliptic curve cipher suites
                enable_ecdh_cipher_suites(highest_tls_version_supported,
                                          ssl_connection.ssl_client)
                try:
                    ssl_connection.connect(should_retry_connection=False)
                    is_ecdh_key_exchange_supported = True
                except ClientCertificateRequested:
                    is_ecdh_key_exchange_supported = True
                except ServerRejectedTlsHandshake:
                    is_ecdh_key_exchange_supported = False
                finally:
                    ssl_connection.close()

        tls_probing_result = ServerTlsProbingResult(
            highest_tls_version_supported=highest_tls_version_supported,
            cipher_suite_supported=cipher_suite_supported,
            client_auth_requirement=client_auth_requirement,
            supports_ecdh_key_exchange=is_ecdh_key_exchange_supported,
        )

        return ServerConnectivityInfo(
            server_location=server_location,
            network_configuration=final_network_config,
            tls_probing_result=tls_probing_result,
        )