[17115] in Kerberos-V5-bugs

home help back first fref pref prev next nref lref last post

[krbdev.mit.edu #9223] Heap-buffer-overflow (out-of-bounds read) in

daemon@ATHENA.MIT.EDU (Ze Sheng via RT)
Sat Jun 20 18:58:09 2026

From: "Ze Sheng via RT" <rt-comment@krbdev.mit.edu>
In-Reply-To: <6a362bf9.7974f483.db227.4e79@mx.google.com>
Message-ID: <rt-4.4.3-2-2272959-1781996282-270.9223-4-0@mit.edu>
To: "AdminCc of krbdev.mit.edu Ticket #9223":;
Date: Sat, 20 Jun 2026 18:58:02 -0400
MIME-Version: 1.0
Reply-To: rt-comment@krbdev.mit.edu
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Errors-To: krb5-bugs-bounces@mit.edu


Sat Jun 20 18:58:02 2026: Request 9223 was acted upon.
 Transaction: Ticket created by zesheng@tamu.edu
       Queue: krb5
     Subject: Heap-buffer-overflow (out-of-bounds read) in PKINIT certificate-matching rule parser (parse_list_value)
       Owner: Nobody
  Requestors: zesheng@tamu.edu
      Status: new
 Ticket <URL: https://krbdev.mit.edu/rt/Ticket/Display.html?id=9223 >


# Heap-buffer-overflow (out-of-bounds read) in PKINIT certificate-matching rule parser (`parse_list_value`)

## Summary

MIT krb5's PKINIT certificate-matching rule parser reads one byte past the end
of a heap buffer when a `<KU>` / `<EKU>` keyword value is a strict prefix of a
known key-usage / extended-key-usage keyword.

The list-value parser (`parse_list_value` in
`src/plugins/preauth/pkinit/pkinit_matching.c`) matches a value token against the
keyword tables with `strncasecmp(value, ku->value, len)`, where `len` is the
length of the *input* token. A match therefore succeeds even when the token is
only a prefix of the keyword (e.g. `em` matches `emailProtection`). On a match
the parser advances the cursor by the full *keyword* length (`ku->length`)
rather than by the matched token length, jumping past the NUL terminator and
past the end of the allocation, and then dereferences the cursor (`if (*value
== ',')`).

This rule string flows from the `pkinit_cert_match` configuration value through
`pkinit_client_cert_match()` -> `parse_rule_set()` -> `parse_rule_component()`
-> `parse_list_value()`, so any party that can influence the matching rule used
during PKINIT certificate selection can drive the out-of-bounds read. With a
crafted rule such as `<EKU>em`, AddressSanitizer reports a 1-byte heap read 12
bytes past a 3-byte allocation.

- Type: heap-buffer-overflow (out-of-bounds READ, 1 byte)
- Location: `src/plugins/preauth/pkinit/pkinit_matching.c:237` in `parse_list_value`
- Affected commit: `cf20efe12e500601ad713dca9bdfe36ec8e91f32`

## Root Cause

`parse_rule_component()` copies the keyword value into an exactly-sized buffer
(`calloc(1, len+1)`, line 331). `parse_list_value()` then walks comma-separated
tokens of that buffer:

```c
/* src/plugins/preauth/pkinit/pkinit_matching.c */
do {
    found = 0;
    comma = strchr(value, ',');
    if (comma != NULL)
        len = comma - value;
    else
        len = strlen(value);          /* len = length of THIS token */

    if (type == kw_eku) {
        ku = eku_keywords;
    } else if (type == kw_ku) {
        ku = ku_keywords;
    }

    for (; ku->value != NULL; ku++) {
        if (strncasecmp(value, ku->value, len) == 0) {   /* prefix match: "em" == "emailProtection"[:2] */
            *bitptr |= ku->bitval;
            found = 1;
            break;
        }
    }
    if (found) {
        value += ku->length;          /* advances by FULL keyword length (e.g. 15), not by len (2) */
        if (*value == ',')            /* line 237: reads past end of the calloc(len+1) buffer */
            value += 1;
    } else {
        retval = EINVAL;
        goto out;
    }
} while (found && *value != '\0');
```

Because `strncasecmp` only compares `len` bytes, a 2-byte token `em` matches the
15-byte keyword `emailProtection`. `value += ku->length` then advances the
cursor by 15 over a 3-byte (`"em\0"`) allocation, and the following `*value`
dereference reads 12 bytes beyond the buffer.

## PoC

A single PKINIT certificate-matching rule string:

```
<EKU>em
```

`em` is a strict prefix of the extended-key-usage keyword `emailProtection`.
Parsing this rule copies `em` into a 3-byte heap buffer, matches it against
`emailProtection`, advances the cursor 15 bytes, and reads out of bounds.

## Reproduction

Build MIT krb5 at commit `cf20efe12e500601ad713dca9bdfe36ec8e91f32` with
AddressSanitizer, and drive the certificate-matching rule parser with the PoC
rule. The list parser is a static helper within the PKINIT preauth module, so the driver
compiles the real, unmodified parser translation unit together with `main()`
and feeds it the crafted rule (the same input an attacker-influenced
`pkinit_cert_match` value reaches via `pkinit_client_cert_match()` ->
`parse_rule_set()`):

```c
/* app.c */
#include "autoconf.h"
#include <stdio.h>
#include "../../plugins/preauth/pkinit/pkinit_matching.c"

/* minimal stubs for unrelated PKINIT crypto helpers referenced by the
 * translation unit but never reached on the rule-parsing path */
/* ... (crypto_*_matching_data, pkinit_libdefault_strings, etc.) ... */

int main(int argc, char **argv)
{
    krb5_context context = NULL;
    rule_set *rs = NULL;
    const char *match_rule = (argc > 1) ? argv[1] : "<EKU>em";

    krb5_init_context(&context);
    parse_rule_set(context, match_rule, &rs);   /* heap-buffer-overflow here */
    if (rs != NULL) free_rule_set(context, rs);
    krb5_free_context(context);
    return 0;
}
```

Build (static krb5 libraries built with the same flags):

```sh
# krb5 libraries
env CC=clang \
    CFLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1" \
    LDFLAGS="-fsanitize=address" \
    ./configure --enable-static --disable-shared \
                --without-libedit --without-readline --disable-rpath
make -C util && make -C include && make -C lib

# standalone driver
clang -O1 -g -fsanitize=address -fno-omit-frame-pointer \
    -I src/include -I src/include/krb5 -I src \
    src/tests/.../app.c \
    -Wl,--start-group src/lib/libkrb5.a src/lib/libk5crypto.a \
        src/lib/libcom_err.a src/lib/libkrb5support.a -Wl,--end-group \
    -lresolv -lkeyutils -lpthread -ldl -lm -o app
ASAN_OPTIONS=detect_leaks=0 ./app
```

Observed AddressSanitizer report:

```
parsing certificate-matching rule: "<EKU>em"
=================================================================
==4015494==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7c0e7c1e0c5f ...
READ of size 1 at 0x7c0e7c1e0c5f thread T0
    #0 parse_list_value      src/plugins/preauth/pkinit/pkinit_matching.c:237:17
    #1 parse_rule_component  src/plugins/preauth/pkinit/pkinit_matching.c:356:18
    #2 parse_rule_set        src/plugins/preauth/pkinit/pkinit_matching.c:416:15
    #3 main                  app.c:93:11

0x7c0e7c1e0c5f is located 12 bytes after 3-byte region [0x7c0e7c1e0c50,0x7c0e7c1e0c53)
allocated by thread T0 here:
    #0 calloc
    #1 parse_rule_component  src/plugins/preauth/pkinit/pkinit_matching.c:331:13
    #2 parse_rule_set        src/plugins/preauth/pkinit/pkinit_matching.c:416:15
    #3 main                  app.c:93:11

SUMMARY: AddressSanitizer: heap-buffer-overflow pkinit_matching.c:237:17 in parse_list_value
```

The faulting read is 12 bytes past the 3-byte (`"em\0"`) buffer allocated at
`pkinit_matching.c:331`.

## Suggested Fix

The parser must not advance the cursor by the keyword length when only a prefix
matched. Two complementary changes:

1. Treat a token as a match only when it equals the whole keyword, not merely a
   prefix:

   ```c
   for (; ku->value != NULL; ku++) {
       if (ku->length == (size_t)len &&
           strncasecmp(value, ku->value, len) == 0) {
           *bitptr |= ku->bitval;
           found = 1;
           break;
       }
   }
   ```

2. Advance the cursor by the consumed token length (`len`), which always lands
   on the comma or the terminating NUL inside the buffer, instead of by
   `ku->length`:

   ```c
   if (found) {
       value += len;
       if (*value == ',')
           value += 1;
   }
   ```

Either change removes the out-of-bounds read; together they also fix the
related correctness bug of accepting a prefix (e.g. `em`) as a full keyword.

## PoC bytes (self-contained)
The 7-byte rule string, base64-encoded and self-contained:

```
PEVLVT5lbQ==
```

Recreate the input file:

```sh
echo 'PEVLVT5lbQ==' | base64 -d > poc && ./app "$(cat poc)"
```

(`PEVLVT5lbQ==` decodes to the ASCII bytes `<EKU>em`.)

## Credit

Aisle Research (Ze Sheng (O2Lab & TAMU), Dmitrijs Trizna, Luigino Camastra, Guido Vranken).


_______________________________________________
krb5-bugs mailing list
krb5-bugs@mit.edu
https://mailman.mit.edu/mailman/listinfo/krb5-bugs

home help back first fref pref prev next nref lref last post