From 55fbf435edbe2e92dd8101669b1ce7144bc96fef Mon Sep 17 00:00:00 2001
From: Greg Hudson <ghudson@mit.edu>
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.

(cherry picked from commit b0a2f8a5365f2eec3e27d78907de9f9d2c80505a)

ticket: 9128
version_fixed: 1.21.3
---
 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 3b4f8cb83..1fcbdfbb8 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 333ee124d..f8e90c35b 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 85a9574f3..21b501731 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
@@ -265,6 +266,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
@@ -285,8 +353,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);
@@ -316,15 +383,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;
     }
@@ -332,7 +398,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 9876a11e6..882e16363 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"
@@ -109,17 +119,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)
@@ -128,15 +146,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;
@@ -150,9 +163,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();
@@ -174,9 +185,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;
@@ -206,9 +215,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;
@@ -219,6 +226,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
@@ -250,6 +269,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)
@@ -399,9 +546,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,
@@ -436,11 +581,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.2

