From fc1ae48857089a0b97c4eac3e2739e917fead0eb Mon Sep 17 00:00:00 2001 From: Hanno Heinrichs Date: Sat, 13 Feb 2016 11:33:53 +0100 Subject: [PATCH] add client-initiated renegotiation detection --- docs/source/development/rating.rst | 6 +- sslscan/module/rating/builtin.py | 9 ++ sslscan/module/report/terminal.py | 42 +++++--- sslscan/module/scan/__init__.py | 16 ++- sslscan/module/scan/server_renegotiation.py | 105 +++++++++++++------- 5 files changed, 126 insertions(+), 52 deletions(-) diff --git a/docs/source/development/rating.rst b/docs/source/development/rating.rst index 6ccce45..593171a 100644 --- a/docs/source/development/rating.rst +++ b/docs/source/development/rating.rst @@ -46,10 +46,12 @@ Rules +-----------------------------------------------------+-----------+----------------------------+ | **Renegotiation** | +-----------------------------------------------------+-----------+----------------------------+ -| server.renegotiation.support | | | -+-----------------------------------------------------+-----------+----------------------------+ | server.renegotiation.secure | | | +-----------------------------------------------------+-----------+----------------------------+ +| server.renegotiation.ci_secure | | | ++-----------------------------------------------------+-----------+----------------------------+ +| server.renegotiation.ci_insecure | | | ++-----------------------------------------------------+-----------+----------------------------+ | **Session** | +-----------------------------------------------------+-----------+----------------------------+ | server.session.compression | Boolean | | diff --git a/sslscan/module/rating/builtin.py b/sslscan/module/rating/builtin.py index acda803..2632ae7 100644 --- a/sslscan/module/rating/builtin.py +++ b/sslscan/module/rating/builtin.py @@ -73,6 +73,15 @@ def __init__(self, **kwargs): ) ) + self.add_rule( + RatingRule( + "server.renegotiation.ci_insecure", + rules=[ + lambda v, i, kb: 6 if v else None, + ] + ) + ) + self.add_rule( RatingRule( "server.renegotiation.secure", diff --git a/sslscan/module/report/terminal.py b/sslscan/module/report/terminal.py index 7769f34..bad0d70 100644 --- a/sslscan/module/report/terminal.py +++ b/sslscan/module/report/terminal.py @@ -271,35 +271,53 @@ def _print_server_ciphers(self, kb): print("") def _print_host_renegotiation(self, kb): - if kb.get("server.renegotiation.support") is None: + if kb.get("server.renegotiation.secure") is None: return - reneg_support = kb.get("server.renegotiation.support") + print("TLS renegotiation:") + + reneg_secure = kb.get("server.renegotiation.secure") rating_renegotiation = self._rating.rate( - "server.renegotiation.support", - reneg_support + "server.renegotiation.secure", + reneg_secure ) - print("TLS renegotiation:") print( - " Supported: {1}{0}{2}".format( - "yes" if reneg_support else "no", + " Secure renegotiation support: {1}{0}{2}".format( + "yes" if reneg_secure else "no", helper.rating2color(self.color, rating_renegotiation), self.color.RESET ) ) - reneg_secure = kb.get("server.renegotiation.secure") + reneg_support = kb.get("server.renegotiation.ci_secure") rating_renegotiation = self._rating.rate( - "server.renegotiation.secure", - reneg_secure + "server.renegotiation.ci_secure", + reneg_support ) print( - " Secure: {1}{0}{2}".format( - "yes" if reneg_secure else "no", + " Client-initiated renegotiation (secure): {1}{0}{2}".format( + "yes" if reneg_support else "no", helper.rating2color(self.color, rating_renegotiation), self.color.RESET ) ) + + reneg_support = kb.get("server.renegotiation.ci_insecure") + # Remove check when insecure, client-initiated renegotiations are + # checked properly (see server_renegotiation.py). + if reneg_support is not None: + rating_renegotiation = self._rating.rate( + "server.renegotiation.ci_insecure", + reneg_support + ) + print( + " Client-initiated renegotiation (insecure): {1}{0}{2}".format( + "yes" if reneg_support else "no", + helper.rating2color(self.color, rating_renegotiation), + self.color.RESET + ) + ) + print("") def _print_server_alpn(self, kb): diff --git a/sslscan/module/scan/__init__.py b/sslscan/module/scan/__init__.py index e5cc16e..6fa4aff 100644 --- a/sslscan/module/scan/__init__.py +++ b/sslscan/module/scan/__init__.py @@ -737,16 +737,21 @@ def _connect_internal_ssl(self, protocol_versions=None): return None - def _connect_openssl(self, protocol_versions=None): - if openssl_enabled == False: + def _connect_openssl( + self, + protocol_versions=None, + ctx_options=None, + methods=None): + if not openssl_enabled: return None from sslscan._helper.openssl import convert_versions2methods if protocol_versions is None: protocol_versions = self._scanner.get_enabled_versions() - methods = convert_versions2methods(protocol_versions) - methods.reverse() + if methods is None: + methods = convert_versions2methods(protocol_versions) + methods.reverse() for method in methods: try: @@ -756,6 +761,9 @@ def _connect_openssl(self, protocol_versions=None): continue ctx.set_cipher_list("ALL:COMPLEMENT") + if ctx_options: + for option in ctx_options: + ctx.set_options(option) conn = self._scanner.handler.connect() conn_ssl = SSL.Connection(ctx, conn._socket) conn_ssl.set_tlsext_host_name( diff --git a/sslscan/module/scan/server_renegotiation.py b/sslscan/module/scan/server_renegotiation.py index 0b9fecd..5a95b3b 100644 --- a/sslscan/module/scan/server_renegotiation.py +++ b/sslscan/module/scan/server_renegotiation.py @@ -1,12 +1,16 @@ from sslscan import modules -from sslscan._helper.openssl import version_openssl, version_pyopenssl, convert_versions2methods +from sslscan._helper.openssl import ( + version_openssl, + version_pyopenssl, + convert_versions2methods + ) from sslscan.module import STATUS_OK, STATUS_ERROR -from sslscan.module.scan import BaseScan +from sslscan.module.scan import BaseInfoScan openssl_enabled = False version_info = [] try: - from OpenSSL import SSL, _util + from OpenSSL import _util openssl_enabled = True if version_pyopenssl: @@ -17,7 +21,7 @@ pass -class ServerRenegotiation(BaseScan): +class ServerRenegotiation(BaseInfoScan): """ Test if renegotiation is supported by the server. """ @@ -25,10 +29,28 @@ class ServerRenegotiation(BaseScan): name = "server.renegotiation" alias = ("renegotiation",) status = STATUS_OK if openssl_enabled else STATUS_ERROR - status_messages = ["OpenSSL is {}".format("available" if openssl_enabled else "missing")] + version_info + status_messages = [ + "OpenSSL is {}".format("available" if openssl_enabled else "missing") + ] + version_info def __init__(self, **kwargs): - BaseScan.__init__(self, **kwargs) + BaseInfoScan.__init__(self, **kwargs) + + def _supports_client_initiated_renegotiation(self, conn_ssl): + tr = _util.lib.SSL_total_renegotiations(conn_ssl._ssl) + if _util.lib.SSL_renegotiate(conn_ssl._ssl) != 1: + return False + if _util.lib.SSL_do_handshake(conn_ssl._ssl) != 1: + return False + + # Check that + # - renegotiation counter has increased and + # - no renegotiations are pending + if (_util.lib.SSL_total_renegotiations(conn_ssl._ssl) == tr + 1 and + _util.lib.SSL_renegotiate_pending(conn_ssl._ssl) == 0): + return True + else: + return False def run(self): kb = self._scanner.get_knowledge_base() @@ -39,41 +61,56 @@ def run(self): methods.reverse() for method in methods: - try: - ctx = SSL.Context(method) - except: - # ToDo: - continue - - ctx.set_cipher_list("ALL:COMPLEMENT") - ctx.set_options(_util.lib.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION) - conn = self._scanner.handler.connect() - conn_ssl = SSL.Connection(ctx, conn._socket) - conn_ssl.set_tlsext_host_name( - self._scanner.handler.hostname.encode("utf-8") + ctx_options = (_util.lib.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION,) + conn_ssl = self._connect_openssl( + methods=(method,), + ctx_options=ctx_options ) - conn_ssl.set_connect_state() - try: - conn_ssl.do_handshake() - except Exception as e: - # ToDo: - # print(e) - conn_ssl.close() + if conn_ssl is None: continue - kb.set("server.renegotiation.support", False) - if _util.lib.SSL_get_secure_renegotiation_support(conn_ssl._ssl) == 1: + # Does server signal support for secure renegotiation? + if (_util.lib.SSL_get_secure_renegotiation_support(conn_ssl._ssl) + == 1): kb.set("server.renegotiation.secure", True) - kb.set("server.renegotiation.support", True) else: kb.set("server.renegotiation.secure", False) - kb.set("server.renegotiation.support", False) - cipher_status = _util.lib.SSL_do_handshake(conn_ssl._ssl) - if cipher_status == 1: - if _util.lib.SSL_get_state(conn_ssl._ssl) == SSL.SSL_ST_OK: - kb.set("server.renegotiation.support", True) - conn_ssl.close() + # Setting ci_secure to False because pySSLScan can detect this + # feature in all cases and set True if needed. + kb.set("server.renegotiation.ci_secure", False) + # Setting ci_insecure to None because pySSLScan lacks support for + # checking insecure, client-initiated renegotitations if the server + # provides secure renegotiation support. None prevents misleading + # output in this case. + # If the server does not provide secure renegotiation support, any + # client-initiated renegotiation must be considered insecure. In + # this case, True and False are set correctly for ci_insecure. + kb.set("server.renegotiation.ci_insecure", None) + + # Does the server accept client-initiated renegotiatons? + if self._supports_client_initiated_renegotiation(conn_ssl): + if kb.get("server.renegotiation.secure"): + # Renegotiation was performed securely, we do not know + # about insecure renegotiations yet. + kb.set("server.renegotiation.ci_secure", True) + else: + # Renegotiation was performed insecurely, no need to check + # any further. + kb.set("server.renegotiation.ci_insecure", True) + else: + if not kb.get("server.renegotiation.secure"): + # Server does not support secure renegotitation and client- + # initiated renegotiation attempt failed. Thus, the server + # does not allow client-initiated insecure renegotiations + # either. + kb.set("server.renegotiation.ci_insecure", False) + + # TODO: Implement insecure, client-initiated renegotiation (i.e. + # without RFC5746-compliance) for scenarios where the server + # provides secure renegotiation support. + + conn_ssl.close() modules.register(ServerRenegotiation)