From b8a81c06357f0360dbb3b59b1da1108bd1574810 Mon Sep 17 00:00:00 2001
From: Andrew Bartlett <abartlet@samba.org>
Date: Fri, 1 Oct 2021 16:14:37 +1300
Subject: [PATCH] CVE-2020-25718 kdc: Confirm the RODC was allowed to issue a
 particular ticket

BUG: https://bugzilla.samba.org/show_bug.cgi?id=14558

Signed-off-by: Andrew Bartlett <abartlet@samba.org>
Reviewed-by: Joseph Sutton <josephsutton@catalyst.net.nz>
Conflict: remove selftest/knownfail_heimdal_kdc
---
 source4/auth/sam.c                            |   5 +-
 source4/dsdb/common/rodc_helper.c             |  47 ++++----
 source4/kdc/mit_samba.c                       |   6 +-
 source4/kdc/pac-glue.c                        | 106 +++++++++++++++++-
 source4/kdc/pac-glue.h                        |  12 +-
 source4/kdc/wdc-samba4.c                      |  40 ++++++-
 source4/rpc_server/drsuapi/getncchanges.c     |  11 +-
 source4/rpc_server/netlogon/dcerpc_netlogon.c |   1 +
 8 files changed, 187 insertions(+), 41 deletions(-)

diff --git a/source4/auth/sam.c b/source4/auth/sam.c
index 39e48c2..93b41be 100644
--- a/source4/auth/sam.c
+++ b/source4/auth/sam.c
@@ -57,7 +57,10 @@
 						\
 	"pwdLastSet",				\
 	"msDS-UserPasswordExpiryTimeComputed",	\
