ipa release 4.9.13-3

- ipa-kdb: Detect and block Bronze-Bit attacks
  Resolves: RHEL-9984
- Fix for CVE-2023-5455
  Resolves: RHEL-12578

Signed-off-by: Julien Rische <jrische@redhat.com>
This commit is contained in:
Julien Rische 2024-01-10 17:35:37 +01:00
parent 2005990bae
commit a321f34b62
4 changed files with 755 additions and 1 deletions

View File

@ -0,0 +1,121 @@
From ae006b436cfb4ccee5972cf1db0a309fcd80e669 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Fri, 6 Oct 2023 20:16:29 +0000
Subject: [PATCH] Check the HTTP Referer header on all requests
The referer was only checked in WSGIExecutioner classes:
- jsonserver
- KerberosWSGIExecutioner
- xmlserver
- jsonserver_kerb
This left /i18n_messages, /session/login_kerberos,
/session/login_x509, /session/login_password,
/session/change_password and /session/sync_token unprotected
against CSRF attacks.
CVE-2023-5455
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
---
ipaserver/rpcserver.py | 34 +++++++++++++++++++++++++++++++---
1 file changed, 31 insertions(+), 3 deletions(-)
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
index 4e8a08b66..3555014ca 100644
--- a/ipaserver/rpcserver.py
+++ b/ipaserver/rpcserver.py
@@ -156,6 +156,19 @@ _success_template = """<html>
</html>"""
class HTTP_Status(plugable.Plugin):
+ def check_referer(self, environ):
+ if "HTTP_REFERER" not in environ:
+ logger.error("Rejecting request with missing Referer")
+ return False
+ if (not environ["HTTP_REFERER"].startswith(
+ "https://%s/ipa" % self.api.env.host)
+ and not self.env.in_tree):
+ logger.error("Rejecting request with bad Referer %s",
+ environ["HTTP_REFERER"])
+ return False
+ logger.debug("Valid Referer %s", environ["HTTP_REFERER"])
+ return True
+
def not_found(self, environ, start_response, url, message):
"""
Return a 404 Not Found error.
@@ -331,9 +344,6 @@ class wsgi_dispatch(Executioner, HTTP_Status):
self.__apps[key] = app
-
-
-
class WSGIExecutioner(Executioner):
"""
Base class for execution backends with a WSGI application interface.
@@ -897,6 +907,9 @@ class jsonserver_session(jsonserver, KerberosSession):
logger.debug('WSGI jsonserver_session.__call__:')
+ if not self.check_referer(environ):
+ return self.bad_request(environ, start_response, 'denied')
+
# Redirect to login if no Kerberos credentials
ccache_name = self.get_environ_creds(environ)
if ccache_name is None:
@@ -949,6 +962,9 @@ class KerberosLogin(Backend, KerberosSession):
def __call__(self, environ, start_response):
logger.debug('WSGI KerberosLogin.__call__:')
+ if not self.check_referer(environ):
+ return self.bad_request(environ, start_response, 'denied')
+
# Redirect to login if no Kerberos credentials
user_ccache_name = self.get_environ_creds(environ)
if user_ccache_name is None:
@@ -967,6 +983,9 @@ class login_x509(KerberosLogin):
def __call__(self, environ, start_response):
logger.debug('WSGI login_x509.__call__:')
+ if not self.check_referer(environ):
+ return self.bad_request(environ, start_response, 'denied')
+
if 'KRB5CCNAME' not in environ:
return self.unauthorized(
environ, start_response, 'KRB5CCNAME not set',
@@ -1015,6 +1034,9 @@ class login_password(Backend, KerberosSession):
logger.debug('WSGI login_password.__call__:')
+ if not self.check_referer(environ):
+ return self.bad_request(environ, start_response, 'denied')
+
# Get the user and password parameters from the request
content_type = environ.get('CONTENT_TYPE', '').lower()
if not content_type.startswith('application/x-www-form-urlencoded'):
@@ -1147,6 +1169,9 @@ class change_password(Backend, HTTP_Status):
def __call__(self, environ, start_response):
logger.info('WSGI change_password.__call__:')
+ if not self.check_referer(environ):
+ return self.bad_request(environ, start_response, 'denied')
+
# Get the user and password parameters from the request
content_type = environ.get('CONTENT_TYPE', '').lower()
if not content_type.startswith('application/x-www-form-urlencoded'):
@@ -1364,6 +1389,9 @@ class xmlserver_session(xmlserver, KerberosSession):
logger.debug('WSGI xmlserver_session.__call__:')
+ if not self.check_referer(environ):
+ return self.bad_request(environ, start_response, 'denied')
+
ccache_name = environ.get('KRB5CCNAME')
# Redirect to /ipa/xml if no Kerberos credentials
--
2.41.0

View File

@ -0,0 +1,359 @@
From f1f8b16def3e809f5773bb8aa40aefb21699347b Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Thu, 12 Oct 2023 20:34:01 +0000
Subject: [PATCH] Integration tests for verifying Referer header in the UI
Validate that the change_password and login_password endpoints
verify the HTTP Referer header. There is some overlap in the
tests: belt and suspenders.
All endpoints except session/login_x509 are covered, sometimes
having to rely on expected bad results (see the i18n endpoint).
session/login_x509 is not tested yet as it requires significant
additional setup in order to associate a user certificate with
a user entry, etc.
This can be manually verified by modifying /etc/httpd/conf.d/ipa.conf
and adding:
Satisfy Any
Require all granted
Then comment out Auth and SSLVerify, etc. and restart httpd.
With a valid Referer will fail with a 401 and log that there is no
KRB5CCNAME. This comes after the referer check.
With an invalid Referer it will fail with a 400 Bad Request as
expected.
CVE-2023-5455
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
---
ipatests/test_ipaserver/httptest.py | 7 +-
ipatests/test_ipaserver/test_changepw.py | 12 +-
.../test_ipaserver/test_login_password.py | 88 ++++++++++++
ipatests/test_ipaserver/test_referer.py | 136 ++++++++++++++++++
ipatests/util.py | 4 +-
5 files changed, 242 insertions(+), 5 deletions(-)
create mode 100644 ipatests/test_ipaserver/test_login_password.py
create mode 100644 ipatests/test_ipaserver/test_referer.py
diff --git a/ipatests/test_ipaserver/httptest.py b/ipatests/test_ipaserver/httptest.py
index 6cd034a71..8924798fc 100644
--- a/ipatests/test_ipaserver/httptest.py
+++ b/ipatests/test_ipaserver/httptest.py
@@ -36,7 +36,7 @@ class Unauthorized_HTTP_test:
content_type = 'application/x-www-form-urlencoded'
accept_language = 'en-us'
- def send_request(self, method='POST', params=None):
+ def send_request(self, method='POST', params=None, host=None):
"""
Send a request to HTTP server
@@ -45,7 +45,10 @@ class Unauthorized_HTTP_test:
if params is not None:
if self.content_type == 'application/x-www-form-urlencoded':
params = urllib.parse.urlencode(params, True)
- url = 'https://' + self.host + self.app_uri
+ if host:
+ url = 'https://' + host + self.app_uri
+ else:
+ url = 'https://' + self.host + self.app_uri
headers = {'Content-Type': self.content_type,
'Accept-Language': self.accept_language,
diff --git a/ipatests/test_ipaserver/test_changepw.py b/ipatests/test_ipaserver/test_changepw.py
index c3a47ab26..df38ddb3d 100644
--- a/ipatests/test_ipaserver/test_changepw.py
+++ b/ipatests/test_ipaserver/test_changepw.py
@@ -53,10 +53,11 @@ class test_changepw(XMLRPC_test, Unauthorized_HTTP_test):
request.addfinalizer(fin)
- def _changepw(self, user, old_password, new_password):
+ def _changepw(self, user, old_password, new_password, host=None):
return self.send_request(params={'user': str(user),
'old_password' : str(old_password),
'new_password' : str(new_password)},
+ host=host
)
def _checkpw(self, user, password):
@@ -89,6 +90,15 @@ class test_changepw(XMLRPC_test, Unauthorized_HTTP_test):
# make sure that password is NOT changed
self._checkpw(testuser, old_password)
+ def test_invalid_referer(self):
+ response = self._changepw(testuser, old_password, new_password,
+ 'attacker.test')
+
+ assert_equal(response.status, 400)
+
+ # make sure that password is NOT changed
+ self._checkpw(testuser, old_password)
+
def test_pwpolicy_error(self):
response = self._changepw(testuser, old_password, '1')
diff --git a/ipatests/test_ipaserver/test_login_password.py b/ipatests/test_ipaserver/test_login_password.py
new file mode 100644
index 000000000..9425cb797
--- /dev/null
+++ b/ipatests/test_ipaserver/test_login_password.py
@@ -0,0 +1,88 @@
+# Copyright (C) 2023 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import pytest
+import uuid
+
+from ipatests.test_ipaserver.httptest import Unauthorized_HTTP_test
+from ipatests.test_xmlrpc.xmlrpc_test import XMLRPC_test
+from ipatests.util import assert_equal
+from ipalib import api, errors
+from ipapython.ipautil import run
+
+testuser = u'tuser'
+password = u'password'
+
+
+@pytest.mark.tier1
+class test_login_password(XMLRPC_test, Unauthorized_HTTP_test):
+ app_uri = '/ipa/session/login_password'
+
+ @pytest.fixture(autouse=True)
+ def login_setup(self, request):
+ ccache = os.path.join('/tmp', str(uuid.uuid4()))
+ try:
+ api.Command['user_add'](uid=testuser, givenname=u'Test', sn=u'User')
+ api.Command['passwd'](testuser, password=password)
+ run(['kinit', testuser], stdin='{0}\n{0}\n{0}\n'.format(password),
+ env={"KRB5CCNAME": ccache})
+ except errors.ExecutionError as e:
+ pytest.skip(
+ 'Cannot set up test user: %s' % e
+ )
+
+ def fin():
+ try:
+ api.Command['user_del']([testuser])
+ except errors.NotFound:
+ pass
+ os.unlink(ccache)
+
+ request.addfinalizer(fin)
+
+ def _login(self, user, password, host=None):
+ return self.send_request(params={'user': str(user),
+ 'password' : str(password)},
+ host=host)
+
+ def test_bad_options(self):
+ for params in (
+ None, # no params
+ {"user": "foo"}, # missing options
+ {"user": "foo", "password": ""}, # empty option
+ ):
+ response = self.send_request(params=params)
+ assert_equal(response.status, 400)
+ assert_equal(response.reason, 'Bad Request')
+
+ def test_invalid_auth(self):
+ response = self._login(testuser, 'wrongpassword')
+
+ assert_equal(response.status, 401)
+ assert_equal(response.getheader('X-IPA-Rejection-Reason'),
+ 'invalid-password')
+
+ def test_invalid_referer(self):
+ response = self._login(testuser, password, 'attacker.test')
+
+ assert_equal(response.status, 400)
+
+ def test_success(self):
+ response = self._login(testuser, password)
+
+ assert_equal(response.status, 200)
+ assert response.getheader('X-IPA-Rejection-Reason') is None
diff --git a/ipatests/test_ipaserver/test_referer.py b/ipatests/test_ipaserver/test_referer.py
new file mode 100644
index 000000000..4eade8bba
--- /dev/null
+++ b/ipatests/test_ipaserver/test_referer.py
@@ -0,0 +1,136 @@
+# Copyright (C) 2023 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import pytest
+import uuid
+
+from ipatests.test_ipaserver.httptest import Unauthorized_HTTP_test
+from ipatests.test_xmlrpc.xmlrpc_test import XMLRPC_test
+from ipatests.util import assert_equal
+from ipalib import api, errors
+from ipapython.ipautil import run
+
+testuser = u'tuser'
+password = u'password'
+
+
+@pytest.mark.tier1
+class test_referer(XMLRPC_test, Unauthorized_HTTP_test):
+
+ @pytest.fixture(autouse=True)
+ def login_setup(self, request):
+ ccache = os.path.join('/tmp', str(uuid.uuid4()))
+ tokenid = None
+ try:
+ api.Command['user_add'](uid=testuser, givenname=u'Test', sn=u'User')
+ api.Command['passwd'](testuser, password=password)
+ run(['kinit', testuser], stdin='{0}\n{0}\n{0}\n'.format(password),
+ env={"KRB5CCNAME": ccache})
+ result = api.Command["otptoken_add"](
+ type='HOTP', description='testotp',
+ ipatokenotpalgorithm='sha512', ipatokenowner=testuser,
+ ipatokenotpdigits='6')
+ tokenid = result['result']['ipatokenuniqueid'][0]
+ except errors.ExecutionError as e:
+ pytest.skip(
+ 'Cannot set up test user: %s' % e
+ )
+
+ def fin():
+ try:
+ api.Command['user_del']([testuser])
+ api.Command['otptoken_del']([tokenid])
+ except errors.NotFound:
+ pass
+ os.unlink(ccache)
+
+ request.addfinalizer(fin)
+
+ def _request(self, params={}, host=None):
+ # implicit is that self.app_uri is set to the appropriate value
+ return self.send_request(params=params, host=host)
+
+ def test_login_password_valid(self):
+ """Valid authentication of a user"""
+ self.app_uri = "/ipa/session/login_password"
+ response = self._request(
+ params={'user': 'tuser', 'password': password})
+ assert_equal(response.status, 200, self.app_uri)
+
+ def test_change_password_valid(self):
+ """This actually changes the user password"""
+ self.app_uri = "/ipa/session/change_password"
+ response = self._request(
+ params={'user': 'tuser',
+ 'old_password': password,
+ 'new_password': 'new_password'}
+ )
+ assert_equal(response.status, 200, self.app_uri)
+
+ def test_sync_token_valid(self):
+ """We aren't testing that sync works, just that we can get there"""
+ self.app_uri = "/ipa/session/sync_token"
+ response = self._request(
+ params={'user': 'tuser',
+ 'first_code': '1234',
+ 'second_code': '5678',
+ 'password': 'password'})
+ assert_equal(response.status, 200, self.app_uri)
+
+ def test_i18n_messages_valid(self):
+ # i18n_messages requires a valid JSON request and we send
+ # nothing. If we get a 500 error then it got past the
+ # referer check.
+ self.app_uri = "/ipa/i18n_messages"
+ response = self._request()
+ assert_equal(response.status, 500, self.app_uri)
+
+ # /ipa/session/login_x509 is not tested yet as it requires
+ # significant additional setup.
+ # This can be manually verified by adding
+ # Satisfy Any and Require all granted to the configuration
+ # section and comment out all Auth directives. The request
+ # will fail and log that there is no KRB5CCNAME which comes
+ # after the referer check.
+
+ def test_endpoints_auth_required(self):
+ """Test endpoints that require pre-authorization which will
+ fail before we even get to the Referer check
+ """
+ self.endpoints = {
+ "/ipa/xml",
+ "/ipa/session/login_kerberos",
+ "/ipa/session/json",
+ "/ipa/session/xml"
+ }
+ for self.app_uri in self.endpoints:
+ response = self._request(host="attacker.test")
+
+ # referer is checked after auth
+ assert_equal(response.status, 401, self.app_uri)
+
+ def notest_endpoints_invalid(self):
+ """Pass in a bad Referer, expect a 400 Bad Request"""
+ self.endpoints = {
+ "/ipa/session/login_password",
+ "/ipa/session/change_password",
+ "/ipa/session/sync_token",
+ }
+ for self.app_uri in self.endpoints:
+ response = self._request(host="attacker.test")
+
+ assert_equal(response.status, 400, self.app_uri)
diff --git a/ipatests/util.py b/ipatests/util.py
index 5c0152b90..c69d98790 100644
--- a/ipatests/util.py
+++ b/ipatests/util.py
@@ -163,12 +163,12 @@ class ExceptionNotRaised(Exception):
return self.msg % self.expected.__name__
-def assert_equal(val1, val2):
+def assert_equal(val1, val2, msg=''):
"""
Assert ``val1`` and ``val2`` are the same type and of equal value.
"""
assert type(val1) is type(val2), '%r != %r' % (val1, val2)
- assert val1 == val2, '%r != %r' % (val1, val2)
+ assert val1 == val2, '%r != %r %r' % (val1, val2, msg)
def assert_not_equal(val1, val2):
--
2.41.0

View File

@ -0,0 +1,265 @@
From 013be398bced31f567ef01ac2471cb7529789b4a Mon Sep 17 00:00:00 2001
From: Julien Rische <jrische@redhat.com>
Date: Mon, 9 Oct 2023 15:47:03 +0200
Subject: [PATCH] ipa-kdb: Detect and block Bronze-Bit attacks
The C8S/RHEL8 version of FreeIPA is vulnerable to the Bronze-Bit attack
because it does not implement PAC ticket signature to protect the
"forwardable" flag. However, it does implement the PAC extended KDC
signature, which protects against PAC spoofing.
Based on information available in the PAC and the
"ok-to-auth-as-delegate" attribute in the database. It is possible to
detect and reject requests where the "forwardable" flag was flipped by
the attacker in the evidence ticket.
---
daemons/ipa-kdb/ipa_kdb.h | 13 +++
daemons/ipa-kdb/ipa_kdb_kdcpolicy.c | 6 +
daemons/ipa-kdb/ipa_kdb_mspac.c | 173 ++++++++++++++++++++++++++++
ipaserver/install/server/install.py | 8 ++
4 files changed, 200 insertions(+)
diff --git a/daemons/ipa-kdb/ipa_kdb.h b/daemons/ipa-kdb/ipa_kdb.h
index 7aa5be494..02b2cb631 100644
--- a/daemons/ipa-kdb/ipa_kdb.h
+++ b/daemons/ipa-kdb/ipa_kdb.h
@@ -367,6 +367,19 @@ krb5_error_code ipadb_is_princ_from_trusted_realm(krb5_context kcontext,
const char *test_realm, size_t size,
char **trusted_realm);
+/* Try to detect a Bronze-Bit attack based on the content of the request and
+ * data from the KDB.
+ *
+ * context krb5 context
+ * request KDB request
+ * detected Set to "true" if a bronze bit attack is detected and the
+ * pointer is not NULL. Remains unset otherwise.
+ * status If the call fails and the pointer is not NULL, set it with a
+ * message describing the cause of the failure. */
+krb5_error_code
+ipadb_check_for_bronze_bit_attack(krb5_context context, krb5_kdc_req *request,
+ bool *detected, const char **status);
+
/* DELEGATION CHECKS */
krb5_error_code ipadb_check_allowed_to_delegate(krb5_context kcontext,
diff --git a/daemons/ipa-kdb/ipa_kdb_kdcpolicy.c b/daemons/ipa-kdb/ipa_kdb_kdcpolicy.c
index f2804c9b2..1032dff0b 100644
--- a/daemons/ipa-kdb/ipa_kdb_kdcpolicy.c
+++ b/daemons/ipa-kdb/ipa_kdb_kdcpolicy.c
@@ -185,6 +185,12 @@ ipa_kdcpolicy_check_tgs(krb5_context context, krb5_kdcpolicy_moddata moddata,
const char **status, krb5_deltat *lifetime_out,
krb5_deltat *renew_lifetime_out)
{
+ krb5_error_code kerr;
+
+ kerr = ipadb_check_for_bronze_bit_attack(context, request, NULL, status);
+ if (kerr)
+ return KRB5KDC_ERR_POLICY;
+
*status = NULL;
*lifetime_out = 0;
*renew_lifetime_out = 0;
diff --git a/daemons/ipa-kdb/ipa_kdb_mspac.c b/daemons/ipa-kdb/ipa_kdb_mspac.c
index 83cb9914d..b4e22d431 100644
--- a/daemons/ipa-kdb/ipa_kdb_mspac.c
+++ b/daemons/ipa-kdb/ipa_kdb_mspac.c
@@ -3298,3 +3298,176 @@ krb5_error_code ipadb_is_princ_from_trusted_realm(krb5_context kcontext,
return KRB5_KDB_NOENTRY;
}
+
+krb5_error_code
+ipadb_check_for_bronze_bit_attack(krb5_context context, krb5_kdc_req *request,
+ bool *detected, const char **status)
+{
+ krb5_error_code kerr;
+ const char *st = NULL;
+ size_t i, j;
+ krb5_ticket *evidence_tkt;
+ krb5_authdata **authdata, **ifrel = NULL;
+ krb5_pac pac = NULL;
+ TALLOC_CTX *tmpctx = NULL;
+ krb5_data fullsign = { 0, 0, NULL }, linfo_blob = { 0, 0, NULL };
+ DATA_BLOB linfo_data;
+ struct PAC_LOGON_INFO_CTR linfo;
+ enum ndr_err_code ndr_err;
+ struct dom_sid asserted_identity_sid;
+ bool evtkt_is_s4u2self = false;
+ krb5_db_entry *proxy_entry = NULL;
+
+ /* If no additional ticket, this is not a constrained delegateion request.
+ * Skip checks. */
+ if (!(request->kdc_options & KDC_OPT_CNAME_IN_ADDL_TKT)) {
+ kerr = 0;
+ goto end;
+ }
+
+ evidence_tkt = request->second_ticket[0];
+
+ /* No need to check the Forwardable flag. If it was not set, this request
+ * would have failed earlier. */
+
+ /* We only support general constrained delegation (not RBCD), which is not
+ * available for cross-realms. */
+ if (!krb5_realm_compare(context, evidence_tkt->server, request->server)) {
+ st = "S4U2PROXY_NOT_SUPPORTED_FOR_CROSS_REALMS";
+ kerr = ENOTSUP;
+ goto end;
+ }
+
+ authdata = evidence_tkt->enc_part2->authorization_data;
+
+ /* Search for the PAC. */
+ for (i = 0; authdata != NULL && authdata[i] != NULL; i++) {
+ if (authdata[i]->ad_type != KRB5_AUTHDATA_IF_RELEVANT)
+ continue;
+
+ kerr = krb5_decode_authdata_container(context,
+ KRB5_AUTHDATA_IF_RELEVANT,
+ authdata[i], &ifrel);
+ if (kerr) {
+ st = "S4U2PROXY_CANNOT_DECODE_EVIDENCE_TKT_AUTHDATA";
+ goto end;
+ }
+
+ for (j = 0; ifrel[j] != NULL; j++) {
+ if (ifrel[j]->ad_type == KRB5_AUTHDATA_WIN2K_PAC)
+ break;
+ }
+ if (ifrel[j] != NULL)
+ break;
+
+ krb5_free_authdata(context, ifrel);
+ ifrel = NULL;
+ }
+
+ if (ifrel == NULL) {
+ st = "S4U2PROXY_EVIDENCE_TKT_WITHOUT_PAC";
+ kerr = ENOENT;
+ goto end;
+ }
+
+ /* Parse the PAC. */
+ kerr = krb5_pac_parse(context, ifrel[j]->contents, ifrel[j]->length, &pac);
+ if (kerr) {
+ st = "S4U2PROXY_CANNOT_DECODE_EVICENCE_TKT_PAC";
+ goto end;
+ }
+
+ /* Check that the PAC extanded KDC signature is present. If it is, it was
+ * already tested.
+ * If absent, the context of the PAC cannot be trusted. */
+ kerr = krb5_pac_get_buffer(context, pac, KRB5_PAC_FULL_CHECKSUM, &fullsign);
+ if (kerr) {
+ st = "S4U2PROXY_MISSING_EXTENDED_KDC_SIGN_IN_EVIDENCE_TKT_PAC";
+ goto end;
+ }
+
+ /* Get the PAC Logon Info. */
+ kerr = krb5_pac_get_buffer(context, pac, KRB5_PAC_LOGON_INFO, &linfo_blob);
+ if (kerr) {
+ st = "S4U2PROXY_NO_PAC_LOGON_INFO_IN_EVIDENCE_TKT";
+ goto end;
+ }
+
+ /* Parse the PAC Logon Info. */
+ tmpctx = talloc_new(NULL);
+ if (!tmpctx) {
+ st = "OUT_OF_MEMORY";
+ kerr = ENOMEM;
+ goto end;
+ }
+
+ linfo_data.length = linfo_blob.length;
+ linfo_data.data = (uint8_t *)linfo_blob.data;
+ ndr_err = ndr_pull_union_blob(&linfo_data, tmpctx, &linfo,
+ PAC_TYPE_LOGON_INFO,
+ (ndr_pull_flags_fn_t)ndr_pull_PAC_INFO);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ st = "S4U2PROXY_CANNOT_PARSE_ENVIDENCE_TKT_PAC_LOGON_INFO";
+ kerr = EINVAL;
+ goto end;
+ }
+
+ /* Check that the extra SIDs array is not empty. */
+ if (linfo.info->info3.sidcount == 0) {
+ st = "S4U2PROXY_NO_EXTRA_SID";
+ kerr = ENOENT;
+ goto end;
+ }
+
+ /* Search for the S-1-18-2 domain SID, which indicates the ticket was
+ * obtained using S4U2Self */
+ kerr = ipadb_string_to_sid("S-1-18-2", &asserted_identity_sid);
+ if (kerr) {
+ st = "S4U2PROXY_CANNOT_CREATE_ASSERTED_IDENTITY_SID";
+ goto end;
+ }
+
+ for (i = 0; i < linfo.info->info3.sidcount; i++) {
+ if (dom_sid_check(&asserted_identity_sid,
+ linfo.info->info3.sids[0].sid, true)) {
+ evtkt_is_s4u2self = true;
+ break;
+ }
+ }
+
+ /* If the ticket was obtained using S4U2Self, the proxy principal entry must
+ * have the "ok_to_auth_as_delegate" attribute set to true. */
+ if (evtkt_is_s4u2self) {
+ kerr = ipadb_get_principal(context, evidence_tkt->server, 0,
+ &proxy_entry);
+ if (kerr) {
+ st = "S4U2PROXY_CANNOT_FIND_PROXY_PRINCIPAL";
+ goto end;
+ }
+
+ if (!(proxy_entry->attributes & KRB5_KDB_OK_TO_AUTH_AS_DELEGATE)) {
+ /* This evidence ticket cannot be forwardable given the privileges
+ * of the proxy principal.
+ * This is a Bronze Bit attack. */
+ if (detected)
+ *detected = true;
+ st = "S4U2PROXY_BRONZE_BIT_ATTACK_DETECTED";
+ kerr = EBADE;
+ goto end;
+ }
+ }
+
+ kerr = 0;
+
+end:
+ if (st && status)
+ *status = st;
+
+ krb5_free_authdata(context, ifrel);
+ krb5_pac_free(context, pac);
+ krb5_free_data_contents(context, &linfo_blob);
+ krb5_free_data_contents(context, &fullsign);
+ talloc_free(tmpctx);
+ ipadb_free_principal(context, proxy_entry);
+ return kerr;
+}
diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py
index 4e4076410..bfbb83bcb 100644
--- a/ipaserver/install/server/install.py
+++ b/ipaserver/install/server/install.py
@@ -981,6 +981,14 @@ def install(installer):
# Set the admin user kerberos password
ds.change_admin_password(admin_password)
+ # Force KDC to refresh the cached value of ipaKrbAuthzData by restarting.
+ # ipaKrbAuthzData has to be set with "MS-PAC" to trigger PAC generation,
+ # which is required to handle S4U2Proxy with the Bronze-Bit fix.
+ # Not doing so would cause API malfunction for around a minute, which is
+ # long enough to cause the hereafter client installation to fail.
+ service.print_msg("Restarting the KDC")
+ krb.restart()
+
# Call client install script
service.print_msg("Configuring client side components")
try:
--
2.41.0

View File

@ -189,7 +189,7 @@
Name: %{package_name}
Version: %{IPA_VERSION}
Release: 2%{?rc_version:.%rc_version}%{?dist}
Release: 3%{?rc_version:.%rc_version}%{?dist}
Summary: The Identity, Policy and Audit system
License: GPLv3+
@ -209,6 +209,9 @@ Source1: https://releases.pagure.org/freeipa/freeipa-%{version}%{?rc_vers
# RHEL spec file only: START
%if %{NON_DEVELOPER_BUILD}
Patch0001: 0001-Handle-samba-exception-type-change_rhel#17623.patch
Patch0002: 0002-Check-the-HTTP-Referer-header-on-all-requests.patch
Patch0003: 0003-Integration-tests-for-verifying-Referer-header-in-th.patch
Patch0004: 0004-ipa-kdb-Detect-and-block-Bronze-Bit-attacks.patch
%if 0%{?rhel} >= 8
Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch
Patch1002: 1002-Revert-freeipa.spec-depend-on-bind-dnssec-utils.patch
@ -1724,6 +1727,12 @@ fi
%endif
%changelog
* Wed Jan 10 2024 Julien Rische <jrische@redhat.com> - 4.9.13-3
- ipa-kdb: Detect and block Bronze-Bit attacks
Resolves: RHEL-9984
- Fix for CVE-2023-5455
Resolves: RHEL-12578
* Thu Nov 30 2023 Rafael Jeffman <rjeffman@redhat.com> - 4.9.13-2
- Handle new samba exception types.
Resolves: RHEL-17623