skip to content
Back to GitHub.com
Home Bounties Research Advisories CodeQL Wall of Fame Get Involved Events
March 15, 2023

GHSL-2023-016_GHSL-2023-018: Out-of-Bounds Read in the MIT Kerberos V5 (krb5) library

Phil Turnbull

Coordinated Disclosure Timeline

Summary

Multiple vulnerabilities in the MIT Kerberos V5 (krb5) library can trigger an out-of-bounds read when parsing and verifying SPNEGO tokens.

Product

MIT Kerberos V5 (krb5)

Tested Version

1.20.1-final

Details

Issue 1: out-of-bounds read in SPNEGO get_mech_set (GHSL-2023-016)

get_mech_set parses a list of mechanism OIDs from the token passed to gss_accept_sec_context. The OIDs are formatted as a sequence of DER-encoded objects:

static gss_OID_set
get_mech_set(OM_uint32 *minor_status, unsigned char **buff_in,
             unsigned int buff_length)
{
        gss_OID_set returned_mechSet;
        OM_uint32 major_status;
        int length;
        unsigned int bytes;
        OM_uint32 set_length;
        unsigned char                *start;
        int i;                                                                      // 1

        ...

        major_status = gss_create_empty_oid_set(minor_status,
                                                &returned_mechSet);
        if (major_status != GSS_S_COMPLETE)
                return (NULL);

        for (set_length = 0, i = 0; set_length < (unsigned int)length; i++) {       // 5
                gss_OID_desc *temp = get_mech_oid(minor_status, buff_in,
                        buff_length - (*buff_in - start));
                if (temp == NULL)
                        break;

                major_status = gss_add_oid_set_member(minor_status,                 // 2
                                                      temp, &returned_mechSet);
                if (major_status == GSS_S_COMPLETE)                                 // 3
                        set_length += returned_mechSet->elements[i].length +2;      // 4
                generic_gss_release_oid(minor_status, &temp);
        }

        return (returned_mechSet);
}

At (1), i represents the number of OIDs contained in returned_mechSet and is used in the main processing loop.

At (2) the function attempts to insert temp into returned_mechSet. This operation can possibly fail. The return code is checked at (3) and the logic at (4) is skipped if gss_add_oid_set_member failed. An attacker can cause gss_add_oid_set_member to fail by adding a zero-length OID to the list of mechanisms.

However, on the next loop iteration, (5), i is incremented even though we did not add an OID to returned_mechSet. If the next loop iteration successfully inserts an OID into returned_mechSet then the returned_mechSet->elements[i] logic at (4) is triggered. This causes an out-of-bounds read because i is greater than the number of elements in returned_mechSet.

By including multiple zero-length OIDs, we can increment i arbitrarily and control the out-of-bounds read.

Impact

This out of bounds read can lead to a denial of service if the accessed memory is not mapped.

Issue 2: out-of-bounds read in SPNEGO g_verify_neg_token_init (GHSL-2023-017)

g_verify_neg_token_init validates a SPNEGO token provided via gss_accept_sec_context:

