- CVE-2023-5455
- ipa-kdb: Detect and block Bronze-Bit attacks
This commit is contained in:
parent
697efbdf54
commit
e82ab6efc0
118
SOURCES/0015-CVE-2023-5455.patch
Normal file
118
SOURCES/0015-CVE-2023-5455.patch
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
From 13778d88ca2ac73b729821bdea844172a18c0cb9 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 b7116469d73..198fc9e7dba 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
|
262
SOURCES/0016-ipa-kdb-Detect-and-block-Bronze-Bit-attacks.patch
Normal file
262
SOURCES/0016-ipa-kdb-Detect-and-block-Bronze-Bit-attacks.patch
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
From a847e2483b4c4832ee5129901da169f4eb0d1392 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 7aa5be4948e..02b2cb6313e 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 f2804c9b23a..1032dff0b5c 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 83cb9914d23..b4e22d4316a 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 4e4076410f1..bfbb83bcbfa 100644
|
||||||
|
--- a/ipaserver/install/server/install.py
|
||||||
|
+++ b/ipaserver/install/server/install.py
|
||||||
|
@@ -978,6 +978,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:
|
356
SOURCES/0017-Integration-tests-for-verifying-Referer.patch
Normal file
356
SOURCES/0017-Integration-tests-for-verifying-Referer.patch
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
From 86b073a7f03ba0edf4dd91f85b96c89107e9e673 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 6cd034a7196..8924798fc93 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 c3a47ab265f..df38ddb3d9e 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 00000000000..9425cb7977f
|
||||||
|
--- /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 00000000000..4eade8bbaf3
|
||||||
|
--- /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 929c3e899c3..61af0c40d07 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):
|
@ -189,7 +189,7 @@
|
|||||||
|
|
||||||
Name: %{package_name}
|
Name: %{package_name}
|
||||||
Version: %{IPA_VERSION}
|
Version: %{IPA_VERSION}
|
||||||
Release: 9%{?rc_version:.%rc_version}%{?dist}.alma.1
|
Release: 11%{?rc_version:.%rc_version}%{?dist}.alma.1
|
||||||
Summary: The Identity, Policy and Audit system
|
Summary: The Identity, Policy and Audit system
|
||||||
|
|
||||||
License: GPLv3+
|
License: GPLv3+
|
||||||
@ -226,6 +226,12 @@ Patch0013: 0013-Installer-activate-nss-and-pam-services-in-sssd.conf_rhbz#2
|
|||||||
# Patches were taken from:
|
# Patches were taken from:
|
||||||
# https://gitlab.com/redhat/centos-stream/rpms/ipa/-/commit/5d0ca0e625aea2553a39ae3e56174285cb123f13
|
# https://gitlab.com/redhat/centos-stream/rpms/ipa/-/commit/5d0ca0e625aea2553a39ae3e56174285cb123f13
|
||||||
Patch0014: 0014-ipa-kdb-Make-AD-SIGNEDPATH-optional-with-krb5-DAL-8.patch
|
Patch0014: 0014-ipa-kdb-Make-AD-SIGNEDPATH-optional-with-krb5-DAL-8.patch
|
||||||
|
# https://github.com/freeipa/freeipa/commit/13778d88ca2ac73b729821bdea844172a18c0cb9
|
||||||
|
Patch0015: 0015-CVE-2023-5455.patch
|
||||||
|
# https://github.com/freeipa/freeipa/commit/a847e2483b4c4832ee5129901da169f4eb0d1392
|
||||||
|
Patch0016: 0016-ipa-kdb-Detect-and-block-Bronze-Bit-attacks.patch
|
||||||
|
# https://github.com/freeipa/freeipa/commit/86b073a7f03ba0edf4dd91f85b96c89107e9e673
|
||||||
|
Patch0017: 0017-Integration-tests-for-verifying-Referer.patch
|
||||||
|
|
||||||
Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch
|
Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch
|
||||||
Patch1002: 1002-Revert-freeipa.spec-depend-on-bind-dnssec-utils.patch
|
Patch1002: 1002-Revert-freeipa.spec-depend-on-bind-dnssec-utils.patch
|
||||||
@ -1741,6 +1747,11 @@ fi
|
|||||||
%endif
|
%endif
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Mon Jan 15 2024 Eduard Abdullin <eabdullin@almalinux.org> - 4.9.12-11.alma.1
|
||||||
|
- CVE-2023-5455
|
||||||
|
- ipa-kdb: Detect and block Bronze-Bit attacks
|
||||||
|
- Integration tests for verifying Referer header in the UI
|
||||||
|
|
||||||
* Wed Nov 14 2023 Eduard Abdullin <eabdullin@almalinux.org> - 4.9.12-9.alma.1
|
* Wed Nov 14 2023 Eduard Abdullin <eabdullin@almalinux.org> - 4.9.12-9.alma.1
|
||||||
- ipa-kdb: Make AD-SIGNEDPATH optional with krb5 DAL 8 and older
|
- ipa-kdb: Make AD-SIGNEDPATH optional with krb5 DAL 8 and older
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user