Skip to content

Commit e1caf6f

Browse files
committed
Fix flaws in LDAP DN checking
KDB_TL_USER_INFO tl-data is intended to be internal to the LDAP KDB module, and not used in disk or wire principal entries. Prevent kadmin clients from sending KDB_TL_USER_INFO tl-data by giving it a type number less than 256 and filtering out type numbers less than 256 in kadm5_create_principal_3(). (We already filter out low type numbers in kadm5_modify_principal()). In the LDAP KDB module, if containerdn and linkdn are both specified in a put_principal operation, check both linkdn and the computed standalone_principal_dn for container membership. To that end, factor out the checks into helper functions and call them on all applicable client-influenced DNs. CVE-2018-5729: In MIT krb5 1.6 or later, an authenticated kadmin user with permission to add principals to an LDAP Kerberos database can cause a null dereference in kadmind, or circumvent a DN container check, by supplying tagged data intended to be internal to the database module. Thanks to Sharwan Ram and Pooja Anil for discovering the potential null dereference. CVE-2018-5730: In MIT krb5 1.6 or later, an authenticated kadmin user with permission to add principals to an LDAP Kerberos database can circumvent a DN containership check by supplying both a "linkdn" and "containerdn" database argument, or by supplying a DN string which is a left extension of a container DN string but is not hierarchically within the container DN. ticket: 8643 (new) tags: pullup target_version: 1.16-next target_version: 1.15-next
1 parent b1367ab commit e1caf6f

File tree

4 files changed

+125
-95
lines changed

4 files changed

+125
-95
lines changed

Diff for: src/lib/kadm5/srv/svr_principal.c