static int
g_verify_neg_token_init(unsigned char **buf_in, unsigned int cur_size)
{
        unsigned char *buf = *buf_in;
        unsigned char *endptr = buf + cur_size;
        int seqsize;
        int ret = 0;
        unsigned int bytes;

        /*
         * Verify this is a NegotiationToken type token
         * - check for a0(context specific identifier)
         * - get length and verify that enoughd ata exists
         */
        if (g_get_tag_and_length(&buf, CONTEXT, cur_size, &bytes) < 0)                  // 1
                return (G_BAD_TOK_HEADER);

        cur_size = bytes; /* should indicate bytes remaining */

        /*
         * Verify the next piece, it should identify this as
         * a strucure of type NegTokenInit.
         */
        if (*buf++ == SEQUENCE) {                                                       // 2
                if ((seqsize = gssint_get_der_length(&buf, cur_size, &bytes)) < 0)      // 3
                        return (G_BAD_TOK_HEADER);
                /*
                 * Make sure we have the entire buffer as described
                 */
                if (seqsize > endptr - buf)
                        return (G_BAD_TOK_HEADER);
        } else {
                return (G_BAD_TOK_HEADER);
        }

        cur_size = seqsize; /* should indicate bytes remaining */

        /*
         * Verify that the first blob is a sequence of mechTypes
         */
        if (*buf++ == CONTEXT) {                                                        // 4
                if ((seqsize = gssint_get_der_length(&buf, cur_size, &bytes)) < 0)
                        return (G_BAD_TOK_HEADER);
                ...

At (1), we use g_get_tag_and_length to check for a CONTEXT tag and decode the following length bytes. g_get_tag_and_length returns a negative value on error or zero on success. On success it places the decoded length in bytes. Zero length sequences are valid in ASN.1 so g_get_tag_and_length does not validate that bytes is non-zero. At (2), buf is dereferenced without validating that cur_size/bytes is non-zero which can trigger an out-of-bounds read.

A similar vulnerability exists at (3) and (4). The gssint_get_der_length call at (3) will return a negative value on error or the length of the ASN.1 object on success. There is no validation that the sequence length (cur_size/seqsize) is non-zero which causes an out-of-bounds read at (4).

Impact

These out of bounds reads could lead to a denial of service if the accessed memory is not mapped.

Issue 3: invalid length calculation in SPNEGO g_verify_neg_token_init (GHSL-2023-018)

In the same function, g_verify_neg_token_init, the cur_size variable does not take into account the tag byte of the ASN.1 object when calculating the number of bytes in an ASN.1 object:

static int
g_verify_neg_token_init(unsigned char **buf_in, unsigned int cur_size)
{
        unsigned char *buf = *buf_in;
        unsigned char *endptr = buf + cur_size;
        int seqsize;
        int ret = 0;
        unsigned int bytes;

        /*
         * Verify this is a NegotiationToken type token
         * - check for a0(context specific identifier)
         * - get length and verify that enoughd ata exists
         */
        if (g_get_tag_and_length(&buf, CONTEXT, cur_size, &bytes) < 0)                  // 1
                return (G_BAD_TOK_HEADER);

        cur_size = bytes; /* should indicate bytes remaining */                         // 2

        /*
         * Verify the next piece, it should identify this as
         * a strucure of type NegTokenInit.
         */
        if (*buf++ == SEQUENCE) {                                                       // 3
                if ((seqsize = gssint_get_der_length(&buf, cur_size, &bytes)) < 0)      // 4
                        return (G_BAD_TOK_HEADER);
                /*
                 * Make sure we have the entire buffer as described
                 */
                if (seqsize > endptr - buf)
                        return (G_BAD_TOK_HEADER);
        } else {
                return (G_BAD_TOK_HEADER);
        }

        cur_size = seqsize; /* should indicate bytes remaining */                       // 5

        /*
         * Verify that the first blob is a sequence of mechTypes
         */
        if (*buf++ == CONTEXT) {                                                        // 6
                if ((seqsize = gssint_get_der_length(&buf, cur_size, &bytes)) < 0)      // 7
                        return (G_BAD_TOK_HEADER);
                ...

At (1), we read the size of the object into bytes, which is then assigned to cur_size at (2). This represents the number of bytes in the object including the tag, length bytes and value. At (3), we inspect the tag byte of the object, *buf, to verify that it is a SEQUENCE object. At the same time we increment buf but do not decrement cur_size. We then call gssint_get_der_length with cur_size at (4). This can then trigger an out-of-bounds read in gssint_get_der_length.

A similar vulnerability exists starting at (4). The result of the gssint_get_der_length call is the size of the SEQUENCE object including the tag, length bytes and value. This is assigned to cur_size at (5), which is not is not decremented when reading the tag byte at (6). This can trigger a similar out-of-bounds read in the gssint_get_der_length call at (7).

Impact

These out of bounds reads could lead to a denial of service if the accessed memory is not mapped.

Credit

These issues were discovered and reported by GHSL team member @philipturnbull (Phil Turnbull).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2023-016, GHSL-2023-017, or GHSL-2023-018 in any communication regarding these issues.