[krbdev.mit.edu #9223] Heap-buffer-overflow (out-of-bounds read) in PKINIT certificate-matching rule parser (parse_list_value)
"Ze Sheng via RT" <[email protected]> Sat, 20 Jun 2026 18:58:02 -0400
| Newsgroups | gmane.comp.encryption.kerberos.bugs |
|---|---|
| Message-ID | <[email protected]> |
Sat Jun 20 18:58:02 2026: Request 9223 was acted upon. Transaction: Ticket created by [email protected] Queue: krb5 Subject: Heap-buffer-overflow (out-of-bounds read) in PKINIT certificate-matching rule parser (parse_list_value) Owner: Nobody Requestors: [email protected] 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).