Coordinated Disclosure Timeline
- 2023-02-06: Issues reported to maintainer
- 2023-02-07: Report acknowledged
- 2023-02-10: CVEs assigned
- 2023-02-12: v1.2.0 released
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
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(×tamp, 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
- CVE-2023-25563 - GHSL-2023-019, GHSL-2023-20, GHSL-2023-21
- CVE-2023-25564 - GHSL-2023-013
- CVE-2023-25565 - GHSL-2023-012
- CVE-2023-25566 - GHSL-2023-010
- CVE-2023-25567 - GHSL-2023-011
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.