+7
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,13 @@ kadm5_create_principal_3(void *server_handle,
330330
return KADM5_BAD_MASK;
331331
if((mask & ~ALL_PRINC_MASK))
332332
return KADM5_BAD_MASK;
333+
if (mask & KADM5_TL_DATA) {
334+
for (tl_data_tail = entry->tl_data; tl_data_tail != NULL;
335+
tl_data_tail = tl_data_tail->tl_data_next) {
336+
if (tl_data_tail->tl_data_type < 256)
337+
return KADM5_BAD_TL_TYPE;
338+
}
339+
}
333340

334341
/*
335342
* Check to see if the principal exists

Diff for: src/plugins/kdb/ldap/libkdb_ldap/kdb_ldap.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ extern int set_ldap_error (krb5_context ctx, int st, int op);
141141
#define UNSTORE16_INT(ptr, val) (val = load_16_be(ptr))
142142
#define UNSTORE32_INT(ptr, val) (val = load_32_be(ptr))
143143

144-
#define KDB_TL_USER_INFO 0x7ffe
144+
#define KDB_TL_USER_INFO 0xff
145145

146146
#define KDB_TL_PRINCTYPE 0x01
147147
#define KDB_TL_PRINCCOUNT 0x02

Diff for: src/plugins/kdb/ldap/libkdb_ldap/ldap_principal2.c

+106-94
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,107 @@ update_ldap_mod_auth_ind(krb5_context context, krb5_db_entry *entry,
651651
return ret;
652652
}
653653

654+
static krb5_error_code
655+
check_dn_in_container(krb5_context context, const char *dn,
656+
char *const *subtrees, unsigned int ntrees)
657+
{
658+
unsigned int i;
659+
size_t dnlen = strlen(dn), stlen;
660+
661+
for (i = 0; i < ntrees; i++) {
662+
if (subtrees[i] == NULL || *subtrees[i] == '\0')
663+
return 0;
664+
stlen = strlen(subtrees[i]);
665+
if (dnlen >= stlen &&
666+
strcasecmp(dn + dnlen - stlen, subtrees[i]) == 0 &&
667+
(dnlen == stlen || dn[dnlen - stlen - 1] == ','))
668+
return 0;
669+
}
670+
671+
k5_setmsg(context, EINVAL, _("DN is out of the realm subtree"));
672+
return EINVAL;
673+
}
674+
675+
static krb5_error_code
676+
check_dn_exists(krb5_context context,
677+
krb5_ldap_server_handle *ldap_server_handle,
678+
const char *dn, krb5_boolean nonkrb_only)
679+
{
680+
krb5_error_code st = 0, tempst;
681+
krb5_ldap_context *ldap_context = context->dal_handle->db_context;
682+
LDAP *ld = ldap_server_handle->ldap_handle;
683+
LDAPMessage *result = NULL, *ent;
684+
char *attrs[] = { "krbticketpolicyreference", "krbprincipalname", NULL };
685+
char **values;
686+
687+
LDAP_SEARCH_1(dn, LDAP_SCOPE_BASE, 0, attrs, IGNORE_STATUS);
688+
if (st != LDAP_SUCCESS)
689+
return set_ldap_error(context, st, OP_SEARCH);
690+
691+
ent = ldap_first_entry(ld, result);
692+
CHECK_NULL(ent);
693+
694+
values = ldap_get_values(ld, ent, "krbticketpolicyreference");
695+
if (values != NULL)
696+
ldap_value_free(values);
697+
698+
values = ldap_get_values(ld, ent, "krbprincipalname");
699+
if (values != NULL) {
700+
ldap_value_free(values);
701+
if (nonkrb_only) {
702+
st = EINVAL;
703+
k5_setmsg(context, st, _("ldap object is already kerberized"));
704+
goto cleanup;
705+
}
706+
}
707+
708+
cleanup:
709+
ldap_msgfree(result);
710+
return st;
711+
}
712+
713+
static krb5_error_code
714+
validate_xargs(krb5_context context,
715+
krb5_ldap_server_handle *ldap_server_handle,
716+
const xargs_t *xargs, const char *standalone_dn,
717+
char *const *subtrees, unsigned int ntrees)
718+
{
719+
krb5_error_code st;
720+
721+
if (xargs->dn != NULL) {
722+
/* The supplied dn must be within a realm container. */
723+
st = check_dn_in_container(context, xargs->dn, subtrees, ntrees);
724+
if (st)
725+
return st;
726+
/* The supplied dn must exist without Kerberos attributes. */
727+
st = check_dn_exists(context, ldap_server_handle, xargs->dn, TRUE);
728+
if (st)
729+
return st;
730+
}
731+
732+
if (xargs->linkdn != NULL) {
733+
/* The supplied linkdn must be within a realm container. */
734+
st = check_dn_in_container(context, xargs->linkdn, subtrees, ntrees);
735+
if (st)
736+
return st;
737+
/* The supplied linkdn must exist. */
738+
st = check_dn_exists(context, ldap_server_handle, xargs->linkdn,
739+
FALSE);
740+
if (st)
741+
return st;
742+
}
743+
744+
if (xargs->containerdn != NULL && standalone_dn != NULL) {
745+
/* standalone_dn (likely composed using containerdn) must be within a
746+
* container. */
747+
st = check_dn_in_container(context, standalone_dn, subtrees, ntrees);
748+
if (st)
749+
return st;
750+
}
751+
752+
return 0;
753+
}
754+
654755
krb5_error_code
655756
krb5_ldap_put_principal(krb5_context context, krb5_db_entry *entry,
656757
char **db_args)
@@ -662,12 +763,12 @@ krb5_ldap_put_principal(krb5_context context, krb5_db_entry *entry,
662763
LDAPMessage *result=NULL, *ent=NULL;
663764
char **subtreelist = NULL;
664765
char *user=NULL, *subtree=NULL, *principal_dn=NULL;
665-
char **values=NULL, *strval[10]={NULL}, errbuf[1024];
766+
char *strval[10]={NULL}, errbuf[1024];
666767
char *filtuser=NULL;
667768
struct berval **bersecretkey=NULL;
668769
LDAPMod **mods=NULL;
669770
krb5_boolean create_standalone=FALSE;
670-
krb5_boolean krb_identity_exists=FALSE, establish_links=FALSE;
771+
krb5_boolean establish_links=FALSE;
671772
char *standalone_principal_dn=NULL;
672773
krb5_tl_data *tl_data=NULL;
673774
krb5_key_data **keys=NULL;
@@ -860,106 +961,17 @@ krb5_ldap_put_principal(krb5_context context, krb5_db_entry *entry,
860961
* any of the subtrees
861962
*/
862963
if (xargs.dn_from_kbd == TRUE) {
863-
/* make sure the DN falls in the subtree */
864-
int dnlen=0, subtreelen=0;
865-
char *dn=NULL;
866-
krb5_boolean outofsubtree=TRUE;
867-
868-
if (xargs.dn != NULL) {
869-
dn = xargs.dn;
870-
} else if (xargs.linkdn != NULL) {
871-
dn = xargs.linkdn;
872-
} else if (standalone_principal_dn != NULL) {
873-
/*
874-
* Even though the standalone_principal_dn is constructed
875-
* within this function, there is the containerdn input
876-
* from the user that can become part of the it.
877-
*/
878-
dn = standalone_principal_dn;
879-
}
880-
881964
/* Get the current subtree list if we haven't already done so. */
882965
if (subtreelist == NULL) {
883966
st = krb5_get_subtree_info(ldap_context, &subtreelist, &ntrees);
884967
if (st)
885968
goto cleanup;
886969
}
887970

888-
for (tre=0; tre<ntrees; ++tre) {
889-
if (subtreelist[tre] == NULL || strlen(subtreelist[tre]) == 0) {
890-
outofsubtree = FALSE;
891-
break;
892-
} else {
893-
dnlen = strlen (dn);
894-
subtreelen = strlen(subtreelist[tre]);
895-
if ((dnlen >= subtreelen) && (strcasecmp((dn + dnlen - subtreelen), subtreelist[tre]) == 0)) {
896-
outofsubtree = FALSE;
897-
break;
898-
}
899-
}
900-
}
901-
902-
if (outofsubtree == TRUE) {
903-
st = EINVAL;
904-
k5_setmsg(context, st, _("DN is out of the realm subtree"));
971+
st = validate_xargs(context, ldap_server_handle, &xargs,
972+
standalone_principal_dn, subtreelist, ntrees);
973+
if (st)
905974
goto cleanup;
906-
}
907-
908-
/*
909-
* dn value will be set either by dn, linkdn or the standalone_principal_dn
910-
* In the first 2 cases, the dn should be existing and in the last case we
911-
* are supposed to create the ldap object. so the below should not be
912-
* executed for the last case.
913-
*/
914-
915-
if (standalone_principal_dn == NULL) {
916-
/*
917-
* If the ldap object is missing, this results in an error.
918-
*/
919-
920-
/*
921-
* Search for krbprincipalname attribute here.
922-
* This is to find if a kerberos identity is already present
923-
* on the ldap object, in which case adding a kerberos identity
924-
* on the ldap object should result in an error.
925-
*/
926-
char *attributes[]={"krbticketpolicyreference", "krbprincipalname", NULL};
927-
928-
ldap_msgfree(result);
929-
result = NULL;
930-
LDAP_SEARCH_1(dn, LDAP_SCOPE_BASE, 0, attributes, IGNORE_STATUS);
931-
if (st == LDAP_SUCCESS) {
932-
ent = ldap_first_entry(ld, result);
933-
if (ent != NULL) {
934-
if ((values=ldap_get_values(ld, ent, "krbticketpolicyreference")) != NULL) {
935-
ldap_value_free(values);
936-
}
937-
938-
if ((values=ldap_get_values(ld, ent, "krbprincipalname")) != NULL) {
939-
krb_identity_exists = TRUE;
940-
ldap_value_free(values);
941-
}
942-
}
943-
} else {
944-
st = set_ldap_error(context, st, OP_SEARCH);
945-
goto cleanup;
946-
}
947-
}
948-
}
949-
950-
/*
951-
* If xargs.dn is set then the request is to add a
952-
* kerberos principal on a ldap object, but if
953-
* there is one already on the ldap object this
954-
* should result in an error.
955-
*/
956-
957-
if (xargs.dn != NULL && krb_identity_exists == TRUE) {
958-
st = EINVAL;
959-
snprintf(errbuf, sizeof(errbuf),
960-
_("ldap object is already kerberized"));
961-
k5_setmsg(context, st, "%s", errbuf);
962-
goto cleanup;
963975
}
964976

965977
if (xargs.linkdn != NULL) {

Diff for: src/tests/t_kdb.py

+11
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,12 @@ def ldap_add(dn, objectclass, attrs=[]):
203203
# in the test LDAP server.
204204
realm.run([kadminl, 'ank', '-randkey', '-x', 'dn=cn=krb5', 'princ1'],
205205
expected_code=1, expected_msg='DN is out of the realm subtree')
206+
# Check that the DN container check is a hierarchy test, not a simple
207+
# suffix match (CVE-2018-5730). We expect this operation to fail
208+
# either way (because "xcn" isn't a valid DN tag) but the container
209+
# check should happen before the DN is parsed.
210+
realm.run([kadminl, 'ank', '-randkey', '-x', 'dn=xcn=t1,cn=krb5', 'princ1'],
211+
expected_code=1, expected_msg='DN is out of the realm subtree')
206212
realm.run([kadminl, 'ank', '-randkey', '-x', 'dn=cn=t2,cn=krb5', 'princ1'])
207213
realm.run([kadminl, 'getprinc', 'princ1'], expected_msg='Principal: princ1')
208214
realm.run([kadminl, 'ank', '-randkey', '-x', 'dn=cn=t2,cn=krb5', 'again'],
@@ -226,6 +232,11 @@ def ldap_add(dn, objectclass, attrs=[]):
226232
'princ3'])
227233
realm.run([kadminl, 'modprinc', '-x', 'containerdn=cn=t2,cn=krb5', 'princ3'],
228234
expected_code=1, expected_msg='containerdn option not supported')
235+
# Verify that containerdn is checked when linkdn is also supplied
236+
# (CVE-2018-5730).
237+
realm.run([kadminl, 'ank', '-randkey', '-x', 'containerdn=cn=krb5',
238+
'-x', 'linkdn=cn=t2,cn=krb5', 'princ4'], expected_code=1,
239+
expected_msg='DN is out of the realm subtree')
229240

230241
# Create and modify a ticket policy.
231242
kldaputil(['create_policy', '-maxtktlife', '3hour', '-maxrenewlife', '6hour',

0 commit comments

Comments
 (0)