diff --git a/SOURCES/0016-Fix-vulnerabilities-in-GSS-message-token-handling.patch b/SOURCES/0016-Fix-vulnerabilities-in-GSS-message-token-handling.patch new file mode 100644 index 0000000..4377fd4 --- /dev/null +++ b/SOURCES/0016-Fix-vulnerabilities-in-GSS-message-token-handling.patch @@ -0,0 +1,535 @@ +From 7d9b143d1749cee1dc81c6b0f5a5493534bc6630 Mon Sep 17 00:00:00 2001 +From: Greg Hudson +Date: Fri, 14 Jun 2024 10:56:12 -0400 +Subject: [PATCH] Fix vulnerabilities in GSS message token handling + +In gss_krb5int_unseal_token_v3() and gss_krb5int_unseal_v3_iov(), +verify the Extra Count field of CFX wrap tokens against the encrypted +header. Reported by Jacob Champion. + +In gss_krb5int_unseal_token_v3(), check for a decrypted plaintext +length too short to contain the encrypted header and extra count +bytes. Reported by Jacob Champion. + +In kg_unseal_iov_token(), separately track the header IOV length and +complete token length when parsing the token's ASN.1 wrapper. This +fix contains modified versions of functions from k5-der.h and +util_token.c; this duplication will be cleaned up in a future commit. + +CVE-2024-37370: + +In MIT krb5 release 1.3 and later, an attacker can modify the +plaintext Extra Count field of a confidential GSS krb5 wrap token, +causing the unwrapped token to appear truncated to the application. + +CVE-2024-37371: + +In MIT krb5 release 1.3 and later, an attacker can cause invalid +memory reads by sending message tokens with invalid length fields. + +ticket: 9128 (new) +tags: pullup +target_version: 1.21-next + +(cherry picked from commit b0a2f8a5365f2eec3e27d78907de9f9d2c80505a) +--- + src/lib/gssapi/krb5/k5sealv3.c | 5 + + src/lib/gssapi/krb5/k5sealv3iov.c | 3 +- + src/lib/gssapi/krb5/k5unsealiov.c | 80 +++++++++- + src/tests/gssapi/t_invalid.c | 233 +++++++++++++++++++++++++----- + 4 files changed, 275 insertions(+), 46 deletions(-) + +diff --git a/src/lib/gssapi/krb5/k5sealv3.c b/src/lib/gssapi/krb5/k5sealv3.c +index 3b4f8cb837..1fcbdfbb87 100644 +--- a/src/lib/gssapi/krb5/k5sealv3.c ++++ b/src/lib/gssapi/krb5/k5sealv3.c +@@ -408,10 +408,15 @@ gss_krb5int_unseal_token_v3(krb5_context *contextptr, + /* Don't use bodysize here! Use the fact that + cipher.ciphertext.length has been adjusted to the + correct length. */ ++ if (plain.length < 16 + ec) { ++ free(plain.data); ++ goto defective; ++ } + althdr = (unsigned char *)plain.data + plain.length - 16; + if (load_16_be(althdr) != KG2_TOK_WRAP_MSG + || althdr[2] != ptr[2] + || althdr[3] != ptr[3] ++ || load_16_be(althdr+4) != ec + || memcmp(althdr+8, ptr+8, 8)) { + free(plain.data); + goto defective; +diff --git a/src/lib/gssapi/krb5/k5sealv3iov.c b/src/lib/gssapi/krb5/k5sealv3iov.c +index 333ee124dd..f8e90c35b4 100644 +--- a/src/lib/gssapi/krb5/k5sealv3iov.c ++++ b/src/lib/gssapi/krb5/k5sealv3iov.c +@@ -402,9 +402,10 @@ gss_krb5int_unseal_v3_iov(krb5_context context, + if (load_16_be(althdr) != KG2_TOK_WRAP_MSG + || althdr[2] != ptr[2] + || althdr[3] != ptr[3] ++ || load_16_be(althdr + 4) != ec + || memcmp(althdr + 8, ptr + 8, 8) != 0) { + *minor_status = 0; +- return GSS_S_BAD_SIG; ++ return GSS_S_DEFECTIVE_TOKEN; + } + } else { + /* Verify checksum: note EC is checksum size here, not padding */ +diff --git a/src/lib/gssapi/krb5/k5unsealiov.c b/src/lib/gssapi/krb5/k5unsealiov.c +index 3ce2a90ce9..6a6585d9af 100644 +--- a/src/lib/gssapi/krb5/k5unsealiov.c ++++ b/src/lib/gssapi/krb5/k5unsealiov.c +@@ -25,6 +25,7 @@ + */ + + #include "k5-int.h" ++#include "k5-der.h" + #include "gssapiP_krb5.h" + + static OM_uint32 +@@ -247,6 +248,73 @@ cleanup: + return retval; + } + ++/* Similar to k5_der_get_value(), but output an unchecked content length ++ * instead of a k5input containing the contents. */ ++static inline bool ++get_der_tag(struct k5input *in, uint8_t idbyte, size_t *len_out) ++{ ++ uint8_t lenbyte, i; ++ size_t len; ++ ++ /* Do nothing if in is empty or the next byte doesn't match idbyte. */ ++ if (in->status || in->len == 0 || *in->ptr != idbyte) ++ return false; ++ ++ /* Advance past the identifier byte and decode the length. */ ++ (void)k5_input_get_byte(in); ++ lenbyte = k5_input_get_byte(in); ++ if (lenbyte < 128) { ++ len = lenbyte; ++ } else { ++ len = 0; ++ for (i = 0; i < (lenbyte & 0x7F); i++) { ++ if (len > (SIZE_MAX >> 8)) { ++ k5_input_set_status(in, EOVERFLOW); ++ return false; ++ } ++ len = (len << 8) | k5_input_get_byte(in); ++ } ++ } ++ ++ if (in->status) ++ return false; ++ ++ *len_out = len; ++ return true; ++} ++ ++/* ++ * Similar to g_verify_token_header() without toktype or flags, but do not read ++ * more than *header_len bytes of ASN.1 wrapper, and on output set *header_len ++ * to the remaining number of header bytes. Verify the outer DER tag's length ++ * against token_len, which may be larger (but not smaller) than *header_len. ++ */ ++static gss_int32 ++verify_detached_wrapper(const gss_OID_desc *mech, size_t *header_len, ++ uint8_t **header_in, size_t token_len) ++{ ++ struct k5input in, mech_der; ++ gss_OID_desc toid; ++ size_t len; ++ ++ k5_input_init(&in, *header_in, *header_len); ++ ++ if (get_der_tag(&in, 0x60, &len)) { ++ if (len != token_len - (in.ptr - *header_in)) ++ return G_BAD_TOK_HEADER; ++ if (!k5_der_get_value(&in, 0x06, &mech_der)) ++ return G_BAD_TOK_HEADER; ++ toid.elements = (uint8_t *)mech_der.ptr; ++ toid.length = mech_der.len; ++ if (!g_OID_equal(&toid, mech)) ++ return G_WRONG_MECH; ++ } ++ ++ *header_in = (uint8_t *)in.ptr; ++ *header_len = in.len; ++ return 0; ++} ++ + /* + * Caller must provide TOKEN | DATA | PADDING | TRAILER, except + * for DCE in which case it can just provide TOKEN | DATA (must +@@ -267,8 +335,7 @@ kg_unseal_iov_token(OM_uint32 *minor_status, + gss_iov_buffer_t header; + gss_iov_buffer_t padding; + gss_iov_buffer_t trailer; +- size_t input_length; +- unsigned int bodysize; ++ size_t input_length, hlen; + int toktype2; + + header = kg_locate_header_iov(iov, iov_count, toktype); +@@ -298,15 +365,14 @@ kg_unseal_iov_token(OM_uint32 *minor_status, + input_length += trailer->buffer.length; + } + +- code = g_verify_token_header(ctx->mech_used, +- &bodysize, &ptr, -1, +- input_length, 0); ++ hlen = header->buffer.length; ++ code = verify_detached_wrapper(ctx->mech_used, &hlen, &ptr, input_length); + if (code != 0) { + *minor_status = code; + return GSS_S_DEFECTIVE_TOKEN; + } + +- if (bodysize < 2) { ++ if (hlen < 2) { + *minor_status = (OM_uint32)G_BAD_TOK_HEADER; + return GSS_S_DEFECTIVE_TOKEN; + } +@@ -314,7 +380,7 @@ kg_unseal_iov_token(OM_uint32 *minor_status, + toktype2 = load_16_be(ptr); + + ptr += 2; +- bodysize -= 2; ++ hlen -= 2; + + switch (toktype2) { + case KG2_TOK_MIC_MSG: +diff --git a/src/tests/gssapi/t_invalid.c b/src/tests/gssapi/t_invalid.c +index fb8fe55111..8192935099 100644 +--- a/src/tests/gssapi/t_invalid.c ++++ b/src/tests/gssapi/t_invalid.c +@@ -36,31 +36,41 @@ + * + * 1. A pre-CFX wrap or MIC token processed with a CFX-only context causes a + * null pointer dereference. (The token must use SEAL_ALG_NONE or it will +- * be rejected.) ++ * be rejected.) This vulnerability also applies to IOV unwrap. + * +- * 2. A pre-CFX wrap or MIC token with fewer than 24 bytes after the ASN.1 ++ * 2. A CFX wrap token with a different value of EC between the plaintext and ++ * encrypted copies will be erroneously accepted, which allows a message ++ * truncation attack. This vulnerability also applies to IOV unwrap. ++ * ++ * 3. A CFX wrap token with a plaintext length fewer than 16 bytes causes an ++ * access before the beginning of the input buffer, possibly leading to a ++ * crash. ++ * ++ * 4. A CFX wrap token with a plaintext EC value greater than the plaintext ++ * length - 16 causes an integer underflow when computing the result length, ++ * likely causing a crash. ++ * ++ * 5. An IOV unwrap operation will overrun the header buffer if an ASN.1 ++ * wrapper longer than the header buffer is present. ++ * ++ * 6. A pre-CFX wrap or MIC token with fewer than 24 bytes after the ASN.1 + * header causes an input buffer overrun, usually leading to either a segv + * or a GSS_S_DEFECTIVE_TOKEN error due to garbage algorithm, filler, or +- * sequence number values. ++ * sequence number values. This vulnerability also applies to IOV unwrap. + * +- * 3. A pre-CFX wrap token with fewer than 16 + cksumlen bytes after the ASN.1 ++ * 7. A pre-CFX wrap token with fewer than 16 + cksumlen bytes after the ASN.1 + * header causes an integer underflow when computing the ciphertext length, + * leading to an allocation error on 32-bit platforms or a segv on 64-bit + * platforms. A pre-CFX MIC token of this size causes an input buffer + * overrun when comparing the checksum, perhaps leading to a segv. + * +- * 4. A pre-CFX wrap token with fewer than conflen + padlen bytes in the ++ * 8. A pre-CFX wrap token with fewer than conflen + padlen bytes in the + * ciphertext (where padlen is the last byte of the decrypted ciphertext) + * causes an integer underflow when computing the original message length, + * leading to an allocation error. + * +- * 5. In the mechglue, truncated encapsulation in the initial context token can ++ * 9. In the mechglue, truncated encapsulation in the initial context token can + * cause input buffer overruns in gss_accept_sec_context(). +- * +- * Vulnerabilities #1 and #2 also apply to IOV unwrap, although tokens with +- * fewer than 16 bytes after the ASN.1 header will be rejected. +- * Vulnerabilities #2 and #5 can only be robustly detected using a +- * memory-checking environment such as valgrind. + */ + + #include "k5-int.h" +@@ -97,17 +107,25 @@ struct test { + } + }; + +-/* Fake up enough of a CFX GSS context for gss_unwrap, using an AES key. */ ++static void * ++ealloc(size_t len) ++{ ++ void *ptr = calloc(len, 1); ++ ++ if (ptr == NULL) ++ abort(); ++ return ptr; ++} ++ ++/* Fake up enough of a CFX GSS context for gss_unwrap, using an AES key. ++ * The context takes ownership of subkey. */ + static gss_ctx_id_t +-make_fake_cfx_context() ++make_fake_cfx_context(krb5_key subkey) + { + gss_union_ctx_id_t uctx; + krb5_gss_ctx_id_t kgctx; +- krb5_keyblock kb; + +- kgctx = calloc(1, sizeof(*kgctx)); +- if (kgctx == NULL) +- abort(); ++ kgctx = ealloc(sizeof(*kgctx)); + kgctx->established = 1; + kgctx->proto = 1; + if (g_seqstate_init(&kgctx->seqstate, 0, 0, 0, 0) != 0) +@@ -116,15 +134,10 @@ make_fake_cfx_context() + kgctx->sealalg = -1; + kgctx->signalg = -1; + +- kb.enctype = ENCTYPE_AES128_CTS_HMAC_SHA1_96; +- kb.length = 16; +- kb.contents = (unsigned char *)"1234567887654321"; +- if (krb5_k_create_key(NULL, &kb, &kgctx->subkey) != 0) +- abort(); ++ kgctx->subkey = subkey; ++ kgctx->cksumtype = CKSUMTYPE_HMAC_SHA1_96_AES128; + +- uctx = calloc(1, sizeof(*uctx)); +- if (uctx == NULL) +- abort(); ++ uctx = ealloc(sizeof(*uctx)); + uctx->mech_type = &mech_krb5; + uctx->internal_ctx_id = (gss_ctx_id_t)kgctx; + return (gss_ctx_id_t)uctx; +@@ -138,9 +151,7 @@ make_fake_context(const struct test *test) + krb5_gss_ctx_id_t kgctx; + krb5_keyblock kb; + +- kgctx = calloc(1, sizeof(*kgctx)); +- if (kgctx == NULL) +- abort(); ++ kgctx = ealloc(sizeof(*kgctx)); + kgctx->established = 1; + if (g_seqstate_init(&kgctx->seqstate, 0, 0, 0, 0) != 0) + abort(); +@@ -162,9 +173,7 @@ make_fake_context(const struct test *test) + if (krb5_k_create_key(NULL, &kb, &kgctx->enc) != 0) + abort(); + +- uctx = calloc(1, sizeof(*uctx)); +- if (uctx == NULL) +- abort(); ++ uctx = ealloc(sizeof(*uctx)); + uctx->mech_type = &mech_krb5; + uctx->internal_ctx_id = (gss_ctx_id_t)kgctx; + return (gss_ctx_id_t)uctx; +@@ -194,9 +203,7 @@ make_token(unsigned char *token, size_t len, gss_buffer_t out) + + assert(mech_krb5.length == 9); + assert(len + 11 < 128); +- wrapped = malloc(len + 13); +- if (wrapped == NULL) +- abort(); ++ wrapped = ealloc(len + 13); + wrapped[0] = 0x60; + wrapped[1] = len + 11; + wrapped[2] = 0x06; +@@ -207,6 +214,18 @@ make_token(unsigned char *token, size_t len, gss_buffer_t out) + out->value = wrapped; + } + ++/* Create a 16-byte header for a CFX confidential wrap token to be processed by ++ * the fake CFX context. */ ++static void ++write_cfx_header(uint16_t ec, uint8_t *out) ++{ ++ memset(out, 0, 16); ++ store_16_be(KG2_TOK_WRAP_MSG, out); ++ out[2] = FLAG_WRAP_CONFIDENTIAL; ++ out[3] = 0xFF; ++ store_16_be(ec, out + 4); ++} ++ + /* Unwrap a superficially valid RFC 1964 token with a CFX-only context, with + * regular and IOV unwrap. */ + static void +@@ -238,6 +257,134 @@ test_bogus_1964_token(gss_ctx_id_t ctx) + free(in.value); + } + ++static void ++test_cfx_altered_ec(gss_ctx_id_t ctx, krb5_key subkey) ++{ ++ OM_uint32 major, minor; ++ uint8_t tokbuf[128], plainbuf[24]; ++ krb5_data plain; ++ krb5_enc_data cipher; ++ gss_buffer_desc in, out; ++ gss_iov_buffer_desc iov[2]; ++ ++ /* Construct a header with a plaintext EC value of 3. */ ++ write_cfx_header(3, tokbuf); ++ ++ /* Encrypt a plaintext and a copy of the header with the EC value 0. */ ++ memcpy(plainbuf, "truncate", 8); ++ memcpy(plainbuf + 8, tokbuf, 16); ++ store_16_be(0, plainbuf + 12); ++ plain = make_data(plainbuf, 24); ++ cipher.ciphertext.data = (char *)tokbuf + 16; ++ cipher.ciphertext.length = sizeof(tokbuf) - 16; ++ cipher.enctype = subkey->keyblock.enctype; ++ if (krb5_k_encrypt(NULL, subkey, KG_USAGE_INITIATOR_SEAL, NULL, ++ &plain, &cipher) != 0) ++ abort(); ++ ++ /* Verify that the token is rejected by gss_unwrap(). */ ++ in.value = tokbuf; ++ in.length = 16 + cipher.ciphertext.length; ++ major = gss_unwrap(&minor, ctx, &in, &out, NULL, NULL); ++ if (major != GSS_S_DEFECTIVE_TOKEN) ++ abort(); ++ (void)gss_release_buffer(&minor, &out); ++ ++ /* Verify that the token is rejected by gss_unwrap_iov(). */ ++ iov[0].type = GSS_IOV_BUFFER_TYPE_STREAM; ++ iov[0].buffer = in; ++ iov[1].type = GSS_IOV_BUFFER_TYPE_DATA; ++ major = gss_unwrap_iov(&minor, ctx, NULL, NULL, iov, 2); ++ if (major != GSS_S_DEFECTIVE_TOKEN) ++ abort(); ++} ++ ++static void ++test_cfx_short_plaintext(gss_ctx_id_t ctx, krb5_key subkey) ++{ ++ OM_uint32 major, minor; ++ uint8_t tokbuf[128], zerobyte = 0; ++ krb5_data plain; ++ krb5_enc_data cipher; ++ gss_buffer_desc in, out; ++ ++ write_cfx_header(0, tokbuf); ++ ++ /* Encrypt a single byte, with no copy of the header. */ ++ plain = make_data(&zerobyte, 1); ++ cipher.ciphertext.data = (char *)tokbuf + 16; ++ cipher.ciphertext.length = sizeof(tokbuf) - 16; ++ cipher.enctype = subkey->keyblock.enctype; ++ if (krb5_k_encrypt(NULL, subkey, KG_USAGE_INITIATOR_SEAL, NULL, ++ &plain, &cipher) != 0) ++ abort(); ++ ++ /* Verify that the token is rejected by gss_unwrap(). */ ++ in.value = tokbuf; ++ in.length = 16 + cipher.ciphertext.length; ++ major = gss_unwrap(&minor, ctx, &in, &out, NULL, NULL); ++ if (major != GSS_S_DEFECTIVE_TOKEN) ++ abort(); ++ (void)gss_release_buffer(&minor, &out); ++} ++ ++static void ++test_cfx_large_ec(gss_ctx_id_t ctx, krb5_key subkey) ++{ ++ OM_uint32 major, minor; ++ uint8_t tokbuf[128] = { 0 }, plainbuf[20]; ++ krb5_data plain; ++ krb5_enc_data cipher; ++ gss_buffer_desc in, out; ++ ++ /* Construct a header with an EC value of 5. */ ++ write_cfx_header(5, tokbuf); ++ ++ /* Encrypt a 4-byte plaintext plus the header. */ ++ memcpy(plainbuf, "abcd", 4); ++ memcpy(plainbuf + 4, tokbuf, 16); ++ plain = make_data(plainbuf, 20); ++ cipher.ciphertext.data = (char *)tokbuf + 16; ++ cipher.ciphertext.length = sizeof(tokbuf) - 16; ++ cipher.enctype = subkey->keyblock.enctype; ++ if (krb5_k_encrypt(NULL, subkey, KG_USAGE_INITIATOR_SEAL, NULL, ++ &plain, &cipher) != 0) ++ abort(); ++ ++ /* Verify that the token is rejected by gss_unwrap(). */ ++ in.value = tokbuf; ++ in.length = 16 + cipher.ciphertext.length; ++ major = gss_unwrap(&minor, ctx, &in, &out, NULL, NULL); ++ if (major != GSS_S_DEFECTIVE_TOKEN) ++ abort(); ++ (void)gss_release_buffer(&minor, &out); ++} ++ ++static void ++test_iov_large_asn1_wrapper(gss_ctx_id_t ctx) ++{ ++ OM_uint32 minor, major; ++ uint8_t databuf[10] = { 0 }; ++ gss_iov_buffer_desc iov[2]; ++ ++ /* ++ * In this IOV array, the header contains a DER tag with a dangling eight ++ * bytes of length field. The data IOV indicates a total token length ++ * sufficient to contain the length bytes. ++ */ ++ iov[0].type = GSS_IOV_BUFFER_TYPE_HEADER; ++ iov[0].buffer.value = ealloc(2); ++ iov[0].buffer.length = 2; ++ memcpy(iov[0].buffer.value, "\x60\x88", 2); ++ iov[1].type = GSS_IOV_BUFFER_TYPE_DATA; ++ iov[1].buffer.value = databuf; ++ iov[1].buffer.length = 10; ++ major = gss_unwrap_iov(&minor, ctx, NULL, NULL, iov, 2); ++ if (major != GSS_S_DEFECTIVE_TOKEN) ++ abort(); ++ free(iov[0].buffer.value); ++} ++ + /* Process wrap and MIC tokens with incomplete headers. */ + static void + test_short_header(gss_ctx_id_t ctx) +@@ -387,9 +534,7 @@ try_accept(void *value, size_t len) + gss_ctx_id_t ctx = GSS_C_NO_CONTEXT; + + /* Copy the provided value to make input overruns more obvious. */ +- in.value = malloc(len); +- if (in.value == NULL) +- abort(); ++ in.value = ealloc(len); + memcpy(in.value, value, len); + in.length = len; + (void)gss_accept_sec_context(&minor, &ctx, GSS_C_NO_CREDENTIAL, &in, +@@ -424,11 +569,23 @@ test_short_encapsulation() + int + main(int argc, char **argv) + { ++ krb5_keyblock kb; ++ krb5_key cfx_subkey; + gss_ctx_id_t ctx; + size_t i; + +- ctx = make_fake_cfx_context(); ++ kb.enctype = ENCTYPE_AES128_CTS_HMAC_SHA1_96; ++ kb.length = 16; ++ kb.contents = (unsigned char *)"1234567887654321"; ++ if (krb5_k_create_key(NULL, &kb, &cfx_subkey) != 0) ++ abort(); ++ ++ ctx = make_fake_cfx_context(cfx_subkey); + test_bogus_1964_token(ctx); ++ test_cfx_altered_ec(ctx, cfx_subkey); ++ test_cfx_short_plaintext(ctx, cfx_subkey); ++ test_cfx_large_ec(ctx, cfx_subkey); ++ test_iov_large_asn1_wrapper(ctx); + free_fake_context(ctx); + + for (i = 0; i < sizeof(tests) / sizeof(*tests); i++) { +-- +2.45.1 + diff --git a/SPECS/krb5.spec b/SPECS/krb5.spec index c318f57..9348531 100644 --- a/SPECS/krb5.spec +++ b/SPECS/krb5.spec @@ -34,7 +34,7 @@ # # baserelease is what we have standardized across Fedora and what # rpmdev-bumpspec knows how to handle. -%global baserelease 1 +%global baserelease 2 # This should be e.g. beta1 or %%nil %global pre_release %nil @@ -98,6 +98,7 @@ Patch0012: 0012-downstream-Allow-to-set-PAC-ticket-signature-as-opti.patch Patch0013: 0013-downstream-Make-PKINIT-CMS-SHA-1-signature-verificat.patch Patch0014: 0014-Enable-PKINIT-if-at-least-one-group-is-available.patch Patch0015: 0015-Fix-double-free-in-KDC-TGS-processing.patch +Patch0016: 0016-Fix-vulnerabilities-in-GSS-message-token-handling.patch License: MIT URL: https://web.mit.edu/kerberos/www/ @@ -662,6 +663,11 @@ exit 0 %{_libdir}/libkadm5srv_mit.so.* %changelog +* Mon Jul 01 2024 Julien Rische - 1.21.1-2 +- CVE-2024-37370 CVE-2024-37371 + Fix vulnerabilities in GSS message token handling + Resolves: RHEL-45401 RHEL-45390 + * Tue Aug 08 2023 Julien Rische - 1.21.1-1 - New upstream version (1.21.1) - Fix double-free in KDC TGS processing (CVE-2023-39975)