403b09
From 18d4af260f777920dc2b474a588c3dcda20d8801 Mon Sep 17 00:00:00 2001
403b09
From: Fraser Tweedale <ftweedal@redhat.com>
403b09
Date: Thu, 30 Jun 2016 10:21:01 +1000
403b09
Subject: [PATCH] cert-revoke: fix permission check bypass (CVE-2016-5404)
403b09
403b09
The 'cert_revoke' command checks the 'revoke certificate'
403b09
permission, however, if an ACIError is raised, it then invokes the
403b09
'cert_show' command.  The rational was to re-use a "host manages
403b09
certificate" check that is part of the 'cert_show' command, however,
403b09
it is sufficient that 'cert_show' executes successfully for
403b09
'cert_revoke' to recover from the ACIError continue.  Therefore,
403b09
anyone with 'retrieve certificate' permission can revoke *any*
403b09
certificate and cause various kinds of DoS.
403b09
403b09
Fix the problem by extracting the "host manages certificate" check
403b09
to its own method and explicitly calling it from 'cert_revoke'.
403b09
403b09
Fixes: https://fedorahosted.org/freeipa/ticket/6232
403b09
Reviewed-By: Jan Cholasta <jcholast@redhat.com>
403b09
---
403b09
 ipaserver/plugins/cert.py | 60 +++++++++++++++++++++++++----------------------
403b09
 1 file changed, 32 insertions(+), 28 deletions(-)
403b09
403b09
diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
403b09
index b8df074a186ca91daa8e8f5e725724ea7bc5a663..6dd9f6ffcdcd9d051d50d912996fea2104d71dff 100644
403b09
--- a/ipaserver/plugins/cert.py
403b09
+++ b/ipaserver/plugins/cert.py
403b09
@@ -242,6 +242,24 @@ def validate_certificate(value):
403b09
     return x509.validate_certificate(value, x509.DER)
403b09
 
403b09
 
403b09
+def bind_principal_can_manage_cert(cert):
403b09
+    """Check that the bind principal can manage the given cert.
403b09
+
403b09
+    ``cert``
403b09
+        An NSS certificate object.
403b09
+
403b09
+    """
403b09
+    bind_principal = kerberos.Principal(getattr(context, 'principal'))
403b09
+    if not bind_principal.is_host:
403b09
+        return False
403b09
+
403b09
+    hostname = bind_principal.hostname
403b09
+
403b09
+    # If we have a hostname we want to verify that the subject
403b09
+    # of the certificate matches it.
403b09
+    return hostname == cert.subject.common_name  #pylint: disable=E1101
403b09
+
403b09
+
403b09
 class BaseCertObject(Object):
403b09
     takes_params = (
403b09
         Bytes(
403b09
@@ -744,18 +762,6 @@ class cert_show(Retrieve, CertMethod, VirtualCommand):
403b09
     def execute(self, serial_number, all=False, raw=False, no_members=False,
403b09
                 **options):
403b09
         ca_enabled_check()
403b09
-        hostname = None
403b09
-        try:
403b09
-            self.check_access()
403b09
-        except errors.ACIError as acierr:
403b09
-            self.debug("Not granted by ACI to retrieve certificate, looking at principal")
403b09
-            bind_principal = kerberos.Principal(getattr(context, 'principal'))
403b09
-            if not bind_principal.is_host:
403b09
-                raise acierr
403b09
-            hostname = bind_principal.hostname
403b09
-
403b09
-        ca_obj = api.Command.ca_show(options['cacn'])['result']
403b09
-        issuer_dn = ca_obj['ipacasubjectdn'][0]
403b09
 
403b09
         # Dogtag lightweight CAs have shared serial number domain, so
403b09
         # we don't tell Dogtag the issuer (but we check the cert after).
403b09
@@ -763,7 +769,15 @@ class cert_show(Retrieve, CertMethod, VirtualCommand):
403b09
         result = self.Backend.ra.get_certificate(str(serial_number))
403b09
         cert = x509.load_certificate(result['certificate'])
403b09
 
403b09
-        if DN(unicode(cert.issuer)) != DN(issuer_dn):
403b09
+        try:
403b09
+            self.check_access()
403b09
+        except errors.ACIError as acierr:
403b09
+            self.debug("Not granted by ACI to retrieve certificate, looking at principal")
403b09
+            if not bind_principal_can_manage_cert(cert):
403b09
+                raise acierr  # pylint: disable=E0702
403b09
+
403b09
+        ca_obj = api.Command.ca_show(options['cacn'])['result']
403b09
+        if DN(unicode(cert.issuer)) != DN(ca_obj['ipacasubjectdn'][0]):
403b09
             # DN of cert differs from what we requested
403b09
             raise errors.NotFound(
403b09
                 reason=_("Certificate with serial number %(serial)s "
403b09
@@ -789,12 +803,6 @@ class cert_show(Retrieve, CertMethod, VirtualCommand):
403b09
             result['revoked'] = ('revocation_reason' in result)
403b09
             self.obj._fill_owners(result)
403b09
 
403b09
-        if hostname:
403b09
-            # If we have a hostname we want to verify that the subject
403b09
-            # of the certificate matches it, otherwise raise an error
403b09
-            if hostname != cert.subject.common_name:    #pylint: disable=E1101
403b09
-                raise acierr
403b09
-
403b09
         return dict(result=result, value=pkey_to_value(serial_number, options))
403b09
 
403b09
 
403b09
@@ -819,22 +827,18 @@ class cert_revoke(PKQuery, CertMethod, VirtualCommand):
403b09
 
403b09
         # Make sure that the cert specified by issuer+serial exists.
403b09
         # Will raise NotFound if it does not.
403b09
-        cert_show_options = dict(cacn=kw['cacn'])
403b09
-        api.Command.cert_show(unicode(serial_number), **cert_show_options)
403b09
+        resp = api.Command.cert_show(unicode(serial_number), cacn=kw['cacn'])
403b09
 
403b09
-        hostname = None
403b09
         try:
403b09
             self.check_access()
403b09
         except errors.ACIError as acierr:
403b09
             self.debug("Not granted by ACI to revoke certificate, looking at principal")
403b09
             try:
403b09
-                # Let cert_show() handle verifying that the subject of the
403b09
-                # cert we're dealing with matches the hostname in the principal
403b09
-                result = api.Command['cert_show'](
403b09
-                    unicode(serial_number), **cert_show_options
403b09
-                )['result']
403b09
+                cert = x509.load_certificate(resp['result']['certificate'])
403b09
+                if not bind_principal_can_manage_cert(cert):
403b09
+                    raise acierr
403b09
             except errors.NotImplementedError:
403b09
-                pass
403b09
+                raise acierr
403b09
         revocation_reason = kw['revocation_reason']
403b09
         if revocation_reason == 7:
403b09
             raise errors.CertificateOperationError(error=_('7 is not a valid revocation reason'))
403b09
-- 
403b09
2.9.3
403b09