[17115] in Kerberos-V5-bugs
[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