-	"accountExpires"
+	"accountExpires",			\
+						\
+	/* Needed for RODC rule processing */	\
+	"msDS-KrbTgtLinkBL"
 
 const char *krbtgt_attrs[] = {
 	KRBTGT_ATTRS, NULL
diff --git a/source4/dsdb/common/rodc_helper.c b/source4/dsdb/common/rodc_helper.c
index 3a9636a..39f3b59 100644
--- a/source4/dsdb/common/rodc_helper.c
+++ b/source4/dsdb/common/rodc_helper.c
@@ -47,19 +47,18 @@ bool sid_list_match(uint32_t num_sids1,
 
 /*
  * Return an array of SIDs from a ldb_message given an attribute name assumes
- * the SIDs are in NDR form (with additional sids applied on the end).
+ * the SIDs are in NDR form (with primary_sid applied on the start).
  */
-WERROR samdb_result_sid_array_ndr(struct ldb_context *sam_ctx,
-				  struct ldb_message *msg,
-				  TALLOC_CTX *mem_ctx,
-				  const char *attr,
-				  uint32_t *num_sids,
-				  struct dom_sid **sids,
-				  const struct dom_sid *additional_sids,
-				  unsigned int num_additional)
+static WERROR samdb_result_sid_array_ndr(struct ldb_context *sam_ctx,
+					 struct ldb_message *msg,
+					 TALLOC_CTX *mem_ctx,
+					 const char *attr,
+					 uint32_t *num_sids,
+					 struct dom_sid **sids,
+					 const struct dom_sid *primary_sid)
 {
 	struct ldb_message_element *el;
-	unsigned int i, j;
+	unsigned int i;
 
 	el = ldb_msg_find_element(msg, attr);
 	if (!el) {
@@ -69,24 +68,25 @@ WERROR samdb_result_sid_array_ndr(struct ldb_context *sam_ctx,
 
 	/* Make array long enough for NULL and additional SID */
 	(*sids) = talloc_array(mem_ctx, struct dom_sid,
-			       el->num_values + num_additional);
+			       el->num_values + 1);
 	W_ERROR_HAVE_NO_MEMORY(*sids);
 
-	for (i=0; i<el->num_values; i++) {
+	(*sids)[0] = *primary_sid;
+
+	for (i = 0; i<el->num_values; i++) {
 		enum ndr_err_code ndr_err;
+		struct dom_sid sid = { 0, };
 
-		ndr_err = ndr_pull_struct_blob_all_noalloc(&el->values[i], &(*sids)[i],
+		ndr_err = ndr_pull_struct_blob_all_noalloc(&el->values[i], &sid,
 					       (ndr_pull_flags_fn_t)ndr_pull_dom_sid);
 		if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
 			return WERR_INTERNAL_DB_CORRUPTION;
 		}
+		/* Primary SID is already in position zero. */
+		(*sids)[i+1] = sid;
 	}
 
-	for (j = 0; j < num_additional; j++) {
-		(*sids)[i++] = additional_sids[j];
-	}
-
-	*num_sids = i;
+	*num_sids = i+1;
 
 	return WERR_OK;
 }
@@ -131,6 +131,7 @@ WERROR samdb_result_sid_array_dn(struct ldb_context *sam_ctx,
 }
 
 WERROR samdb_confirm_rodc_allowed_to_repl_to_sid_list(struct ldb_context *sam_ctx,
+						      const struct dom_sid *rodc_machine_account_sid,
 						      struct ldb_message *rodc_msg,
 						      struct ldb_message *obj_msg,
 						      uint32_t num_token_sids,
@@ -190,6 +191,12 @@ WERROR samdb_confirm_rodc_allowed_to_repl_to_sid_list(struct ldb_context *sam_ct
 		return WERR_DS_DRA_SECRETS_DENIED;
 	}
 
+	/* The RODC can replicate and print tickets for itself. */
+	if (dom_sid_equal(&token_sids[0], rodc_machine_account_sid)) {
+		TALLOC_FREE(frame);
+		return WERR_OK;
+	}
+
 	if (never_reveal_sids &&
 	    sid_list_match(num_token_sids,
 			   token_sids,
@@ -218,6 +225,7 @@ WERROR samdb_confirm_rodc_allowed_to_repl_to_sid_list(struct ldb_context *sam_ct
  * rather than relying on the caller providing those
  */
 WERROR samdb_confirm_rodc_allowed_to_repl_to(struct ldb_context *sam_ctx,
+					     struct dom_sid *rodc_machine_account_sid,
 					     struct ldb_message *rodc_msg,
 					     struct ldb_message *obj_msg)
 {
@@ -244,12 +252,13 @@ WERROR samdb_confirm_rodc_allowed_to_repl_to(struct ldb_context *sam_ctx,
 					  frame, "tokenGroups",
 					  &num_token_sids,
 					  &token_sids,
-					  object_sid, 1);
+					  object_sid);
 	if (!W_ERROR_IS_OK(werr) || token_sids==NULL) {
 		return WERR_DS_DRA_SECRETS_DENIED;
 	}
 
 	werr = samdb_confirm_rodc_allowed_to_repl_to_sid_list(sam_ctx,
+							      rodc_machine_account_sid,
 							      rodc_msg,
 							      obj_msg,
 							      num_token_sids,
diff --git a/source4/kdc/mit_samba.c b/source4/kdc/mit_samba.c
index 04be462..c6fa6af 100644
--- a/source4/kdc/mit_samba.c
+++ b/source4/kdc/mit_samba.c
@@ -440,7 +440,8 @@ int mit_samba_get_pac(struct mit_samba_context *smb_ctx,
 					    &logon_info_blob,
 					    cred_ndr_ptr,
 					    &upn_dns_info_blob,
-					    NULL, NULL);
+					    NULL, NULL,
+					    NULL);
 	if (!NT_STATUS_IS_OK(nt_status)) {
 		talloc_free(tmp_ctx);
 		return EINVAL;
@@ -568,7 +569,8 @@ krb5_error_code mit_samba_reget_pac(struct mit_samba_context *ctx,
 						    &pac_blob,
 						    NULL,
 						    &upn_blob,
-						    NULL, NULL);
+						    NULL, NULL,
+						    NULL);
 		if (!NT_STATUS_IS_OK(nt_status)) {
 			code = EINVAL;
 			goto done;
diff --git a/source4/kdc/pac-glue.c b/source4/kdc/pac-glue.c
index f0d61bb..92171fd 100644
--- a/source4/kdc/pac-glue.c
+++ b/source4/kdc/pac-glue.c
@@ -35,6 +35,7 @@
 #include "libcli/security/security.h"
 #include "dsdb/samdb/samdb.h"
 #include "auth/kerberos/pac_utils.h"
+#include "source4/dsdb/common/util.h"
 
 static
 NTSTATUS samba_get_logon_info_pac_blob(TALLOC_CTX *mem_ctx,
@@ -752,13 +753,19 @@ int samba_krbtgt_is_in_db(struct samba_kdc_entry *p,
 	return 0;
 }
 
+/*
+ * We return not just the blobs, but also the user_info_dc because we
+ * will need, in the RODC case, to confirm that the returned user is
+ * permitted to be replicated to the KDC
+ */
 NTSTATUS samba_kdc_get_pac_blobs(TALLOC_CTX *mem_ctx,
 				 struct samba_kdc_entry *p,
 				 DATA_BLOB **_logon_info_blob,
 				 DATA_BLOB **_cred_ndr_blob,
 				 DATA_BLOB **_upn_info_blob,
 				 DATA_BLOB **_pac_attrs_blob,
-				 const krb5_boolean *pac_request)
+				 const krb5_boolean *pac_request,
+				 struct auth_user_info_dc **_user_info_dc)
 {
 	struct auth_user_info_dc *user_info_dc;
 	DATA_BLOB *logon_blob = NULL;
@@ -856,7 +863,15 @@ NTSTATUS samba_kdc_get_pac_blobs(TALLOC_CTX *mem_ctx,
 		}
 	}
 
-	TALLOC_FREE(user_info_dc);
+	/*
+	 * Return to the caller to allow a check on the allowed/denied
+	 * RODC replication groups
+	 */
+	if (_user_info_dc == NULL) {
+		TALLOC_FREE(user_info_dc);
+	} else {
+		*_user_info_dc = user_info_dc;
+	}
 	*_logon_info_blob = logon_blob;
 	if (_cred_ndr_blob != NULL) {
 		*_cred_ndr_blob = cred_blob;
@@ -1123,3 +1138,90 @@ out:
 	TALLOC_FREE(frame);
 	return code;
 }
+
+
+/*
+ * In the RODC case, to confirm that the returned user is permitted to
+ * be replicated to the KDC (krbgtgt_xxx user) represented by *rodc
+ */
+WERROR samba_rodc_confirm_user_is_allowed(uint32_t num_object_sids,
+					  struct dom_sid *object_sids,
+					  struct samba_kdc_entry *rodc,
+					  struct samba_kdc_entry *object)
+{
+	int ret;
+	WERROR werr;
+	TALLOC_CTX *frame = talloc_stackframe();
+	const char *rodc_attrs[] = { "msDS-KrbTgtLink",
+				     "msDS-NeverRevealGroup",
+				     "msDS-RevealOnDemandGroup",
+				     "userAccountControl",
+				     "objectSid",
+				     NULL };
+	struct ldb_result *rodc_machine_account = NULL;
+	struct ldb_dn *rodc_machine_account_dn = samdb_result_dn(rodc->kdc_db_ctx->samdb,
+						 frame,
+						 rodc->msg,
+						 "msDS-KrbTgtLinkBL",
+						 NULL);
+	const struct dom_sid *rodc_machine_account_sid = NULL;
+
+	if (rodc_machine_account_dn == NULL) {
+		DBG_ERR("krbtgt account %s has no msDS-KrbTgtLinkBL to find RODC machine account for allow/deny list\n",
+			ldb_dn_get_linearized(rodc->msg->dn));
+		TALLOC_FREE(frame);
+		return WERR_DS_DRA_BAD_DN;
+	}
+
+	/*
+	 * Follow the link and get the RODC account (the krbtgt
+	 * account is the krbtgt_XXX account, but the
+	 * msDS-NeverRevealGroup and msDS-RevealOnDemandGroup is on
+	 * the RODC$ account)
+	 *
+	 * We need DSDB_SEARCH_SHOW_EXTENDED_DN as we get a SID lists
+	 * out of the extended DNs
+	 */
+
+	ret = dsdb_search_dn(rodc->kdc_db_ctx->samdb,
+			     frame,
+			     &rodc_machine_account,
+			     rodc_machine_account_dn,
+			     rodc_attrs,
+			     DSDB_SEARCH_SHOW_EXTENDED_DN);
+	if (ret != LDB_SUCCESS) {
+		DBG_ERR("Failed to fetch RODC machine account %s pointed to by %s to check allow/deny list: %s\n",
+			ldb_dn_get_linearized(rodc_machine_account_dn),
+			ldb_dn_get_linearized(rodc->msg->dn),
+			ldb_errstring(rodc->kdc_db_ctx->samdb));
+		TALLOC_FREE(frame);
+		return WERR_DS_DRA_BAD_DN;
+	}
+
+	if (rodc_machine_account->count != 1) {
+		DBG_ERR("Failed to fetch RODC machine account %s pointed to by %s to check allow/deny list: (%d)\n",
+			ldb_dn_get_linearized(rodc_machine_account_dn),
+			ldb_dn_get_linearized(rodc->msg->dn),
+			rodc_machine_account->count);
+		TALLOC_FREE(frame);
+		return WERR_DS_DRA_BAD_DN;
+	}
+
+	/* if the object SID is equal to the user_sid, allow */
+	rodc_machine_account_sid = samdb_result_dom_sid(frame,
+					  rodc_machine_account->msgs[0],
+					  "objectSid");
+	if (rodc_machine_account_sid == NULL) {
+		return WERR_DS_DRA_BAD_DN;
+	}
+
+	werr = samdb_confirm_rodc_allowed_to_repl_to_sid_list(rodc->kdc_db_ctx->samdb,
+							      rodc_machine_account_sid,
+							      rodc_machine_account->msgs[0],
+							      object->msg,
+							      num_object_sids,
+							      object_sids);
+
+	TALLOC_FREE(frame);
+	return werr;
+}
diff --git a/source4/kdc/pac-glue.h b/source4/kdc/pac-glue.h
index 1b6264c..3b761f4 100644
--- a/source4/kdc/pac-glue.h
+++ b/source4/kdc/pac-glue.h
@@ -52,7 +52,8 @@ NTSTATUS samba_kdc_get_pac_blobs(TALLOC_CTX *mem_ctx,
 				 DATA_BLOB **_cred_ndr_blob,
 				 DATA_BLOB **_upn_info_blob,
 				 DATA_BLOB **_pac_attrs_blob,
-				 const krb5_boolean *pac_request);
+				 const krb5_boolean *pac_request,
+				 struct auth_user_info_dc **_user_info_dc);
 NTSTATUS samba_kdc_get_pac_blob(TALLOC_CTX *mem_ctx,
 				struct samba_kdc_entry *skdc_entry,
 				DATA_BLOB **_logon_info_blob);
@@ -82,3 +83,12 @@ krb5_error_code samba_kdc_validate_pac_blob(
 		krb5_context context,
 		struct samba_kdc_entry *client_skdc_entry,
 		const krb5_pac pac);
+
+/*
+ * In the RODC case, to confirm that the returned user is permitted to
+ * be replicated to the KDC (krbgtgt_xxx user) represented by *rodc
+ */
+WERROR samba_rodc_confirm_user_is_allowed(uint32_t num_sids,
+					  struct dom_sid *sids,
+					  struct samba_kdc_entry *rodc,
+					  struct samba_kdc_entry *object);
diff --git a/source4/kdc/wdc-samba4.c b/source4/kdc/wdc-samba4.c
index b73dc84..51640b1 100644
--- a/source4/kdc/wdc-samba4.c
+++ b/source4/kdc/wdc-samba4.c
@@ -27,6 +27,7 @@
 #include "kdc/pac-glue.h"
 #include "sdb.h"
 #include "sdb_hdb.h"
+#include "librpc/gen_ndr/auth.h"
 
 /*
  * Given the right private pointer from hdb_samba4,
@@ -68,7 +69,8 @@ static krb5_error_code samba_wdc_get_pac(void *priv, krb5_context context,
 					    cred_ndr_ptr,
 					    &upn_blob,
 					    &pac_attrs_blob,
-					    pac_request);
+					    pac_request,
+					    NULL);
 	if (!NT_STATUS_IS_OK(nt_status)) {
 		talloc_free(mem_ctx);
 		return EINVAL;
@@ -161,9 +163,15 @@ static krb5_error_code samba_wdc_reget_pac2(krb5_context context,
 		}
 	}
 
-	/* If the krbtgt was generated by an RODC, and we are not that
+	/*
+	 * If the krbtgt was generated by an RODC, and we are not that
 	 * RODC, then we need to regenerate the PAC - we can't trust
-	 * it */
+	 * it, and confirm that the RODC was permitted to print this ticket
+	 *
+	 * Becasue of the samba_kdc_validate_pac_blob() step we can be
+	 * sure that the record in 'client' matches the SID in the
+	 * original PAC.
+	 */
 	ret = samba_krbtgt_is_in_db(krbtgt_skdc_entry, &is_in_db, &is_untrusted);
 	if (ret != 0) {
 		talloc_free(mem_ctx);
@@ -237,6 +245,8 @@ static krb5_error_code samba_wdc_reget_pac2(krb5_context context,
 
 	if (is_untrusted) {
 		struct samba_kdc_entry *client_skdc_entry = NULL;
+		struct auth_user_info_dc *user_info_dc = NULL;
+		WERROR werr;
 
 		if (client == NULL) {
 			return KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN;
@@ -247,12 +257,30 @@ static krb5_error_code samba_wdc_reget_pac2(krb5_context context,
 
 		nt_status = samba_kdc_get_pac_blobs(mem_ctx, client_skdc_entry,
 						    &pac_blob, NULL, &upn_blob,
-						    NULL, NULL);
+						    NULL, NULL,
+						    &user_info_dc);
 		if (!NT_STATUS_IS_OK(nt_status)) {
 			talloc_free(mem_ctx);
-			return EINVAL;
+			return KRB5KDC_ERR_TGT_REVOKED;
+		}
+
+		/*
+		 * Now check if the SID list in the user_info_dc
+		 * intersects correctly with the RODC allow/deny
+		 * lists
+		 */
+
+		werr = samba_rodc_confirm_user_is_allowed(user_info_dc->num_sids,
+							  user_info_dc->sids,
+							  krbtgt_skdc_entry,
+							  client_skdc_entry);
+		if (!W_ERROR_IS_OK(werr)) {
+			talloc_free(mem_ctx);
+			return KRB5KDC_ERR_TGT_REVOKED;
 		}
-	} else {
+	}
+
+	if (!is_untrusted) {
 		pac_blob = talloc_zero(mem_ctx, DATA_BLOB);
 		if (!pac_blob) {
 			talloc_free(mem_ctx);
diff --git a/source4/rpc_server/drsuapi/getncchanges.c b/source4/rpc_server/drsuapi/getncchanges.c
index 0d36a94..7084801 100644
--- a/source4/rpc_server/drsuapi/getncchanges.c
+++ b/source4/rpc_server/drsuapi/getncchanges.c
@@ -1177,7 +1177,6 @@ static WERROR getncchanges_repl_secret(struct drsuapi_bind_state *b_state,
 				     NULL };
 	const char *obj_attrs[] = { "tokenGroups", "objectSid", "UserAccountControl", "msDS-KrbTgtLinkBL", NULL };
 	struct ldb_result *rodc_res = NULL, *obj_res = NULL;
-	const struct dom_sid *object_sid = NULL;
 	WERROR werr;
 
 	DEBUG(3,(__location__ ": DRSUAPI_EXOP_REPL_SECRET extended op on %s\n",
@@ -1261,15 +1260,6 @@ static WERROR getncchanges_repl_secret(struct drsuapi_bind_state *b_state,
 	ret = dsdb_search_dn(b_state->sam_ctx_system, mem_ctx, &obj_res, obj_dn, obj_attrs, 0);
 	if (ret != LDB_SUCCESS || obj_res->count != 1) goto failed;
 
-	/* if the object SID is equal to the user_sid, allow */
-	object_sid = samdb_result_dom_sid(mem_ctx, obj_res->msgs[0], "objectSid");
-	if (object_sid == NULL) {
-		goto failed;
-	}
-	if (dom_sid_equal(user_sid, object_sid)) {
-		goto allowed;
-	}
-
 	/*
 	 * Must be an RODC account at this point, verify machine DN matches the
 	 * SID account
@@ -1287,6 +1277,7 @@ static WERROR getncchanges_repl_secret(struct drsuapi_bind_state *b_state,
 	}
 
 	werr = samdb_confirm_rodc_allowed_to_repl_to(b_state->sam_ctx_system,
+						     user_sid,
 						     rodc_res->msgs[0],
 						     obj_res->msgs[0]);
 
diff --git a/source4/rpc_server/netlogon/dcerpc_netlogon.c b/source4/rpc_server/netlogon/dcerpc_netlogon.c
index 11e8280..171b269 100644
--- a/source4/rpc_server/netlogon/dcerpc_netlogon.c
+++ b/source4/rpc_server/netlogon/dcerpc_netlogon.c
@@ -2810,6 +2810,7 @@ static bool sam_rodc_access_check(struct ldb_context *sam_ctx,
 	if (ret != LDB_SUCCESS || obj_res->count != 1) goto denied;
 
 	werr = samdb_confirm_rodc_allowed_to_repl_to(sam_ctx,
+						     user_sid,
 						     rodc_res->msgs[0],
 						     obj_res->msgs[0]);
 
-- 
2.27.0

