Coordinated Disclosure Timeline

Summary

Multiple vulnerabilities in the gss-ntlmssp library can allow remote attackers to trigger a denial-of-service or memory corruption in applications using NTLM authentication.

Product

GSS-NTLMSSP

Tested Version

v1.1.0

Details

Issue 1: Memory leak when parsing usernames (GHSL-2023-010)

parse_user_name parses a given username, such as FOO\bar@example.com, into a “domain” and “username” components. There are several different username formats involving @ and \ characters that parse_user_name needs to parse and validate:

static uint32_t parse_user_name(uint32_t *minor_status,
                                const char *str, size_t len,
                                char **domain, char **username)
{
    uint32_t retmaj;
    uint32_t retmin;
    char *at, *sep;

    ...

    /* let's check if there are '@' or '\' signs */
    at = memchr(str, '@', len);                                                  // 1
    sep = memchr(str, '\\', len);                                                // 2

    /* Check if enterprise name first */
    if (at && sep) {                                                             // 3
        /* we may have an enterprise name here */
        char strbuf[len + 1];
        char *buf = strbuf;
        bool domain_handled = false;

        /* copy buf to manipulate it */
        memcpy(buf, str, len);
        buf[len] = '\0';

        /* adjust pointers relative to new buffer */
        sep = buf + (sep - str);
        at = buf + (at - str);

        ...

        for (at = strchr(buf, '@'); at != NULL; at = strchr(at, '@')) {          // 4
            if (*(at - 1) == '\\') {
                if (domain_handled) {
                    /* Invalid forms like DOM\foo\@bar or foo@bar\@baz */
                    free(*domain);
                    *domain = NULL;
                    set_GSSERR(EINVAL);
                    goto done;
                }
                /* remove escape, moving all including terminating '\0' */
                memmove(at - 1, at, len - (at - buf) + 1);
            } else if (!domain_handled) {
                /* an '@' without escape and no previous
                 * domain was split out.
                 * the rest of the string is the domain */
                *at = '\0';
                *domain = strdup(at + 1);                                        // 5
                if (NULL == *domain) {
                    set_GSSERR(ENOMEM);
                    goto done;
                }
                /* note we continue the loop to check if any invalid
                 * \@ escapes is found in the domain part */
            }
            at += 1;
        }

The code starts by searching for the first @ and \ characters in str at (1) and (2) respectively. The code at (3) checks if both characters are found which indicates an enterprise name form. After making a copy of str, the code searches for @ characters in the loop starting at (4). A copy of the remaining portion of the string is made at (5) when an @ character is found and assigned to *domain. However, the domain_handled flag is not set. If the next loop iteration finds another @ character then we reach (5) again and overwrite *domain causing a memory leak.

Impact

An attacker can leak memory via the main gss_accept_sec_context entry point, potentially causing a denial-of-service.

Issue 2: Out-of-bounds read when decoding target information (GHSL-2023-011)

NTLM tokens contain target information in attribute/value pairs. These are encoded as a list of variable-length wire_av_pair structures:

#pragma pack(push, 1)
struct wire_av_pair {
    uint16_t av_id;
    uint16_t av_len;
    uint8_t value[]; /* variable */
};
#pragma pack(pop)

ntlm_decode_target_info is responsible for decoding target information from a buffer:

int ntlm_decode_target_info(struct ntlm_ctx *ctx, struct ntlm_buffer *buffer,
                            char **nb_computer_name, char **nb_domain_name,
                            char **dns_computer_name, char **dns_domain_name,
                            char **dns_tree_name, char **av_target_name,
                            uint32_t *av_flags, uint64_t *av_timestamp,
                            struct ntlm_buffer *av_single_host,
                            struct ntlm_buffer *av_cb)
{
    struct wire_av_pair *av_pair;
    uint16_t av_id = (uint16_t)-1;
    uint16_t av_len = (uint16_t)-1;
    ...
    size_t data_offs = 0;
    uint64_t timestamp = 0;
    uint32_t flags = 0;
    int ret = 0;

    while (data_offs + 4 <= buffer->length) {
        av_pair = (struct wire_av_pair *)&buffer->data[data_offs];           // 1
        data_offs += 4;
        av_id = le16toh(av_pair->av_id);
        av_len = le16toh(av_pair->av_len);
        if (av_len > buffer->length - data_offs) {
            ret = ERR_DECODE;
            goto done;
        }
        data_offs += av_len;

        switch (av_id) {
        ...
        case MSV_AV_TIMESTAMP:
            if (!av_timestamp) continue;
            memcpy(&timestamp, av_pair->value, sizeof(timestamp));           // 2
            timestamp = le64toh(timestamp);
            break;
        case MSV_AV_FLAGS:
            if (!av_flags) continue;
            memcpy(&flags, av_pair->value, sizeof(flags));                   // 3
            flags = le32toh(flags);
            break;

buffer is iterated over and the next av_pair is extracted at (1) and the length is validated. If the attribute is a MSV_AV_TIMESTAMP then av_pair->value is copied into timestamp. However, there is no validation that av_pair->len is large enough to contain a timestamp which triggers an out-of-bounds read.

A similar vulnerability exists at (3), where the length of the MSV_AV_FLAGS attribute is not validated.

Impact

The out-of-bounds read can be triggered via the main gss_accept_sec_context entry point and could cause a denial-of-service if the memory is unmapped.

Issue 3: Incorrect free when decoding target information (GHSL-2023-012)

ntlm_decode_target_info is responsible for decoding target information:

#define safefree(x) do { free(x); x = NULL; } while(0)

...

void ntlm_free_buffer_data(struct ntlm_buffer *buf)
{
    if (!buf) return;

    safefree(buf->data);
    buf->length = 0;
}

...

int ntlm_decode_target_info(struct ntlm_ctx *ctx, struct ntlm_buffer *buffer,
                            char **nb_computer_name, char **nb_domain_name,
                            char **dns_computer_name, char **dns_domain_name,
                            char **dns_tree_name, char **av_target_name,
                            uint32_t *av_flags, uint64_t *av_timestamp,
                            struct ntlm_buffer *av_single_host,
                            struct ntlm_buffer *av_cb)
{
    struct wire_av_pair *av_pair;
    uint16_t av_id = (uint16_t)-1;
    uint16_t av_len = (uint16_t)-1;
    struct ntlm_buffer sh = { NULL, 0 };
    struct ntlm_buffer cb = { NULL, 0 };
    ...
    size_t data_offs = 0;
    ...
    int ret = 0;

    while (data_offs + 4 <= buffer->length) {
        av_pair = (struct wire_av_pair *)&buffer->data[data_offs];           // 1
        data_offs += 4;
        av_id = le16toh(av_pair->av_id);
        av_len = le16toh(av_pair->av_len);
        if (av_len > buffer->length - data_offs) {
            ret = ERR_DECODE;
            goto done;
        }
        data_offs += av_len;

        switch (av_id) {
        case MSV_AV_CHANNEL_BINDINGS:
            if (!av_cb) continue;
            cb.data = av_pair->value;
            cb.length = av_len;
            break;
        ...
        case MSV_AV_SINGLE_HOST:
            if (!av_single_host) continue;
            sh.data = av_pair->value;                                        // 2
            sh.length = av_len;
            break;
        ...
        default:
            /* unknown av_pair, or EOL */
            break;
        }
        if (av_id == MSV_AV_EOL) break;
    }

    if (av_id != MSV_AV_EOL || av_len != 0) {                                // 3
        ret = ERR_DECODE;                                                    // 4
    }

done:
    if (ret) {
        ntlm_free_buffer_data(&sh);                                          // 5
        ntlm_free_buffer_data(&cb);                                          // 6

buffer is iterated over and the next av_pair is extracted at (1) and the length is validated. If the target information contains a MSV_AV_SINGLE_HOST attribute then av_pair->value is assigned to sh.data at (2). sh.data now points to an offset into buffer->data. At (3), we validate that the list of attribute/value pairs ends with a zero-length MSV_AV_EOL attribute. If not, we set ret to an error at (4). If an error is encountered, then we free sh.data at (5). However, sh.data does not point to an allocated piece of memory; it points to an offset within buffer->data.

A similar vulnerability exists in the case of the MSV_AV_CHANNEL_BINDINGS attribute and the cb.data pointer at (6).

Impact

This vulnerability can be triggered via the main gss_accept_sec_context entry point. This will likely trigger an assertion failure in free, causing a denial-of-service. An attacker controls the contents of memory around sh.data so it may be possible to avoid the assertion failure in free and trigger memory corruption.

Issue 4: Memory corruption when decoding UTF16 strings (GHSL-2023-013)

ntlm_decode_u16l_str_hdr converts a UTF16LE encoded string into UTF8:

static int ntlm_decode_u16l_str_hdr(struct ntlm_ctx *ctx,
                                    struct wire_field_hdr *str_hdr,
                                    struct ntlm_buffer *buffer,
                                    size_t payload_offs, char **str)
{
    char *in, *out = NULL;
    uint16_t str_len;
    uint32_t str_offs;
    size_t outlen;                                                         // 1
    int ret = 0;

    str_len = le16toh(str_hdr->len);
    if (str_len == 0) goto done;

    str_offs = le32toh(str_hdr->offset);
    if ((str_offs < payload_offs) ||
        (str_offs > buffer->length) ||
        (str_offs + str_len > buffer->length)) {
        return ERR_DECODE;
    }

    in = (char *)&buffer->data[str_offs];

    out = malloc(str_len * 2 + 1);
    if (!out) return ENOMEM;

    ret = ntlm_str_convert(ctx->to_oem, in, out, str_len, &outlen);        // 2

    /* make sure to terminate output string */
    out[outlen] = '\0';                                                    // 5

done:
    if (ret) {
        safefree(out);
    }
    *str = out;
    return ret;
}
static int ntlm_str_convert(iconv_t cd,
                            const char *in, char *out,
                            size_t baselen, size_t *outlen)
{
    char *_in;
    size_t inleft, outleft;
    size_t ret;

    ret = iconv(cd, NULL, NULL, NULL, NULL);
    if (ret == -1) return errno;

    _in = discard_const(in);
    inleft = baselen;
    /* conservative max_size calculation in case lots of octects end up
     * being multiple bytes in length (in both directions) */
    outleft = baselen * 2;

    ret = iconv(cd, &_in, &inleft, &out, &outleft);                        // 3
    if (ret == -1) return errno;                                           // 4

    if (outlen) {
        *outlen = baselen * 2 - outleft;
    }
    return 0;
}

Initially at (1), outlen is uninitialized and represents the length of the converted UTF8 string. The address of outlen is passed into ntlm_str_convert at (2). iconv is called at (3) to perform the string conversion. However, if iconv encounters an error (for example, a malformed UTF16LE encoded string) then the function returns without initializing *outlen. After returning back to ntlm_decode_u16l_str_hdr, outlen is used to NUL terminate the string before checking the return value.

Impact

This vulnerability can trigger an out-of-bounds write leading to memory corruption. This vulnerability can be triggered via the main gss_accept_sec_context entry point.

Issue 5: Memory corruption when importing host-based service names (GHSL-2023-014)

gssntlm_import_name_by_mech creates a gss_name_t based on a buffer and a specified mechanism:

uint32_t gssntlm_import_name_by_mech(uint32_t *minor_status,
                                     gss_const_OID mech_type,
                                     gss_buffer_t input_name_buffer,
                                     gss_OID input_name_type,
                                     gss_name_t *output_name)
{
    ...

    if (gss_oid_equal(input_name_type, GSS_C_NT_HOSTBASED_SERVICE) ||
        gss_oid_equal(input_name_type, GSS_C_NT_HOSTBASED_SERVICE_X)) {
        char *spn = NULL;
        char *p = NULL;

        name->type = GSSNTLM_NAME_SERVER;

        if (input_name_buffer->length > 0) {
            spn = strndup(input_name_buffer->value, input_name_buffer->length);      // 1
            if (!spn) {
                set_GSSERR(ENOMEM);
                goto done;
            }
            p = memchr(spn, '@', input_name_buffer->length);                         // 2
            if (p && input_name_buffer->length == 1) {
                free(spn);
                spn = p = NULL;
            }
        }

        if (p) {
            /* Windows expects a SPN not a GSS Name */
            if (p != spn) {
                *p = '/';                                                            // 3
                name->data.server.spn = spn;
                spn = NULL;
            }

input_name_buffer is a pointer/length pair and strndup is used at (1) to make a copy with a NUL terminator. The resulting string is then searched for a @ character at (2). If input_name_buffer->value contains embedded NUL characters then strndup will only copy up to the first NUL character. This means that the length of spn would be less than input_name_buffer->length. However, memchr is used at (2) to search the first input_name_buffer->length bytes of spn, leading to an out-of-bounds read.

Impact

This vulnerability can be triggered via the main gss_import_name entry point and could cause a denial-of-service if the memory is unmapped.

This can also trigger memory corruption under certain circumstances. At (2), a memory allocation following spn could coincidentally contain a @ character which leads to p pointing to a byte outside the spn allocation. At (3), the code would then corrupt that byte when replacing the contents of p with a / character.

Issue 6: Out-of-bounds read in ntlm_decode_oem_str (GHSL-2023-019)

ntlm_decode_oem_str decodes a OEM encoded string from an NTLM buffer:

static int ntlm_decode_oem_str(struct wire_field_hdr *str_hdr,
                               struct ntlm_buffer *buffer,
                               size_t payload_offs, char **_str)
{
    uint16_t str_len;
    uint32_t str_offs;
    char *str = NULL;

    str_len = le16toh(str_hdr->len);                   // 1
    if (str_len == 0) goto done;

    str_offs = le32toh(str_hdr->offset);               // 2
    if ((str_offs < payload_offs) ||
        (str_offs > buffer->length) ||
        (str_offs + str_len > buffer->length)) {       // 3
        return ERR_DECODE;
    }

At (1) we read the length of the string as a 16-bit integer and at (2) we read the offset of the string as a 32-bit integer. At (3) the code attempts to ensure that the offset of the string plus its length is not larger than the size of the input buffer. buffer->length is a size_t and hence 64-bits on modern machines. However the calculation of str_offs + str_len is treated as a 32-bit integer and can overflow.

For example, if buffer->length is just over 4GB (0x00000001’00000001), str_offs is just below 4GB (0xffffffff) and str_len is 1 then the str_offs + str_len calculation will overflow back to zero before the comparison against buffer->length.

Impact

This vulnerability can be triggered via the main gss_accept_sec_context entry point if the application allows tokens greater than 4GB in length. This can lead to a large, up to 65KB, out-of-bounds read which could cause a denial-of-service if it reads from unmapped memory.

Issue 7: Out-of-bounds read in ntlm_decode_u16l_str_hdr (GHSL-2023-020)

Similar to issue 6, an out of bounds read can occur in the ntlm_decode_u16l_str_hdr function when decoding a UTF-16 encoded string from an NTLM buffer:

static int ntlm_decode_u16l_str_hdr(struct ntlm_ctx *ctx,
                                    struct wire_field_hdr *str_hdr,
                                    struct ntlm_buffer *buffer,
                                    size_t payload_offs, char **str)
{
    char *in, *out = NULL;
    uint16_t str_len;
    uint32_t str_offs;
    size_t outlen;
    int ret = 0;

    str_len = le16toh(str_hdr->len);                   // 1
    if (str_len == 0) goto done;

    str_offs = le32toh(str_hdr->offset);               // 2
    if ((str_offs < payload_offs) ||
        (str_offs > buffer->length) ||
        (str_offs + str_len > buffer->length)) {       // 3
        return ERR_DECODE;
    }

The length read at (1) and offset read at (2) can overflow when validating those values against the length of the buffer at (3).

Impact

This vulnerability can be triggered via the main gss_accept_sec_context entry point if the application allows tokens greater than 4GB in length. This can lead to a large, up to 65KB, out-of-bounds read which could cause a denial-of-service if it reads from unmapped memory.

Issue 8: Out-of-bounds read in ntlm_decode_field (GHSL-2023-021)

Similar to issue 6, an out of bounds read can occur in the ntlm_decode_field function when decoding a binary field from an NTLM buffer:

static int ntlm_decode_field(struct wire_field_hdr *hdr,
                             struct ntlm_buffer *buffer,
                             size_t payload_offs,
                             struct ntlm_buffer *field)
{
    struct ntlm_buffer b = { NULL, 0 };
    uint32_t offs;
    uint16_t len;

    len = le16toh(hdr->len);                           // 1
    if (len == 0) goto done;

    offs = le32toh(hdr->offset);                       // 2
    if ((offs < payload_offs) ||
        (offs > buffer->length) ||
        (offs + len > buffer->length)) {               // 3
        return ERR_DECODE;
    }

The length read at (1) and offset read at (2) can overflow when validating those values against the length of the buffer at (3).

Impact

This vulnerability can be triggered via the main gss_accept_sec_context entry point if the application allows tokens greater than 4GB in length. This can lead to a large, up to 65KB, out-of-bounds read which could cause a denial-of-service if it reads from unmapped memory.

CVE

GHSL-2023-014 was not assigned a CVE

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-010, GHSL-2023-011, GHSL-2023-012, GHSL-2023-013, GHSL-2023-014, GHSL-2023-019, GHSL-2023-020 or GHSL-2023-021 in any communication regarding these issues.